El diccionario de la Real Academia Española define bucle como «Rizo de cabello de forma helicoidal». Y se me viene a la mente la imagen de David Bisbal, mientras niego con la cabeza: ¡Qué cuesta arriba se me hace siempre escribir este primer parrafito resumiendo lo que voy a contar a continuación! Pues, sí, voy a hablar bucles. Y no los de la cabeza de ningún cantante aborrecible. Bucles de programación. Para programar un juego son imprescindibles, debido a que el propio juego ha de estar encapsulado en un gran bucle que abarca todo. Un ciclo sin fin de cálculos matemáticos que logra transmitir mucho más que cifras y letras. Suena incluso bonito.
Loop
Los bucles son uno de los conceptos que quizá más cuesta entender cuando uno está empezando a programar. Es una herramienta tan básica que la mayoría de las veces se enseña cómo se construye un bucle antes incluso de explicar qué necesidad hay, o qué son. Pero se usan continuamente, casi sin excepción.
Un bucle (loop en inglés) no es más que la repetición de un bloque de código un número determinado de veces o hasta que se cumpla una condición. Si hay que realizar la misma operación, o una muy parecida, varias veces seguidas, el código se puede simplificar con un bucle.
Suelen ser muy típicos, cuando se aprende a programar, los ejercicios que tratan sobre imprimir en pantalla una serie de numeritos, que se resuelven con un simple bucle que llama a una función (una función o subrutina es un bloque de código que realiza una tarea concreta), pero el caso más común para utilizar bucles es recorrer una serie de datos almacenados en memoria y realizar alguna operación sobre ellos. Por ejemplo, actualizar los objetos que hay en un juego: bastará un bucle muy sencillo para recorrer todos esos objetos y realizar la operación de actualización sobre ellos. Los bucles tienen sus pegas (que no voy detallar), pero son inevitables muchas veces. Y una vez casi todo está organizado en listas (o arrays) es muy sencillo usar bucles para recorrerlas; la programación de alto nivel está preparada para hacer eso. En la mayoría de lenguajes de programación se suelen usar palabras como for, do o while para implementarlos.
Game Loop
Pero el caso que nos interesa aquí es el llamado Bucle de Juego o Game Loop. Un pequeño bucle que engloba todo juego.
Como ya sabrá el lector, un procesador ejecuta las instrucciones de su programa una a una, en el estricto orden en que están escritas, pero cuando esas instrucciones se acaban, el programa termina. Y eso deja muy poco sitio para la interactividad. La mayoría de programas han de ejecutarse continuamente hasta que el usuario decide terminarlos. Para conseguir eso se necesita un bucle: básicamente el programa ejecuta sus instrucciones, y cuando llega al final todo se repite. Se vuelve al principio una y otra vez mientras no se reciba la orden de acabar.
Algunos programas esperan a recibir las acciones del usuario, y, mientras eso ocurre, el programa está parado. No es el caso de los videojuegos. Incluso aunque el usuario no haga nada y el juego esté en pausa, todo ha de seguir en marcha: se ejecutan animaciones, se reproduce música, etc.
El bucle de juego más sencillo consiste simplemente en repetir estas tres operaciones:
- Process Input – Recibe y procesa el input del jugador (teclado, mandos, etc.)
- Update – Actualiza los objetos del juego, realiza los cálculos de IA y física (si es que los hay)
- Render – Pinta la escena en la pantalla
Idealmente, esas tres funciones son completamente independientes, y su complejidad dependerá del juego en cuestión. La función Render sólo actuaría con los datos que hay una vez la función Update ha terminado, de forma que se podría intercambiar con cualquier otro código que rellenase la pantalla de manera totalmente distinta, por ejemplo, en 2D en lugar de 3D. La realidad es que eso no suele ser tan sencillo: a menudo el juego se estructura usando una gran lista de objetos que definen su propia actualización y pintado, y cambiar esa función de Render significaría modificar todos los distintos tipos de objetos que haya en el juego.
Fixed Time Step vs. Variable Time Step
Pero ahí no acaba todo. La función de Update necesita saber cuánto tiempo ha pasado desde la última vez que se llamó para actualizar los objetos adecuadamente. Esto lanza un problema, debido a que tanto la actualización como el render pueden tomar una distinta cantidad de tiempo cada vez que se llaman, dependiendo del estado el juego y de la máquina que lo está ejecutando.
Así que hay que calcularlo al vuelo. No es difícil usando el reloj del sistema, al que normalmente se tiene acceso. Basta con almacenar temporalmente el tiempo del sistema cuando se ejecuta la actualización para poder compararlo la siguiente vez y así obtener cuánto ha transcurrido. Usando el tiempo que ha pasado desde la última actualización o, como se le suele llamar habitualmente, delta time, se pueden realizar todos los cálculos de velocidades y físicas adecuadamente.
Sin embargo, todavía hay un problema. La precisión del sistema es limitada. Cada vez que se produce una actualización también se produce un pequeño error de cálculo, debido a que los valores almacenados no poseen un infinito número de dígitos. Por ello, actualizaciones demasiado rápidas pueden hacer que los errores derivados de la precisión de los cálculos se acumulen. Por el contrario, si son demasiado lentas afectarían la experiencia de juego. Y si el tiempo es variable, se pueden dar ambas situaciones. Además, ya que ese tiempo será arbitrario, el sistema será impredecible (no determinista) lo que dificulta la programación de física y juego en red.
Para evitarlo se suele usar un tiempo fijo de actualización o Fixed Time Step. En lugar de actualizar cada vez utilizando el tiempo transcurrido, sea cual sea, se hace usando una determinada cantidad de tiempo, y se llama a la actualización las veces que sea necesario hasta que la simulación alcance el tiempo de juego real.
En resumen
El bucle de juego es la estructura más básica que un juego necesita, y por la que se suele empezar cuando se está programando un juego desde cero. Usando un motor gráfico, en la mayoría de casos, el bucle de juego estará oculto e inaccesible, ya que es el motor el que se encarga de gestionarlo, pero eso no significa que no esté ahí.
Es interesante conocer la diferencia entre un tiempo de actualización variable y uno fijo, y los problemas derivados. En muchos casos se usa uno u otro dependiendo de lo que se necesite actualizar; es bastante habitual que el bucle de juego habilite los dos modos de funcionamiento: tomando Unity3D como ejemplo, los componentes reciben la función Update (tiempo variable) y FixedUpdate (tiempo fijo) quedando a elección del desarrollador qué usar en cada momento.