Tutorial canvas 2D - Cómo hacer un juego en javascript 1ª parte

Created at: 2023-02-02

Bienvenidos al remake de mi primer tutorial sobre cómo realizar un pequeño juego en JavaScript, más concretamente el tan viejo como popular Space Invaders. Para escribirlo, me fue de gran utilidad el tutorial de Kevin Glass sobre cómo programar videojuegos en Java, quien también eligió este juego como punto de partida. En su día, a mediados de 2011, aprendí una emular un estilo de programación orientada a objetos mediante prototipado para hacer la conversión de Java a Javascript aunque intentando resolver los conceptos de clases y herencia de forma similar. Aunque en el nombre de estos lenguajes ambos incluyen la palabra java, JS se parece a aquel tanto como un huevo a una castaña. Sin embargo, para este remake de mi viejo tutorial del blog Experimentando en la web, he reordenado las ideas y reescrito el juego de forma más sencilla con lo que he aprendido en estos años, y he usado las características nuevas que ofrece javascript para las versiones de ecmascript 5 y ecmascript 6, que incorporan de serie el concepto de clase.

Atari Space Invaders

Por aquí dejo un enlace al juego del tutorial original programado con prototipos y con el código escrito en cristiano, por si alguien siente la misma nostalgia que yo. Si puedes, te recomiendo que eches un vistazo a trabajos tuyos del pasado remoto, con mentalidad crítica. Lo normal es que te pase como a mí, que leyendo tu trabajo veas cagaditas de novato y se te ocurran multitud de cambios de mejora por todas partes.

Para quien tenga la desgracia de no conocer el videojuego original, de la edad de oro de las recreativas arcades, el juego trataba de una invasión alienígena que el protagonista debe evitar. Para ello, cuenta con una nave que solo puede moverse de izquierda a derecha y disparar hacia arriba. Las naves alienígenas se van moviendo todas juntas y en zigzag desde la parte superior a la parte inferior de la pantalla, donde se encuentra el protagonista. El jugador debe destruir todas las naves antes de que lo alcancen, o perderá la partida.

Estos tutoriales están pensados para personas con unos razonables conocimientos previos de programación, pero sin experiencia en el desarrollo de videojuegos en 2D. El objetivo que busco es conocer las rutinas básicas que se siguen para desarrollarlos, con una buena comprensión de lo que está ocurriendo en el código en cada momento. Insisto en que yo ni fuí, ni soy, ni seré, un experto en este tema. Todo lo que he hecho aquí es fruto de lo que he ido aprendiendo con mi experiencia personal y de leer código fuente y tutoriales de terceras personas con las mismas vocaciones que yo. Sin duda alguna existen mejores ideas que las que seguido para solucionar cada problema que inevitablemente surge mientras desarrollas juegos. Si usas directamente mi código, que sea bajo tu propio riesgo; aunque tampoco creo que esté mal del todo.

Si mientras sigues los artículos crees que puedes aportar nuevas ideas, o detectas algún bug, no dudes en dejar un comentario contándomelo. Siempre es interesante conocer nuevos puntos de vista, y sin duda me ayudarán a mejorar a mí y a cualquier posible usuario que nos esté leyendo.

Entorno de desarrollo

Bien, empezamos con la chicha. ¿Qué herramientas se necesitan para la programación de un minijuego en JS? Pues muy poca cosa: Un simple editor de código, y un navegador. Nada más. Editas tu código HTML y Javascript con el editor, y abres el fichero html principal en un navegador. De esta forma tan simple, podrás correr todos los tutoriales de esta serie.

Como editor de código suelo usar Visual Code Studio, que es gratuíto y multi plataforma. Antes usaba Atom Editor y Sublime Text, pero en mi opinión microsoft les ha pasado la mano por la cara a todos. Atom fue el editor que mas usé en el pasado, así que en homenaje, he intentado emular el aspecto visual de este blog al de aquel IDE.

Como navegador web, vale cualquiera. Hoy en día los grandes navegadores ya son prácticamente idénticos en cuanto a funcionalidad y adaptación de los estándares web. Yo me he decantado por Google Chrome, ya que sus herramientas para programadores me enamoraron hace ya una década y ya no sé vivir sin ellas. Pero lo dicho, Mozilla Firefox y Microsoft Edge tienen también desde hace mucho tiempo herramientas similares.

Conceptos básicos

Antes de comenzar a picar código como posesos, voy a intentar explicar cómo creo que funciona un videojuego a nivel general. Hay juegos muy sencillos de programar, como el Tetris, el Snake, o el Space Invaders. Y también puede ser extremadamente difícil, como cualquier juego de estrategia con muchas opciones, juego de rol, etcétera.

Pero en general, todos comparten como mínimo estos pilares básicos:

  • Poseen algún tipo de modo gráfico.
  • Leen los eventos de teclado y/o de ratón para comunicar al juego las órdenes del jugador.
  • Implementan algún tipo de mecánica, es decir, la lógica necesaria para determinar el nuevo estado del juego después de ejecutar las órdenes del jugador.
  • Tienen un bucle principal que gestiona todos estos aspectos.

El modo gráfico que vamos a utilizar en este primer tutorial, como ya te estás imaginando por el título del artículo, va a ser de puro y ochentero 2D. Los primeros videojuegos eran solo texto, donde las pantallas consistían en descripciones precisas sobre lo que el jugador estaba viendo, tras lo cual el jugador debía de escribir lo que quería hacer sobre un conjunto de órdenes muy limitado, como "ir norte", "coger llave", o "atacar goblin". Luego llegaron las 2D, cuyos gráficos eran íntegramente gestionados por la CPU del ordenador; y finalmente llegó la revolución 3D, con las primeras tarjetas gráficas del mercado, que trasladaban a la GPU la infinidad de cálculos de operaciones en coma flotante que requiere dibujar un mundo en 3 dimensiones.

Hace casi dos décadas, desarrollar juegos en HTML con JavaScript era una tortura de chinos, que encima no todos los navegadores podían interpretar correctamente. La idea era crear y usar elementos HTML como los DIV e ir posicionándolos de forma relativa o absoluta con código JS para conseguir el efecto deseado. Años después llegó Flash, un plugin de los navegadores que permitía ejecutar aplicaciones basadas en ActionScript con muchísimas instrucciones de dibujo gráfico vectorial, y con capacidades sin parangón para lidiar con la multimedia, el streaming de video y audio, y el networking. Youtube pude nacer en su día básicamente por la existencia de esta tecnología. Finalmente, el consorcio de las tres uves dobles reaccionó y creó HTML5 para poder competir con flash, al cual acabó liquidando en poco tiempo. HTML5 proporcionaba un nuevo elemento, el CANVAS (lienzo); y una nueva remesa de instrucciones de dibujo gráfico para javascript, tanto en 2D, que usaremos aquí, como en 3D, bajo WebGL. En 2D será la CPU la encargada de ejecutar absolutamente todo el código, por lo que juegos donde se tengan que dibujar tropecientos mil items en pantalla y al mismo tiempo, gestionar las mecánicas de todos ellos, puede suponer un cuello de botella que lastre un poco el rendimiento del juego. WebGL utiliza la tarjeta gráfica para renderizar todo lo que se mueve en la pantalla, liberando de ese trabajo al procesador, así que es increíblemente más rápido a la par que complejo.

WebGL será el objetivo de futuros tutoriales, pero de momento, lo haremos todo con la CPU en busca de la sencillez. Nuestro Space Invaders no requiere nada de potencia para poder correr correctamente (básicamente hasta una tostadora podría hacerlo con soltura).

Leer los eventos de teclado y/o de ratón no necesita mucha explicación: es la forma más sencilla que tenemos para que el jugador pueda interactuar con el juego. También hay más posibles puntos de entrada. Si tienes tiempo libre puedes dedicarlo a leer más acerca de reconocer gamepads, o los más modernos sensores de movimiento típicos de la realidad virtual. En el mundo de los smartphones, también podemos equiparar los gestos que hacemos con los dedos con los eventos del ratón, ya que en esencia son equiparables: Pulsar, arrastrar, soltar. También disponen de apis especiales, como el giroscopio y el acelerómetro, para controlar el movimiento en el espacio y la inclinación que tiene el móvil en un momento dado. Tal vez experimente con algunas de estas posibilidades en un futuro tutorial.

Para este primer juego vamos a ser menos ambiciosos: Nos bastará un teclado y cuatro de sus teclas: Los cursores derecho e izquierdo para el movimiento de la nave protagonista, el espacio para disparar, y la tecla ENTER para reiniciar una partida.

Las mecánicas del juego básicamente son básicamente las reglas que describen cómo funciona y su comportamiento, y a la postre, si lo hacen atractivo para jugarlo o no. El Space Invaders es un arcade de acción de disparos sencillito, asi que nuestras mecánicas consisten en mover los personajes por la pantalla, poder disparar, y detectar cuando un disparo impacta en un enemigo.

La complejidad del algoritmo para mover personajes y detectar colisiones está directamente relacionado con el modo gráfico que estemos usando. La forma gráfica más sencilla para representar los personajes es dibujarlos como polígonos rectangulares. Cada nave alienígena es un rectángulo, la nave del jugador es un rectángulo, y los disparos son rectángulos pequeñitos. Cada uno de estos rectángulos no puede rotar, asi que sus lados siempre estarán en paralelo al eje de coordenadas. De esta manera solo necesitaremos un sencillo algoritmo de detección de colisiones de rectángulos, que es el más básico de todos, para saber cuándo una bala ha pegado a un enemigo, o cuándo una nave enemiga ha chocado contra el jugador. También puedes dibujar polígonos usando triángulos, líneas y círculos, o una combinación de ellos. En estos casos, y como te podrás imaginar, implementar algoritmos de colisión entre tanta variedad de figuras será sensíblemente más complicado, pero matemáticamente posible. No olvides que el coste de tanto cálculo podría empeorar el rendimiento general del juego si tuviera que gestionar cientos o miles de instancias.

Colisión simple

Por eso es importante elegir siempre el modo más sencillo posible capaz de lidiar con la idea que quieres plasmar, y utilizar gráficos acordes. Los juegos profesionales en 2D, donde los personajes suelen ser imágenes, se suele utilizar multitud de técnicas: Por ejemplo, una muy popular es que en el juego se dibuja una imagen, pero para calcular las colisiones, se utiliza un rectángulo que la envuelve. Sin embargo, el algoritmo detectará como colisión lo que se muestra la primera imagen de la izquierda, cuando realmente la zona que corresponde al cuerpo de los personajes, no están en contacto. El jugador que pierda la partida por ello se acordará de tu madre. Para solucionar ese problema, se suele utilizar esta idea: para dibujar, se utiliza la imagen como antes, pero para calcular las colisiones, se mantiene una lista de polígonos cuya posición se aproxima a la forma real del personaje que hay en la imagen, como podemos ver en la imagen de la derecha. Si alguno de los rectángulos colisiona, se comunica que el personaje ha colisionado. O incluso que "una parte X del cuerpo" ha colisionado, si quieres ser más fino y ampliar las posibilidades de tu juego. Por supuesto, esos polígonos para detectar colisiones no se dibujan en la pantalla.

Colisión compleja

Hay otra técnica aún más compleja basada en comprobar pixel a pixel si figuras contenidas en dos imágenes colisionan, pero es una forma muy costosa y que podría bajar el rendimiento del juego hasta hacerlo injugable.

El bucle principal del juego es el corazón de la criatura, y se encargará de manejar absolutamente todos los aspectos del juego. Es una explicación obvia, pero nunca está de más recordar cómo funciona la ilusión del movimiento: consiste en mostrar al menos 20 imágenes por segundo sucesivamente, con pequeños cambios de posición de los objetos cada vez. A las imágenes por segundo se les conoce en el mudo de los videojuegos como FPS, frames per second, y cuantas más se consigan dibujar, mejor sensación de fluidez percibirá el jugador.

Asi que el bucle principal del juego será el trozo de código que se ejecuta en cada una de esas interacciones, con la idea de detectar las órdenes del jugador, ejecutar las mecánicas del juego sobre el estado que se tenga en ese momento, mover los elementos dibujables a la posición esperada tras aplicar las mecánicas y órdenes del jugador, y por fin, dibujarlo todo en pantalla. En nuestro, se traduce a que en cada vuelta del bucle se calcularán las físicas, es decir, los movimientos de todas las naves alienígenas, el jugador, y sus disparos, y se calcularán posibles colisiones entre los disparos, las naves alienígenas, y nuestra nave protagonista. También se leerá el teclado para saber si el jugador ha pulsado alguna tecla. Debo aclarar que los pasos anteriores no hay por qué hacerlos en el orden en el que los he mencionado, aunque debes de ser consciente del resultado. Y por supuesto, se pueden realizar más tareas de las que he nombrado. Todo depende de la naturaleza del juego que quieras realizar.

Diseño del juego

Una vez conocemos a brocha gorda lo que se necesita para hacer funcionar nuestro minijuego, ya va siendo hora de entrar en materia: El código. El primer paso es elegir un paradigma de programación con el que nos sintamos más cómodos representando ideas y soluciones. Yo estoy muy familiarizado con la programación orientada a objetos (POO), pero no habría ningún problema en implementarlo con la programación estructurada, o si eres lo suficientemente valiente (o zumbado), con programación funcional.

Si nunca has programado bajo el paradigma de mi elección, o no conoces bien cuáles son sun características principales, es un buen momento para que interrumpas la lectura de este tutorial y leas al menos algo acerca de sus fundamentos en algún lenguaje más puro, como Java. Si sabes de qué va el paradigma POO, pero no sabes cómo se implementa en javascript, que es una característica relatívamente reciente, puedes echarle un vistazo a esto. También puedes arriesgarte e intentar seguir este tutorial para aprender ambas cosas a la vez, quizás no sea tan complicado después de todo.

Como se puede ver en el link anterior, las clases de Javascript en realidad no son clases. Por debajo serán convertidas a objetos de javascript, cuyo tipo de orientación a objetos va por prototipado. Algunas de las "limitaciones" a la hora de adoptar POO son que JS no tiene interfaces, por ser un lenguaje débilmente tipado, ni existe al menos de forma explícita para el concepto de visibilidad protegida. Hay quien aún diseña prototipos siguiendo el patrón Module para conseguir resultados parecidos.

El motivo por el que me he decantado por la orientación a objetos es el siguiente: Una vez que coges soltura, me parece la forma más sencilla de representar la "realidad" por medio de código, y de poder modificarlo, mantenerlo y reutilizarlo posteriormente. Al menos en teoría, luego ya...

Cada componente del juego será una clase. Cada clase tendrá sus atributos propios, y una lista de funciones, que en POO se llaman métodos, para actuar sobre esa clase. Programar un juego (o cualquier cosa) es básicamente relacionar distintas clases entre sí a través de sus métodos, para conseguir el resultado esperado.

Lo primero que tenemos que hacer, es identificar los componentes claves que tendrá el juego, su lógica y sus mecánicas. ¿Qué podemos entender como un componente? Pues más o menos sería cada "pieza" que tenga una lógica propia y que sea lo suficientemente compleja como para merecer tener una clase propia. Por ejemplo, el protagonista, los enemigos, las armas, los disparos, el mapa, los items... Cada juego es un mundo, nunca mejor dicho.

En el space invaders, se me ocurren los siguientes componentes:

  • Player. Esta entidad podrá moverse horizontalmente en la pantalla y, disparar, a petición del jugador, y ser impactada por alguna de las naves enemigas.
  • Alien. Puede moverse tanto horizontal como verticalmente, aunque su movimiento está automatizado. En nuestra versión no podrá disparar, pero sí impactar con la nave del jugador.
  • Shot. Esta clase será el disparo del jugador, que podrá chocar con alguna nave enemiga, y que solo se puede mover verticalmente.

El "mapa" del juego es muy básico, así que no necesita clase propia. La nave protagonista solo tiene una forma de disparar, así que no merece la pena meter un concepto de "arma". En este juego no hay items ni premios para el jugador, complejidad que nos ahorramos. Nuestras entidades, como ya dijimos antes, estarán representadas gráficamente por simples rectángulos blancos en el caso de los aliens, verde para el jugador, y amarillo para los disparos. En un tutorial posterior me dedicaré a explicar cómo utilizar imágenes en su lugar, introduciéndonos en el maravilloso mundo de las animaciones.

Ahora que tengo los componentes principales del juego, voy a pensar alguna forma de gestionar esas clases, para hacer funcionar el juego, y la interacción con el jugador. Como se puede ver, me interesa trazar una línea entre las clases que forman mi juego, que son concretas y difícilmente reusables para otros juegos, de las que simplemente son una especie de "framework" que puedo utilizar en parte en otros proyectos.

  • Keyboard. Se encargará de la gestión de los eventos de teclado.
  • Render. Se encargará de dibujar el juego en pantalla.
  • Game. Clase principal del juego. Contendrá el bucle infinito y hará de director de orquesta de las clases principales que forman el juego.

Como podéis ver, se busca diferenciar y separar el código que gestiona la lógica del juego, con el código que leerá eventos del teclado, o el de la parte que dibujará el juego en pantalla. Esto me facilitará que otro día pueda implementar otro tipo de control, por ejemplo un ratón; o de dibujar en perspectiva isométrica o incluso 3D este juego. Todo ello sin que requiera modificar profundamente el juego, y por tanto con una escasa inversión de tiempo y con menos probabilidad de romperlo.

Tampoco pasa nada programarlo todo en una clase Game gigante. A este nivel de refinamiento se llega por experiencia. Y probablemente cuando me ponga a picar código, quizá vea interesante meter más clases. Lo que es absolutamente recomendable es tener claro qué es lo mínimo que tienes que hacer para considerar el juego como acabado, conocido como producto viable mínimo. Cuando empieces a implementarlo, seguramente te encontrarás problemas inesperados, o nuevas ideas que antes no tenías en mente. Mi recomendación es que salvo que sean vitales, apúntalos en una lista tipo TO DO, vuelve el plan original y cíñete a él. Cuando el juego funcione, valora si merece la pena implementar o desechar cada una de estas anotaciones.

Cómo depurar el juego

Si bien lo que voy a explicar a continuación no es estrictamente necesario, sí puede resultarte verdaderamente útil para cuando programes y depures tu primer videojuego. El control de errores en javascript es particularmente odioso. Javascript es un lenguaje débilmente tipado; no hace apenas comprobaciones de ningún tipo, y cuando maneja las variables, suele castearlas (convertirlas de un tipo a otro) automáticamente sin avisar, con lo que detectar errores a veces puede ser una odisea.

Soy consciente de que en la actualidad, los desarrollos de front han evolucionado muchísimo, y que hay multitud de capas para forzar tipos en la etapa de desarrollo, como type script o flow, aprovechando los procesos de compilación-transpilación de otras herramientas como babel. En esta serie de tutoriales voy a tirar por el camino old-school de programar directamente código javascript nativo, sin ninguna dependencia, mandándolos tal cual al navegador. La idea que busco es lidiar personalmente y a pelo con todos los posibles problemas que se tienen durante el desarrollo del juego, con la satisfacción de ver que mi solución funciona.

Por tanto, voy a echar mano intensamente del objeto console y de las "herramientas para desarrolladores" que tienen los navegadores modernos. En chrome se abre pulsando F12. En él tenemos un depurador del código capaz de insertar breakpoints en las líneas que queramos, para poder ir ejecutando el código línea a línea a partir de ellos, y poder ver mediante un simple doble click sobre la ventana del código el valor que contienen las variables en un momento dado. Una vez que lo pruebes y te acostumbres a usarlo, no querrás otra cosa. Por si fuera poco, Google tiene el mejor motor para interpretar javascript. En chrome, tus códigos normalmente se ejecutarán algo más rápidos que en otros navegadores. Lee cómo funciona, aprende a exprimirlo, y ámalo. Puedes empezar con este enlace.

Pero como dije antes, no pasa nada si usas Firefox, o Microsoft Edge. Ambos tienen entornos de desarrollo muy similares, con los que puedes hacer lo mismo.

Y con este tochazo, damos por finalizada la introducción de cómo programar un videojuego en javascript. En el próximo capítulo, empezaremos a picar algo de código del juego. ¡Hasta la próxima!