Construyendo un Game Loop
Hace tiempo que tengo ganas de armar este post, hay muchas formas de hacerlo, seguramente hay mejores. Está es una forma a la que llegué yo probando varias cosas y la queria compartir.
Antes que nada, el propósito de este post es ir creando un Game Loop paso por paso intentando ver detalladamente cada aspecto para llegar al código final, lo que puede ser el alma de un juego en HTML5.
Temas
- Que es el Game Loop?
- Encapsulando el juego
- Agregando Canvas de HTML5
- Actualizando y Dibujando
- Optimizando
- Canvas Buffer
- Conclusión
Que es el Game Loop?
Dentro de la jerga gamer se le llama asi al ciclo en el que se basa todo el juego, un ciclo “cuasi” infinito por el cual el juego actualiza sus estados y se dibuja una y otra vez mientras este vive.
Lo anterior es una versión extremadamente reducida del game loop, básicamente tengo una funcion loop a la que se llama a sí misma cada 20 mili-segundos y es la encargada de primero actualizar los estados del juego y después dibujar.
Para una explicación detallada del setTimeout te dejo este post
Encapsulando el juego
Para empezar a darle forma vamos a usar el Patrón Módulo y a mejorarlo.
La idea de usar este patrón es encapsular la funcionalidad del juego en un módulo, de esta manera cerrar el alcance y dejar lo que debería ser privado, como privado (en este caso el loop, actualizar y dibujar)
Como se vé retornamos un objeto con el acceso a nuestro módulo, para poder iniciar o detener el juego.
Agregando Canvas de HTML5
Ahora, para hacerlo un poco mas real, vamos a meter un canvas, ya que es una excelente opción hoy en día al momento de desarrollar un juego con HTML5.
Lo que hicimos fue agregar 2 variables dentro del alcance del módulo, uno para el canvas y otro para el contexto para poder referenciarlo desde la función dibujar. Creamos una función para iniciar y asignar las variables, y agregamos la llamada a esa función al momento de iniciar el juego.
Te recomiendo unos posts si no estas familiarizado con Canvas: Dibujando y Animando
Actualizando y Dibujando
Para completarlo vamos a hacer que realmente funcione con algo, como ejemplo hacer que dibuje y mueva un cuadrado cuando el usuario presiona las flechas.
Primero agregamos las 2 variables que vamos a usar para conocer la tecla presionada y el estado del cuadrado actual:
Implementamos el manejo de los eventos keydown y keyup para saber la tecla presionada:
Luego implementamos el actualizar() con el cambio de estado del cuadrado (cuadrado.x) dependendiendo de la tecla presionada:
Entonces cada vez que se ejecute la función comprueba la flecha presionada y actualiza la x del cuadrado.
Ahora nos queda dibujar el estado del cuadrado en cada momento que se ejecute dibujar():
En la primer linea limpiamos todo el canvas, y despues dibujamos el cuadrado en la posición actual.
Nos queda algo asi:
Optimizando
Hay muchas optimizaciones para tener en cuenta al hacer un juego en javascript, quiero centrarme en 2 que considero importantes y principales, más adelante veremos otras.
Canvas Buffer
Como suena, tener un canvas que funcione como un buffer para el redibujo continuo, básicamente la idea es dibujar sobre otro canvas oculto y cuando esté todo listo dibujarlo completo sobre el real, porque esto?, si bien la mejora no es increible, logramos evitar el famoso “flickering” y es esa sensación de que medio se traba la animación.
Para explicarlo mejor: suponiendo que tenemos 30 elementos que se dibujan de a uno sobre el canvas, y a medida que se van dibujando y armando la escena de una secuencia de animación puede tardar un mínimo de tiempo en el cual el ojo llega a percibirlo y nos queda un efecto no muy feliz. Bueno, para evitar ese “efecto”, dibujamos todos en un canvas “falso” y después aplicamos el dibujo completo de la escena en el canvas “real”.
Genial, vamos a agregar esta optimización al game loop volviendo a nuestro código, empezando por la función donde iniciamos el canvas:
Lo que hicimos ahi es crearnos el “falso” canvas y su contexto partiendo como base del tamaño del canvas real, ahora lo que necesitamos es, al momento de dibujar, hacerlo sobre el falso y después aplicarlo en el real, entonces nuestra funcion dibujar() quedaría así:
Request Animation Frame
Nuestro setTimeout() para realizar el loop es genial, pero no sería mejor avisarle al explorador que vamos a correr una animación y queremos que se prepare y lo haga lo mejor que pueda?, bueno eso es el requestAnimationFrame(). Fue creado con ese propósito, que el explorador sepa cuando vamos a hacer este tipo de cosas, como por ejemplo: un loop con muchos “frames” para realizar una animación.
El cambio es bastante simple para nuestro game loop, ya que es “casi” reemplazar el setTimeout() por el pedido de animaciones por frames así que cambiemos el loop:
Como se vé el cambio fue bastante simple, pero no se si notaste que ya no tenemos velocidad, bueno, es mejor no tenerla jeje, personalmente prefiero que el requestAnimationFrame la maneje por mi, por el hecho de que es bastante complicado calcular un buen “frame rate” no siendo un experto en el tema y sobretodo pensando que ese frame rate es el tiempo que tenemos para dibujar tooodo el estado de la escena (suena complicado para mi :P)
Y si el explorador no lo soporta?, bueno para ese caso les recomiendo utilizar el requestAnimationFrame polyfill de Erik Möller, en el cual se armó un script buenisimo con todo ese problema resuelto, dejandonos el window.requestAnimationFrame listo para usar esté o no soportado (esto lo hace llevandolo a un setTimeout como última medida)
Conclusión
Que sigue ahora?, bueno primero que nada este código que construimos es a modo de ejemplo, las funciones de actualizar() y dibujar() deberían encargarse de llamar a otros módulos o clases y delegar totalmente las funcionalidades, con esto quiero decir que no debería crecer mas el módulo de lo que está y mantenerse sólo en la lógica del game loop, no queremos que se vuelva un mounstro gigante
Les dejo el Game Loop terminado con el ejemplo:
Y un Gist con el Game Loop completo como template para que hagan cosas locas: