5 Controlar el tiempo

Es el momento de volver al bucle del juego. Recordemos:

  1. Inicialización
  2. Bucle de juego.
    1. Capturar los eventos.
    2. Procesar la información.
    3. Mostrar los resultados.
  3. Limpieza.

En los ejemplos que hemos visto hasta ahora la velocidad de las animaciones se ajusta usando la función SDL_Delay. Sin embargo, esta técnica no es muy recomendable por varios motivos.

Para empezar, SDL_Delay espera el tiempo especificado en milisegundos, pero lo hace de modo que ocupa todo el tiempo de la CPU, o al menos del núcleo del procesador que está ejecutando el hilo actual. Esto impide que la CPU pueda realizar otras tareas en ese tiempo, como por ejemplo, procesar el siguiente cuadro del juego, o atender a otras tareas importantes del sistema operativo. Es decir, los tiempos que se esperan usando esta función son tiempos perdidos para procesamiento.

Por otra parte, nuestro juego puede ejecutarse en multitud de equipos con diferentes configuraciones y potencias de procesamiento y en diferentes plataformas, de modo que tenemos que asegurarnos de que la experiencia sea lo más parecida posible en todos los casos.

Si diseñamos el juego de modo que funcione a un número constante de fps, en un sistema con aceleración gráfica y un procesador potente funcionará correctamente, pero es posible que en un sistema con menos potencia no sea posible generar tantos fps, y el juego vaya más despacio.

Por ejemplo, supongamos que nuestro juego en el equipo potente requiere 5ms para generar un cuadro, lo que le da la posibilidad de mostrar 200fps, es decir, puede funcionar perfectamente a 60fps.

Sin embargo, en otro equipo se pueden necesitar 33ms para generar un cuadro, por lo que solo tendremos la posibilidad de mostrar 30fps como máximo. Si nuestro juego hace los cálculos del movimiento de los objetos del juego para mostrar 60fps, el juego funcionará a la mitad de la velocidad deseada.

Por lo tanto no podemos asumir que el hardware en el que se ejecutará el juego puede hacerlo en los tiempos requeridos.

Podemos diseñar el juego para que se muestren tantos fps como sea posible. En ese caso deberemos calcular las nuevas posiciones de cada elemento que deba mostrarse en función del tiempo que haya transcurrido desde el anterior cuadro.

En realidad, en un juego real, cada iteración del bucle de juego no se ejecutará siempre en la misma cantidad de tiempo, lo que complica aún más las cosas.

Pongamos por caso que queremos que nuestro juego funcione a 60 fps. Eso nos da un tiempo máximo para cada iteración del bucle de 16.7ms. En un dispositivo A, el tiempo máximo de una iteración puede ser 6ms, por lo que de media tendremos que esperar 10 ms cada vez. Podemos ajustar cada iteración consultando el tiempo que requiere y esperar antes de empezar la siguiente iteración, usando funciones como SDL_GetTicks o mejor SDL_GetTicks64 al iniciar y finalizar cada iteración, y restando los valores obtenidos.

Veamos un ejemplo. Este bucle se ejecuta aproximadamente 30 veces por segundo:

    do {
        tick = SDL_GetTicks64();
        SDL_SetRenderDrawColor(renderer, 0,0,0,255);
        SDL_RenderClear(renderer);
        SDL_SetRenderDrawColor(renderer, 255,255,255,255);
        SDL_RenderDrawRect(renderer, &rect);
        rect.x += 1;
        SDL_RenderPresent(renderer);
        SDL_Delay(33-(SDL_GetTicks64()-tick));
    } while(rect.x < 640);

Ejemplo 6

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 6 sdl_006.zip 2024-01-07 1116 bytes 6

Pero tiene dos inconvenientes:

El primero que usamos SDL_Delay, que con esta frecuencia y en una máquina relativamente potente, consumirá la mayor parte del tiempo.

El segundo es que en una máquina lenta una iteración puede requerir más de 33ms, y por lo tanto irá más lenta de lo que se espera.

El concepto delta time Δt

Supongamos que la posición del personaje en la ventana no depende de cálculos matemáticos, sino de una cámara o sensor que está dándonos la posición de un objeto real.

El objeto real se estará moviendo independientemente de las veces que nuestro programa actualice la ventana, por lo que si entre iteraciones el objeto se desplaza un metro por segundo, y en nuestra ventana un metro son 10 pixels, en nuestra ventana deberemos reflejar ese desplazamiento. Si podemos actualizar la ventana 100 veces por segundo, cada 10 cuadros avanzaremos un pixel, si la actualizamos 10 veces por segundo, en cada cuadro avanzaremos un pixel y si solo actualizamos una vez por segundo, en cada cuadro avanzaremos 10 pixels.

Incluso si la frecuencia de actualización no es constante, los avances serán reflejo de la posición real del objeto.

Nuestros juegos deben funcionar de forma parecida, salvo que en este caso las posiciones de los objetos son consecuencia de los cálculos.

Así, en cada iteración calcularemos el tiempo transcurrido desde la iteración anterior, y calcularemos las nuevas posiciones de los objetos a mostrar en función del tiempo transcurrido. Esto nos dará un reflejo de los movimientos, independientemente de la velocidad a la que nuestro programa pueda procesar el bucle de juego.

Al tiempo entre fotogramas consecutivos es lo que se denomina Δt. Y nuestros cálculos se realizarán en función de ese parámetro.

Si nuestro programa es lento tendremos pocos fps, y los objetos se moverán a saltos entre fotogramas. Si es rápido tendremos muchos fps y los objetos se moverán suavemente. Pero siempre lo harán a la misma velocidad.

    Uint64 tick, tick0;
    SDL_Rect rect = {0,80,20,20};
    float velocidad = 80.0; // 80 pixels/s
    int frames=0;

    tick0 = SDL_GetTicks64();
    do {
        tick = SDL_GetTicks64()-tick0;
        rect.x = velocidad*(tick)/1000.0;
        SDL_SetRenderDrawColor(renderer, 0,0,0,255);
        SDL_RenderClear(renderer);
        SDL_SetRenderDrawColor(renderer, 255,255,255,255);
        SDL_RenderDrawRect(renderer, &rect);
        SDL_RenderPresent(renderer);
        frames++;
        SDL_Delay(retardo);
    } while(rect.x < 640);

Ejemplo 7

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 7 sdl_007.zip 2024-01-07 1353 bytes 6

Probablemente habrás visto juegos donde el tiempo de cada iteración es mayor del necesario para conseguir los fps requeridos. En esos casos experimentamos una latencia o lag que no permite disfrutar de una experiencia agradable. Cuando un juego baja de los 25 fps resulta difícil de jugar: las cosas suceden a saltos, o directamente perdemos detalles importantes del juego. En esos casos se pueden modificar las opciones del juego para reducir el tiempo necesario para cada iteración del bucle del juego, ya sea disminuyendo la resolución, eliminando partículas o dejando de dibujar detalles como sombras, texturas, etc.