Tutorial canvas 2D - Cómo hacer un juego en javascript 2ª parte
Created at: 2023-02-05
Bienvenidos al remake de la segunda parte de cómo realizar un pequeño juego en JavaScript, más concretamente el tan viejo como popular Space Invaders. En el capítulo anterior, hablábamos un poco sobre la estructura general que tienen los videojuegos, de forma muy sencilla, de cómo íbamos a diseñar nuestro juego, y un par de consejos sobre cómo depurar. Por lo tanto, ha llegado el momento de empezar con la programación real del juego.

Aquí puedes ejecutar la versión del juego que vamos a comentar en este artículo.
Aquí tienes el enlace al repositorio con todo el código de esta serie de tutoriales.
He estado pensando en cómo abordar el tutorial cuando hable del código. Por un lado, podría programar la versión final del juego, e ir explicándola más o menos siguiendo el orden en el que se ejecuta. Me facilitaría la vida a la hora de escribir el tutorial, pero quizá pasaría por alto conceptos que se me pasaron por la cabeza cuando hice tal cosa, o no quedaría lo suficientemente claro el funcionamiento concreto de algún trozo de código que afecta a otra parte del juego que aún no se ha visto y que se verá muy posteriormente.
Así que finalmente me he decidido por la que me parece más natural: Explicar el código en el mismo orden en el que lo he ido programando. Nadie en su sano juicio pensaría que un juego (o cualquier aplicación ligeramente complicada) se programa del tirón, es decir, escribir línea a línea todo el código final de forma seguida. Normalmente se programa un poco de una clase, se salta a otra clase que la maneja, se vuelve a la primera clase y se le añaden unos pocos atributos y métodos más, etc, siguiendo algún esquema mental. Concretamente, a mí me gusta programar primero una versión muy sencilla que dibuje algo en pantalla. A continuación, hago una muy básica versión del bucle del juego para conseguir un poco de movimiento. Después empiezo a tratar de hacer que ese movimiento obedezca a mis instrucciones por teclado. Así, poco a poco y con pequeños éxitos, voy cogiendo moral para continuar con el desarrollo y no me tiro demasiado tiempo programando si ver con mis propios ojos el efecto que producen los cambios introducidos en la última iteración. También ayuda a la detección temprana de los errores (te garantizo que va a ocurrir sí o sí) que van a ir apareciendo y a solucionarlos lo más pronto posible. Ya me entenderás cuando te pongas a programar vuestro propio juego.
El elemento CANVAS
Una de las novedades más importantes que trajo HTML5 es el elemento CANVAS, y junto a él, un objeto en javascript con un montón de comandos de dibujo gráfico para pintar líneas, arcos, cualquier típo de polígono, etc. Podemos echar un vistazo rápido al conjunto de instrucciones que trae en multitud de páginas web, como por ejemplo esta.
Así que antes de empezar, vamos a comentar un poco sobre lo que nos interesa saber del canvas. Un canvas no es más que un montón de píxeles dispuestos en forma rectangular. Cada píxel está definido con dos coordenadas, su posición X, y su posición Y. El eje (0,0) es, desde tu punto de vista, el vértice superior izquierdo. El eje X se incrementa hacia la derecha, y el eje Y hacia abajo. Los atributos más representativos de un canvas son su contexto, su ancho y su alto. Su ancho y su alto son obvios, determinan el número de píxeles que tienen de anchura y de altura disponibles para dibujar. El contexto es un concepto más avanzado, pero de forma resumida sirve para decirle a javascript qué repertorio de instrucciones de dibujo gráfico quiero usar para pintar en el canvas. Nosotros usaremos el contexto 2d
, que es el más sencillo de aprender, pero que por contra, utilizará solo la CPU para dibujar en pantalla. Pero de momento, eso no nos importa, nuestro juego es muy sencillo e irá rapidísimo en cualquier PC. También existe otro contexto llamado experimental-webgl
, o webgl
cuando deje de ser experimental, que sirve para hacer que la tarjeta gráfica se encargue de realizar los cálculos necesarios para dibujar en pantalla, posibilitando así mundos en 3D muy complejos que cualquier CPU se ahogaría si intentara gestionar.
Entidades del juego, primer vistazo
Una vez tenemos claro de qué va el juego, lo que más nos interesa es diferenciar cada componente dibujable y relevante que lo conforman y lo distingue de otros juegos. Cada componente será al menos una clase, así que sería como identificar las partes importantes que formarán tu juego. En el space invaders casi es evidente que tenemos tres tipos de componentes, que llamaremos por ejemplo entidades: La nave del jugador, sus disparos, y las naves alienígenas. Para no complicarme mucho la vida, en mi juego todo serán rectángulos de diferente tamaño.
La entidad que represente a las naves alienígenas serán rectángulos blancos que se van moviendo en zigzag desde la parte superior de la pantalla hacia la inferior y que pueden morir por colisionar con disparos del jugador. La entidad que representa a la nave del jugador será un rectángulo verde que puede moverse de derecha a izquierda en la parte inferior de la pantalla, y que puede morir si colisiona con alguna nave alienígena. Por último los disparos serán otra entidad rectangular de color amarillo, que se mueven verticalmente de abajo a arriba cuando el jugador dispare, y que pueden colisionar con algún alienígena.
De momento tengo bastante claras las entidades del juego, y me estoy dando cuenta de que todas ellas comparten ciertas características comunes: Son cuadradas, tienen algún color, y tienen la capacidad de moverse. Es una buena oportunidad para aplicar herencia de clases, ahorrándome código repetitivo. De momento solo quiero dibujar en pantalla una imagen estática con todas las naves alienígenas y el jugador, sin movimiento de ninguna clase. De todo lo demás me olvido por el momento. ¿Cómo lo hacemos? Pues bien, creando una clase padre Entity
, y dos clases hijas: Alien
y Player
.
Veámos qué aspecto tiene la primera versión de la clase Entity
:
'use strict'
class Entity {
#x;
#y;
#width;
#height;
#color;
constructor(x, y, width, height, color) {
this.#x = x;
this.#y = y;
this.#width = width;
this.#height = height;
this.#color = color;
}
get x() {
return this.#x;
}
get y() {
return this.#y;
}
get width() {
return this.#width;
}
get height() {
return this.#height;
}
get color() {
return this.#color;
}
}
Como comenté antes, todas las entidades son en realidad rectángulos de colores. Hay varias formas de representar un rectángulo en una estructura de datos, como por ejemplo, mantener una lista con sus cuatro coordenadas, y luego ir dibujando las líneas entre cada par de coordenadas, para finalmente rellenar todo el área que forman de un color. Ello requeriría de por lo menos 6 líneas de código para dibujarlo en pantalla. ¿Hay otra forma más rápida? Sí. Viendo el tutorial de canvas que puse antes, se puede ver que existe una función, fillRect
, que dibuja rectángulos en una sola línea. Fill significa rellenar, asi que esa instrucción lo que hace es pintar el área que forma un rectángulo con el último color de relleno especificado. Éso es justo lo que necesito. Veo que la función necesita 4 parámetros: Una coordenada x
, una coordenada y
(de su vértice superior izquierdo), un width
ancho, y un height
alto. Así que esos atributos son los que meto a mi clase Entity
, de momento. Además, añado otro atributo para almacenar su color. El constructor
simplemente recibe todos esos argumentos, y los guarda en sus respectivos atributos. JavaScript no lo permite, pero tenemos que tener en mente que esta clase es abstracta, es decir, jamás se declararán instancias directamente de ella. Solo instanciaremos a sus futuros hijos.
Como has podido notar, estoy usando una novedad relatívamente reciente de javascript, que consiste en tipar atributos y métodos como privados en nuestras clases. En versiones anteriores donde no era posible usar esta característica del lenguaje, o quienes la evitan por ser demasiado engorrosa, se solía usar la convención de poner un prefijo _
o __
a todos los atributos y métodos que el programador quería proteger, aunque como te puedes imaginar, seguían siendo totalmente públicos. Ya era hora JS. Los que amamos el paradigma POO por fín podemos usar esta característica vital para poder encapsular la información de nuestras clases de otros programadores malintencionados, es decir, ocultar la implementación interna de la clase y hacer que solo mediante métodos públicos que la clase ofrezca, poder modificarla o recuperar alguno de sus valores.
¿Y qué código tienen esas clases hijas? Pues veámoslas.
'use strict'
class Alien extends Entity {
constructor(x, y) {
super(x, y, 40, 25, 'white');
}
}
Y en otro archivo:
'use strict'
class Player extends Entity {
constructor(x, y) {
super(x, y, 40, 25, 'green');
}
}
Tras heredar Entity
, necesitamos declarar un constructor
con sus valores específicos de color y tamaño, aunque necesitan que alguien les diga la coordenada donde van a estar colocados inicialmente. Bueno, fijándonos bien, vemos que lo poco que hacen es una llamada al constructor del padre, diciéndole con qué ancho, alto y color deben dibujarse. Y ya está. El color tiene que tener el formato típico de la web. Puede ser escrito directamente, yellow
, o estilo hexadecimal #1234ab
, RGB rgb(100,255,0)
, RGBA rgba(100,255,0,0.7)
o incluso HSL hsl(230,50%,10%)
. Si tienes alguna duda con alguno de ellos, consulta alguno de los varios millares de tutoriales que hay pululando por la red. Fácil, ¿no?
Renderizado
Nuestras clases de momento son DTOs planos, clases que solo contienen atributos y que sirven como mecanismo de transmisión de argumentos entre otras clases, así que... ¿Quién se encarga de dibujarlas en la pantalla? ¿No debería estar el código de dibujo, dentro de la clase? Esta es una pregunta razonable, con una respuesta difícil. Sí, dicho código podría estar dentro de la clase, pero entonces estaríamos acoplando el juego a la vista. Si lo hiciera así y algún día quisiera cambiar la forma de dibujar el juego, y usar por ejemplo WebGl, o incluso a texto ASCII, estaría obligado a tocar casi todas las clases del juego, con más probabilidades de romperlo.
¿Dónde metemos finalmente el código de renderizado entonces? La respuesta en POO es obvia: En una clase nueva Render2D
con el código mínimo necesario para dibujar nuestras entidades en pantalla:
'use strict'
class Render2D {
#canvas;
#ctx;
constructor(id, width, height, color) {
this.#canvas = document.getElementById(id);
this.#canvas.width = width;
this.#canvas.height = height;
this.#canvas.style.backgroundColor = color;
this.#ctx = this.#canvas.getContext('2d');
}
clear() {
this.#ctx.clearRect(0, 0, this.#canvas.width, this.#canvas.height);
}
drawEntity(entity) {
this.#ctx.fillStyle = entity.color;
this.#ctx.fillRect(entity.x, entity.y, entity.width, entity.height);
}
}
En esta clase centraremos toda la lógica del dibujado del juego. Si el lenguaje lo permitiera, contaría con una interfaz, para poder implementarla en mis clases de render y poder cambiar en un futuro la forma de renderizar de forma muy rápida. En Javascript no existe ese concepto, pero si tenemos clara la idea, podríamos escribir la clase y su lógica de forma que algún día poder cambiarla por otra implementación que dibuje por ejemplo usando WebGL. Su constructor acepta un identificador, el del elemento canvas de la parte HTML. Como esta clase tiene la responsabilidad de dibujar, es también la encargada de gestionar el dónde. Así que le establecemos el típico ancho y alto de los minijuegos, pintamos el fondo de algún color, y guardamos en un atributo su contexto para acceder a él directamente, porque lo necesitaremos para poder usar su paleta de funciones de dibujo gráfico.
Aunque ahora es una tontería, no quiero que pase desapercibido lo de pintar el fondo de negro. Tenía dos opciones: Dibujar el fondo de negro con otro fillRect
del canvas, para pintar después todas las entidades, o ponerle un color negro en el HTML, lo cual es perfecto porque el fondo siempre estará debajo de lo que pinte en el canvas. Ésto último es mucho más rápido, ya he aprovechamos el CSS de HTML (aunque establecido con javascript) para ahorrarnos código de dibujo gráfico, que es más lento de ejecutar (y que además tendría que hacerlo en cada vuelta del bucle principal). Lo que quiero señalar es que no perdáis de vista de que estamos programando juegos dentro de una página web, por lo que hay que aprovechar todas las alternativas que nos brinda HTML y CSS, que muchas veces son menos costosas que hacerlas con funciones de dibujo gráfico. Por ejemplo, si quisiera incluir un menú, quizá hacerlo con elementos HTML sea muchísimo más sencillo que estar dibujando cuadrados y tener que buscarme la vida para saber cuándo se hace click sobre encima de algo que represente un botón.
Como todas las clases a dibujar son subtipos de Entity
, me aprovecho de que comparten los mismos atributos y las puedo dibujar a todas con un único método entity
. Necesitamos especificar un color de relleno, en el atributo fillStyle
del objeto contexto. Luego llamamos a fillRect
con los atributos adecuados, y vualá, tenemos dibujado el rectángulo.
Y aunque en esta fase del juego en realidad no lo necesitamos porque no hay movimiento, metemos un método para borrar todo el contenido del canvas, clear
. Este método hará un clearRect
sobre el objeto contexto del canvas, que como os podéis imaginar, borrará todo el área visible del canvas y se verá el color del background del elemento HTML como mencionamos antes.
La clase Juego, primera versión
Una vez que tenemos definidos las entidades y el componente que va a dibujarlas en pantalla, tenemos que programar alguna clase que las utilice de forma controlada. En un alarde de inspiración llamaremos a esta clase principal, Game
. Nuestra clase juego tendrá que manejar todas las entidades involucradas en una partida, declarándolas y usándo sus métodos públicos. Esta clase será la que contenga el bucle principal, pero de momento se encargará de crear las entidades y dibujarlas quietas en pantalla. Pero... ¿Cómo lo incrustamos todo en el HTML? Es hora de ver la una versión primitiva de la página web donde se ejecutará el juego:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Space Invaders Tutorial 01</title>
<script type="text/javascript" src="js/Entity.js"></script>
<script type="text/javascript" src="js/Player.js"></script>
<script type="text/javascript" src="js/Alien.js"></script>
<script type="text/javascript" src="js/Game.js"></script>
<script type="text/javascript" src="js/Render2D.js"></script>
<script type="text/javascript">
'use strict'
window.addEventListener('load', () => new Game('main'));
</script>
</head>
<body>
<canvas id="main"></canvas>
</body>
</html>
Aún no hemos visto nada de la clase Juego, pero el poco javascript que contiene la página principal se puede entender perfectamente. Para empezar, cargamos los scripts que tenemos hasta el momento en archivos separados, y luego añadimos el listener que será ejecutada cuando cargue la página mediante una llamada en el evento load
del objeto nativo window
. El HTML contiene un elemento CANVAS donde vamos a dibujar el juego, sin ninguna clase de atributos. Ya se encargará nuestra clase Game de darle tamaño y color.
Hay formas y técnicas más adecuadas para lidiar con esta carga inicial del juego, pero para un primer tutorial mejor mantener las cosas simples.
Y por fin, veámos la versión alpha-v0.0.1-no-fake-1-link-mega de la clase Juego:
'use strict'
class Game {
#renderer;
#player;
#aliens;
constructor(canvasId) {
this.#renderer = new Render2D(canvasId, 800, 600, 'black');
this.#init();
}
#init() {
this.#player = new Player(370, 550);
this.#aliens = [];
for (let row = 0; row < 5; row++) {
for (let column = 0; column < 12; column++) {
this.#aliens.push(
new Alien(100 + column * 50, 50 + row * 30)
);
}
}
this.#render();
}
#render() {
this.#renderer.clear();
this.#renderer.drawEntity(this.#player);
this.#aliens.forEach(alien => this.#renderer.drawEntity(alien));
}
}
Su constructor es básico, simplemente creamos la clase render para usarla cuando se necesite.
El método init
tiene algo más de chicha. Mediante llamadas a otros métodos, creamos una instancia de la clase Player y la posicionamos en la coordenada (370, 550), que recordemos, es la posición sobre el canvas en la que situaremos su vértice superior izquierda. Es decir, lo pondremos abajo del todo y casi en el centro de la pantalla.
A continuación hacemos un bucle anidado para instanciar y añadir los aliens a su propia colección.
Para dibujar todos los aliens, tiramos de un bucle anidado. Queremos 5 filas de aliens, y cada fila estará compuesta por 12 aliens. Sabemos que cada alien ocupa 40 píxeles de ancho y 25 de alto, como pusimos en su constructor. Quiero dejarle a cada nave un margen de 5 píxeles por la izquierda, 5 píxeles por la derecha, y 5 píxeles por abajo, lo que hace un total de 50x30 píxeles de superficie para cada alien. 50 píxeles x 12 aliens = 600 píxeles de anchura cada fila de enemigos, y sabiendo que el canvas mide 800x600, para centrarlos tengo que dejar 100 píxeles a cada lado. Con esos datos, ya puedes deducir la fórmula que utilizo en el bucle para posicionar correctamente todos los aliens.
Para los que tengan más nociones de programación, es doloroso ver tantos valores de tamaños y posiciones absolutas harcodeados por todas partes, lo que complicará hacer cambios de proporciones visuales de forma rápida porque todos los valores son coherentes con el tamaño actual del mapa, pero quedará mal si se cambia el valor de cualquiera de ellos. Por la ya mencionada simplicidad seguiremos por este camino. Tal vez aplique una refactorización a este juego en el futuro, o explique alternativas en otra nueva serie de tutoriales más avanzados para evitar depender a toda costa de la resolución del juego. Como expliqué en el artículo anterior, este asunto lo apunto en mi lista de posibles mejoras y sigo avanzando hacia una versión del juego que sea jugable. Cuando haya alcanzado este objetivo, ya habrá tiempo de hacer estos cambios no esenciales.
Y con esto, ya tengo todas las entidades del juego iniciadas y posicionadas correctamente.
Nos queda pintar las entidades, que es una de las tareas principales (de momento la única) del bucle principal del juego. De esto se encargará el método render
, donde una primitiva interfaz gráfica dibujará la puntuación, el mapa, el jugador, y todos los aliens mediante el uso adecuado de los métodos de la clase Render2D
. Cada vez que se dibuje en el canvas, lo hará sobre cualquier cosa que se haya pintado previamente, por lo que antes deberíamos limpiarlo (vaciarlo). Bueno, en esta iteración, nuestras entidades no se mueven. Siempre estarán en el mismo sitio, asi que las entidades de la lista siempre se pintarán en el mismo sitio, con lo que no haría falta limpiar el canvas. Pero esto solo es una situación temporal, por lo que me curo en salud y lo limpio ya.
Como nota al pie, para renderizar los aliens he usado una función flecha sobre un método iterador del array, típicos de la programación funcional.
this.#aliens.forEach(alien => this.#renderer.drawEntity(alien));
El código equivalente usando una función anónima sería:
const self = this;
this.#aliens.forEach(
function (alien) {
self.#renderer.drawEntity(alien);
}
);
Y si estamos más cómodos con una versión old-school:
for (const alien in this.#aliens) {
this.#renderer.drawEntity(alien);
}
O incluso:
for (let i = 0, n = this.#aliens.length; i < n; i++) {
this.#renderer.drawEntity(this.#aliens[i]);
}
Y con ésto, ya tenemos algo que mostrar en pantalla. ¿A que tal y como dije, ver cosas dibujadas os ha subido la moral? Veremos cómo hacer que se muevan en el próximo capítulo.