[ 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