[ contenidos | #winprog ]

Timers y Animaciones

Ejemplo: anim_one

[images/anim_one.gif]

Antes de comenzar...

Antes que veamos cosas animadas necesitamos crear una estructura para almacenar la posici�n de la esfera entre las actualizaciones. Esta estructura almacenar� la posici�n actual y el tama�o de la esfera, como as� tambi�n los valores delta (cuanto queremos mover la esfera en cada frame).

Una vez que hemos declarado el tipo de la estructura, tambi�n declaramos una instancia global de la misma. Esto est� bien, debido a que solo tenemos una esfera, si fueramos a animar varias de ellas deberiamos probablemente usar un arreglo o una lista para contenerlas de una forma mas conveniente.

Tambi�n hemos definido una constante BALL_MOVE_DELTA la cual indica cuan lejos queremos que la esfera se desplace en cada actualizaci�n. La raz�n para almacenar deltas en la estructura BALLINFO es que queremos mover la esfera hacia arriba o hacia abajo, hacia izquierda o derecha, de forma independiente, BALL_MOVE_DELTA es s�lo un nombre significativo que nos facilitar� la tarea de cambiar el valor en el futuro.

Ahora necesitamos inicializar la estructura despu�s de que cargamos nuestros bitmaps:

    BITMAP bm;
    GetObject(g_hbmBall, sizeof(bm), &bm);

    ZeroMemory(&g_ballInfo, sizeof(g_ballInfo));
    g_ballInfo.width = bm.bmWidth;
    g_ballInfo.height = bm.bmHeight;

    g_ballInfo.dx = BALL_MOVE_DELTA;
    g_ballInfo.dy = BALL_MOVE_DELTA;

La esfera comienza en la esquina superior izquierda, movi�ndose hacia la derecha y bajando de acuerdo a los miembros dx y dy de la estructura BALLINFO.

Configurando el Timer

La forma m�s f�cil de agregar un timer dentro de un programa en Windows es con SetTimer(), aunque no es la mejor forma y tampoco es la forma recomendada para aplicaci�nes multimedia o juegos, sin embargo es lo suficientemente buena para aplicaciones simples como �sta. Cuando necesites algo mejor, �chale una mirada a timeSetEvent() en MSDN, la cual es mas apropiada.
const int ID_TIMER = 1;
    ret = SetTimer(hwnd, ID_TIMER, 50, NULL);
    if(ret == 0)
        MessageBox(hwnd, "Could not SetTimer()!", "Error", MB_OK | MB_ICONEXCLAMATION);

Aqu� hemos declarado un ID para el timer para que podamos referirnos a �ste posteriormente (para eliminarlo) y pusimos el timer en el manejador del mensaje WM_CREATE de nuestra ventana principal. Cada vez que transcurre un determinado tiempo, el timer env�a un mensaje WM_TIMER a la ventana y pasa como par�metro en wParam su ID. Debido a que s�lo tenemos un timer no necesitamos el ID, pero es muy �til si utilizamos mas de uno.

Hemos configurado el timer para que env�e una se�al cada 50 milisegundos, lo cual resulta en aproximadamente 20 cuadros por segundo. He dicho aproximadamente debido a que, como dije, SetTimer( ) es un poco impresiso, pero este no es un c�digo cr�tico donde milisegundos mas milisegundos menos causar�n estragos, por lo tanto podemos usarlo.

Animaci�n en WM_TIMER

Ahora, cuando obtenemos el mensaje WM_TIMER queremos calcular la nueva posici�n de la esfera y dibujarla en la nueva posici�n.

    case WM_TIMER:
    {
        RECT rcClient;
        HDC hdc = GetDC(hwnd);

        GetClientRect(hwnd, &rcClient);

        UpdateBall(&rcClient);
        DrawBall(hdc, &rcClient);

        ReleaseDC(hwnd, hdc);
    }
    break;

He puesto el c�digo para actualizar y dibujar la esfera en sus respectivas funciones. Esto es una buena pr�ctica y nos permite dibujar la esfera fuera de WM_TIMER o de WM_PAINT sin necesidad de duplicar el c�digo. Observa que el m�todo que utilizamos para obtener el HDC en cada caso es diferente , por lo tanto es mejor dejar este c�digo en los manejadores de mensajes y pasar el resultado dentro de la funci�n DrawBall( ).

void UpdateBall(RECT* prc)
{
    g_ballInfo.x += g_ballInfo.dx;
    g_ballInfo.y += g_ballInfo.dy;

    if(g_ballInfo.x < 0)
    {
        g_ballInfo.x = 0;
        g_ballInfo.dx = BALL_MOVE_DELTA;
    }
    else if(g_ballInfo.x + g_ballInfo.width > prc->right)
    {
        g_ballInfo.x = prc->right - g_ballInfo.width;
        g_ballInfo.dx = -BALL_MOVE_DELTA;
    }

    if(g_ballInfo.y < 0)
    {
        g_ballInfo.y = 0;
        g_ballInfo.dy = BALL_MOVE_DELTA;
    }
    else if(g_ballInfo.y + g_ballInfo.height > prc->bottom)
    {
        g_ballInfo.y = prc->bottom - g_ballInfo.height;
        g_ballInfo.dy = -BALL_MOVE_DELTA;
    }
}

Todo lo que esto hace es b�sicamente matem�tico, sumamos el valor delta a la posici�n x para mover la esfera. Si el valor pasa afuera del �rea cliente lo ponemos nuevamente en el rango y cambiamos el valor delta a la direcci�n opuesta para que la esfera "rebote" en los bordes.

void DrawBall(HDC hdc, RECT* prc)
{
    HDC hdcBuffer = CreateCompatibleDC(hdc);
    HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom);
    HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer);

    HDC hdcMem = CreateCompatibleDC(hdc);
    HBITMAP hbmOld = SelectObject(hdcMem, g_hbmMask);

    FillRect(hdcBuffer, prc, GetStockObject(WHITE_BRUSH));

    BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCAND);

    SelectObject(hdcMem, g_hbmBall);
    BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCPAINT);

    BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);

    SelectObject(hdcMem, hbmOld);
    DeleteDC(hdcMem);

    SelectObject(hdcBuffer, hbmOldBuffer);
    DeleteDC(hdcBuffer);
    DeleteObject(hbmBuffer);
}

Este es escencialmente el mismo c�digo de dibujo de los ejemplos anteriores, con la excepci�n de que obtiene la posici�n y las dimensiones de la esfera a partir de la estructura BALLINFO. Sin embargo hay una diferencia importante...

Doble Buffering

Cuando dibujamos directamente al HDC de la ventana, es totalmente posible que la pantalla se actualice antes de que lo hagamos... por ejemplo, despu�s de que dibujemos la m�scara y antes que dibujemos la imagen original encima el usuario puede ver un parpadeo del fondo antes de que nuestro programa tenga una chance de de dibujar sobre �ste la imagen en color. Mientras mas lerda sea la computadora y mientras mas operaciones de dibujo realicemos, aparecer�n mas parpadeos.

Esto es terriblemente dr�stico y podemos resolverlo simplemente haciendo primero todos los dibujos en memoria y luego copiar a la pantalla la obra maestra completa en un solo BitBlt() para que la pantalla sea actualizada directamente de dicha imagen, sin que sean visibles ninguna de las operaciones intermedias.

Para hacer esto creamos en memoria un HBITMAP temporal que es del tama�o exacto del �rea que vamos a dibujar sobre la pantalla. Tambi�n necesitamos un HDC para que podamos usar BitBlt() sobre el bitmap.

    HDC hdcBuffer = CreateCompatibleDC(hdc);
    HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom);
    HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer);

Ahora que tenemos un lugar en memoria donde dibujar, todas las operaciones de dibujo usan hdcBuffer en vez de hdc (la ventana) y los resultados son almacenados sobre el bitmap en memoria hasta que hallamos terminado. Luego podemos copiar dicha imagen a la ventana en un solo "disparo".

    BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);
Por �ltimo podemos eliminar nuestro HDC y HBITMAP de manera usual.

Doble Buffering Veloz

En este ejemplo estoy creando y destruyendo el bitmap usado para el doble buffering con cada frame, hice esto porque es m�s f�cil siempre crear un nuevo b�ffer que registrar cuando cambia la posici�n de la ventana y cambiar el tama�o del b�ffer. Pero podr�a ser m�s eficiente crear un doble b�ffer global y no permitir cambiar el tama�o de la ventana o s�lo cambiar el tama�o del bitmap cuando es cambiado el tama�o de la ventana, en vez de crearlo y destruirlo todo el tiempo. Se deja como tarea al lector implementar esta optimizaci�n si desea mejorar el dibujado sobre la pantalla, para un juego o algo por el estilo.

Eliminando el Timer

Es una buena idea liberar todos los recursos cuando la ventana es destruida y en este caso tambi�n se incluye el timer que hemos usado. Para detenerlo, simplemente llamamos a KillTimer( ) y le pasamos el ID que hemos usado para crearlo.

    KillTimer(hwnd, ID_TIMER);

Copyright © 1998-2003, Brook Miles (theForger). All rights reserved.

Versi�n en espa�ol: Federico Pizarro - 2003