Guía fácil de juegos HTML5

Daniel X. Moore
Daniel X. Moore

Introducción

¿Quieres crear un juego con Canvas y HTML5? Sigue este instructivo y estarás listo en poco tiempo.

En el instructivo, se supone que tienes, al menos, un nivel intermedio de conocimientos de JavaScript.

Primero, puedes jugar el juego o ir directamente al artículo y ver el código fuente del juego.

Crea el lienzo

Para dibujar, necesitaremos crear un lienzo. Como esta es una guía sin lágrimas, usaremos jQuery.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

Bucle de juego

Para simular la apariencia de un juego fluido y continuo, queremos actualizar el juego y volver a dibujar la pantalla más rápido de lo que pueden percibir la mente y el ojo humanos.

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

Por ahora, podemos dejar los métodos de actualización y dibujo en blanco. Lo importante es saber que setInterval() se encarga de llamarlos periódicamente.

function update() { ... }
function draw() { ... }

Hello World

Ahora que tenemos un bucle de juego en marcha, actualicemos nuestro método de dibujo para dibujar texto en la pantalla.

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}

Eso es muy útil para el texto estático, pero como ya tenemos configurado un bucle de juego, deberíamos poder hacer que se mueva con bastante facilidad.

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Ahora pruébala. Si estás siguiendo los pasos, debería estar en movimiento, pero también debería dejar las veces anteriores en que se dibujó en la pantalla. Tómate un momento para adivinar por qué podría ser así. Esto se debe a que no estamos borrando la pantalla. Por lo tanto, agreguemos código para borrar la pantalla al método de dibujo.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Ahora que tienes texto que se mueve en la pantalla, estás a mitad de camino para tener un juego real. Solo tienes que ajustar los controles, mejorar el juego y mejorar los gráficos. Bien, tal vez sea 1/7 del camino para tener un juego real, pero la buena noticia es que hay mucho más en el instructivo.

Crea el reproductor

Crea un objeto para contener los datos del jugador y ser responsable de elementos como el dibujo. Aquí, creamos un objeto jugador con un literal de objeto simple para contener toda la información.

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

Por ahora, usaremos un rectángulo de color simple para representar al jugador. Cuando dibujemos el juego, borraremos el lienzo y dibujaremos al jugador.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

Controles del teclado

Cómo usar las teclas de acceso rápido de jQuery

El complemento Hotkeys de jQuery facilita mucho el manejo de teclas en todos los navegadores. En lugar de llorar por problemas indescifrables de keyCode y charCode entre navegadores, podemos vincular eventos de la siguiente manera:

$(document).bind("keydown", "left", function() { ... });

No tener que preocuparse por los detalles de qué claves tienen qué códigos es una gran ventaja. Solo queremos poder decir algo como "cuando el jugador presione el botón hacia arriba, haz algo". Las teclas de acceso rápido de jQuery permiten hacerlo de forma sencilla.

Movimiento del jugador

La forma en que JavaScript controla los eventos del teclado está completamente basada en eventos. Eso significa que no hay una consulta integrada para verificar si una clave está inactiva, por lo que tendremos que usar la nuestra.

Es posible que te preguntes: "¿Por qué no usar una forma de controlar las claves basada en eventos?" Bueno, esto se debe a que la tasa de repetición del teclado varía según el sistema y no está vinculada al tiempo del bucle de juego, por lo que el juego puede variar mucho de un sistema a otro. Para crear una experiencia coherente, es importante que la detección de eventos del teclado esté bien integrada en el bucle de juego.

La buena noticia es que incluí un wrapper de JS de 16 líneas que hará que las consultas de eventos estén disponibles. Se llama key_status.js y puedes consultar el estado de una clave en cualquier momento. Para ello, revisa keydown.left, etcétera.

Ahora que podemos consultar si las teclas están presionadas, podemos usar este método de actualización simple para mover al jugador.

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

Pruébala.

Es posible que notes que el reproductor se puede mover fuera de la pantalla. Limitemos la posición del jugador para mantenerlo dentro de los límites. Además, el reproductor parece ser un poco lento, así que también aumentemos la velocidad.

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

Agregar más entradas será igual de fácil, así que agreguemos algún tipo de proyectil.

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) Well at least adding the key binding was easy...
};

Agrega más objetos de juego

Proyectiles

Ahora agreguemos los proyectiles de verdad. Primero, necesitamos una colección para almacenarlos todos:

var playerBullets = [];

A continuación, necesitamos un constructor para crear instancias de viñetas.

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

Cuando el jugador dispara, debemos crear una instancia de bala y agregarla a la colección de balas.

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

Ahora debemos agregar la actualización de los viñetas a la función del paso de actualización. Para evitar que la colección de viñetas se llene de forma indefinida, filtramos la lista de viñetas para que solo incluya las viñetas activas. Esto también nos permite quitar las balas que chocaron con un enemigo.

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

El último paso es dibujar los viñetas:

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

Enemigos

Ahora es momento de agregar enemigos de la misma manera que agregamos las balas.

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

Carga y dibuja imágenes

Es genial ver todos esos cuadros volando, pero sería aún mejor tener imágenes para ellos. Cargar y dibujar imágenes en el lienzo suele ser una experiencia emotiva. Para evitar ese dolor y sufrimiento, podemos usar una clase de utilidad simple.

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

Detección de colisiones

Tenemos todos estos elementos volando por la pantalla, pero no interactúan entre sí. Para que todo sepa cuándo explotar, necesitaremos agregar algún tipo de detección de colisión.

Usemos un algoritmo simple de detección de colisiones rectangulares:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

Hay algunas colisiones que queremos verificar:

  1. Balas del jugador => Naves enemigas
  2. Jugador => Naves enemigas

Construyamos un método para controlar las colisiones a las que podemos llamar desde el método de actualización.

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

Ahora debemos agregar los métodos de explosión al jugador y a los enemigos. Esto los marcará para su eliminación y agregará una explosión.

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

Sonido

Para completar la experiencia, agregaremos algunos efectos de sonido agradables. Los sonidos, al igual que las imágenes, pueden ser un poco difíciles de usar en HTML5, pero gracias a nuestra fórmula mágica sin lágrimas sound.js, el sonido puede ser muy simple.

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

Aunque la API ahora no tiene seccionamientos, agregar sonidos es actualmente la forma más rápida de hacer que tu aplicación falle. Es común que los sonidos se corten o cierren toda la pestaña del navegador, así que prepárate.

Farewell

Una vez más, aquí tienes la demostración completa del juego en funcionamiento. También puedes descargar el código fuente como un archivo ZIP.

Espero que hayas disfrutado aprendiendo los conceptos básicos para crear un juego simple en JavaScript y HTML5. Si programamos en el nivel correcto de abstracción, podemos aislarnos de las partes más difíciles de las APIs y ser resilientes ante los cambios futuros.

Referencias