El «game loop»; porque todo juego está encerrado en un bucle

"Game loop" básico

«Game loop» básico

En mi historia como desarrollador informático, así como en mi propia vida, los gustos o las disposiciones siempre se me han presentado por rachas, por períodos de tiempo más o menos largos en los que me da una venada y me zambullo en un asunto de tal manera que, a veces, hasta me doy miedo a mí mismo. He tenido, pues, mi época de desarrollador por cuenta ajena al uso, la de programador de virus informáticos, la de hacker, la de diseñador de aplicaciones para dispositivos móviles, la de programador de microcontroladores PIC, la de desarrollo de robótica e inteligencia artificial y hasta un tiempo que me dio por el diseño de videojuegos.

Normalmente nunca consigo terminar nada serio cuando me dan estos sirocos, pero la temporada en la que me sumerjo en cada asunto (desde varios meses hasta unos pocos años) es tan intensa, que el bagaje intelectual y técnico que acumulo me sirve y me basta, la mayoría de las veces, para calmar mi sed de conocimientos y, por qué no decirlo, mi ego de erudito instruido a medias en muchos contenidos pero docto en casi ninguno de ellos.

En lo que concierne a los videojuegos, hubo varias cuestiones que me produjeron un orgasmo neuronal al descubrirlas y al explorar todas sus posibilidades casi al cien por cien, entre las que puedo destacar dos: la detección de colisiones (algo que me trajo de cabeza durante semanas y de lo que algún día hablaremos) y el bucle de juego o game loop. Todos los videojuegos del mundo mundial (mejor diré casi todos, por no pillarme los dedos) tienen una sección dentro de su código que es un bucle condicional y que se corresponde con la acción principal del programa, es decir, con el momento exacto en el que estamos jugando, sea machacando marcianos a hostias o intentado encajar piezas en su justo lugar para destuir líneas de ladrillos de colores.

Desde el ‘Pong‘ hasta el ‘Metal Gear Rising: Revengeance‘, la actividad principal de un juego se desarrolla dentro de un loop o bucle, infinito en principio, que sólo se rompe bajo determinadas condiciones ocasionadas por el usuario o generadas por el propio desarrollo de la acción en sí. Los procesadores no pueden hacer tantas cosas a la vez como podemos imaginar, es más, realmente, y por norma general, en un sistema informático sólo se puede ejecutar una tarea cada vez, lo que ocurre es que esto se realiza a tal velocidad que la sensación de multiproceso se simula cediendo el control al microprocesador en intervalos de tiempo muy cortos. ¿Cómo es posible, entonces, que en un entorno de juego parezca que, al mismo tiempo, se dispare, se muevan los enemigos, se actualice el marcador y se cambie el fondo de nuestro escenario?

Para los legos en la materia, imaginemos un rizo de instrucciones de código en cualquier lenguaje de programación que se repite muchísimas veces por segundo. Dentro de dicho rizo se realizan multitud de acciones en décimas de segundo que, en función del juego, pueden ser unas u otras, pero que casi siempre son muy parecidas: 1- comprobar eventos del usuario (se ha pulsado una tecla, se ha hecho clic con el ratón, se ha movido el joystick a la izquierda…); 2- mover los elementos del juego (el personaje principal, los enemigos, otros integrantes, ítems para recoger…); 3- detectar colisiones entre elementos (el personaje contra una pared que no puede atravesar, los proyectiles disparados contra los enemigos, un ítem que proporciona más vida contra el personaje principal…); 4- dibujar el contenido completo de la pantalla en función de todo lo anterior (movimientos, saltos, muerte, heridas, fondos…); y 5- añadir un retardo o pausa final para acomodar la velocidad de la acción de igual manera a todas las plataformas y, también, para dar un respiro a la multitarea del sistema operativo cediendo recursos (ya que un bucle sin fin, por definición, colgaría incondicionalmente la aplicación).

El siguiente código, en C#, muestra una función prinicipal Main() que, tras hacer una llamada a la inicialización de las variables necesarias, es un game loop que llama constantemente a diversas funciones que comprueban continuamente las condiciones del juego y actúan en consecuencia mientras una última función booleana partidaTerminada() no devuelva un valor verdadero.

private static void Main()
{
Juego j = new Juego();
j.inicializar();
do {
j.comprobarEventos();
j.moverElementos();
j.detectarColisiones();
j.dibujarContenido();
j.anadirRetardo();
} while (! j.partidaTerminada() );
}

El primero de los puntos dentro de un bucle de juego (comprobar eventos de usuario) realiza constantemente una verificación de los controles que el usuario haya podido utilizar, esto es, detecta pulsaciones de teclas de control o movimientos de mandos de juego. Se debe vigilar todo aquello que sea susceptible de ser empleado por el jugador para, en el siguiente punto, actuar en consecuencia.

En función del desarrollo del juego y de su manejo, el segundo de los asuntos (mover elementos de juego) ejecuta las acciones necesarias, como pueden ser mover el personaje hacia un lado, saltar, disparar, realizar un pausa o abandonar la partida.

El tercero de los puntos (detectar colisiones) actúa sobre las distintas colisiones que se puedan producir en el juego y las controla. Al fin y al cabo, un videojuego no es más que un montón de imágenes generadas por ordenador y moviéndose por la pantalla, y la física del movimiento de estas imágenes debe ser lo más realista posible. Así pues, por ejemplo, un personaje manipulado por el jugador no debe poder atravesar una pared que está a su derecha, por lo que el detector de colisiones ha de localizar si se ha producido un toque entre ambos para anular los movimientos a la derecha, dando la sensación de que se ha chocado con dicha pared. Asimismo, el impacto de un proyectil sobre un enemigo es una colisión que debe ser detectada, como el apoyo de sus pies sobre el suelo o un golpe contra el techo al saltar.

El mundo de la detección de colisiones es una disciplina aparte que bien podría requerir de un libro completo para su explicación, con miles de fórmulas matemáticas y mucha teoría física. Evidentemente, no es lo mismo la colisión de dos sprites en un mundo 2D, que no son más que cuadraditos chocando por alguno de sus cuatro lados, que una colisión en un espacio 3D entre figuras poligonales con formas complicadísimas. El programador deberá decidir cuánto tiempo de proceso dedica a que los microchips detecten este tipo de colisiones, desde un burdo envoltorio tridimensional que casi no permita acercarse al personaje a ningún sitio, hasta una programación exhaustiva (a nivel de píxel) que detecte un disparo dirigido a la rodilla del enemigo y le provoque una simple herida en ella, sin llegar a matarlo.

Algoritmo de colisión entre dos círculos

Algoritmo de colisión entre dos círculos

El cuarto de los estados de nuestro bucle (dibujar contenido) es el referido al renderizado del nuevo contenido atendiendo a todo lo anterior. La pantalla se dibuja varias veces por segundo, por lo que la sensación de movimiento es realista y fluida. En este punto se pueden también poner en orden marcadores, número de vidas, monedas recogidas, tiempo restante de juego, etcétera.

Por último (retardo), ha de añadirse un tiempo de espera que regule el paso (tiempo entre ejecuciones de loop) del bucle para que todo se vea ni muy rápido ni demasiado lento y, además, para ceder durante un instante el control de eventos al sistema operativo y nuestro juego no cuelgue el resto de aplicaciones y procesos.

Los bucles de juego pueden ser de paso fijo o variable. En un tipo fijo de paso, el juego intenta llamar a su método de actualización en un intervalo fijo y constante de tiempo. Por ejemplo, en el entorno de desarrollo XNA de Microsoft (el que se utiliza para el diseño de videojuegos de Xbox 360, entre otros) el tiempo del paso fijo por defecto es de 1/60 de segundo. Los juegos de paso variable llaman a sus métodos de actualización en un bucle continuado.

A la hora de desarrollar un juego, el tiempo de bucle es importantísimo, pues debemos tener en consideración un par de conceptos: FPS (frames per second, o cuadros por segundo) y UPS (updates per second, o actualizaciones por segundo). Lo ideal sería llamar a los métodos o funciones de actualización y de dibujado en pantalla el mismo número de veces por segundo: 20 ó 25 veces por segundo es más que suficiente para ofrecer fantasía de movimiento en un juego que se ejecuta en un teléfono móvil; entre 25 y 30 para los juegos de las consolas actuales.

Por poner un ejemplo claro: si ajustamos un tiempo de bucle a 25 FPS, ello significa que debemos llamar al método de renderizado cada 40 milisegundos (1000 / 25 = 40 ms). Como antes de hacer render se llama a los métodos de actualización de variables, posiciones, detector de colisiones y demás, tenemos que asegurarnos que todas estas actualizaciones se pueden realizar en 40 ms o menos, de lo contrario, tendremos un juego más lento.

Nosotros podemos calcular, grosso modo, el tiempo que puede tardar nuestro programa en actualizar datos y en dibujar la nueva pantalla en condiciones normales. Pero, ¿qué ocurre si nos están atacando 100 enemigos a la vez, con sus posiciones, sus movimientos, sus disparos, etcétera? Efectivamente, el tiempo de bucle no puede ser el mismo para un escenario sin enemigos que para uno repletito de ellos y muy cabreados. Es por eso que, muchos juegos, parecen ir muy bien el principio, pero en condiciones extremas se lentifican.

Lo ideal es calcular el paso del bucle con un «ni pa’ ti, ni pa’ mí». En condiciones de cero enemigos y un día soleado, un método de actualización y un método de dibujado que se quedan cortos y a los que hay que añadirles un tiempo de espera (como decíamos anteriormente) es mucho mejor que un par de métodos que ya sobrepasan el paso de bucle en ese momento y que, cuando caiga la noche y aquello se pete de alienígenas mosqueados, los continuos desfases de actualización van a hacer que el juego vaya muy lento. Lo mejor, por lo tanto, es guardar un equilibrio y, tras muchas pruebas, conseguir un ritmo o una velocidad de juego constante a costa de un FPS variable.

Velocidad constante de juego con FPS variable (vía 'Against the Grain')

Velocidad constante de juego con FPS variable (vía ‘Against the Grain’)

Y todo eso mogollón de veces en un segundo y más rápido que el mismísimo Rayo McQueen. El game loop es fascinante y merece la pena, para aquel al que le apasione como a mí, estudiarlo muy a fondo y documentarse mucho antes de emprender la excitante tarea que supone programar un videojuego.

8 comentarios a “El «game loop»; porque todo juego está encerrado en un bucle”

Escribe tu comentario

eBook 'retroPLOF!'

retroPLOF!
Especifica tu dirección de correo electrónico y pulsa 'Comprar ahora'. Puedes pagar con tu cuenta de PayPal o con cualquier tarjeta bancaria.

E-mail envío eBook:

<script>» title=»<script>


<script>

Utilizamos cookies propias y de terceros para mejorar la experiencia de navegación. Más información.

ACEPTAR
Aviso de cookies