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 da por sentado que tienes un nivel intermedio de conocimiento de JavaScript.

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

Crea el lienzo

Para dibujar cosas, tendremos que 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 la mente y el ojo humanos pueden percibir.

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 que setInterval() se encarga de llamarlos periódicamente.

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

Hello World

Ahora que tenemos un bucle de juego, 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 bueno para el texto estático, pero como ya tenemos un bucle de juego configurado, 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);
}

Prueba eso. Si la sigues, debería estar en movimiento, pero también dejar las veces en que se dibujó en la pantalla. Tómate un momento para adivinar por qué podría ser el caso. Esto se debe a que no estamos borrando la pantalla. Así que agreguemos un poco de código de borrado de 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 el texto se mueve por la pantalla, estás a mitad de camino para disfrutar de un juego real. Solo ajusta los controles, mejora el juego y retoca los gráficos. Podría ser un séptimo lugar del camino para tener un juego real, pero la buena noticia es que el instructivo requiere mucho más.

Creando el reproductor

Crear un objeto para contener los datos del jugador y ser responsable de realizar, por ejemplo, dibujar Aquí creamos un objeto del 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 el momento, estamos usando un rectángulo de color simple para representar al jugador. Cuando dibujemos el juego, quitaremos el lienzo y dibujaremos al jugador.

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

Controles del teclado

Usa las teclas de acceso rápido de jQuery

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

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

No tener que preocuparte por los detalles de qué claves tienen qué códigos es una gran ventaja. Solo queremos poder decir frases como “cuando el jugador presione el botón hacia arriba, haga algo”. Las teclas de acceso rápido de jQuery lo permiten.

Movimiento del jugador

La forma en que JavaScript controla los eventos del teclado está completamente controlada por 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 solo una forma controlada por eventos para manejar las claves?”. Se debe a que la tasa de repetición del teclado varía entre sistemas y no está vinculada al tiempo del bucle de juego, por lo que el juego podría variar mucho de un sistema a otro. Para crear una experiencia coherente, es importante tener la detección de eventos del teclado estrechamente integrada con el bucle de juego.

La buena noticia es que incluí un wrapper de JS de 16 líneas que pondrá a disposición la consulta de eventos. Se llama key_status.js y puedes consultar el estado de una clave en cualquier momento si marcas keydown.left, etcétera.

Ahora que podemos consultar si las teclas están inactivas, podemos usar este método de actualización simple para mover el reproductor.

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

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

Inténtalo.

Tal vez notes que el reproductor se puede mover fuera de la pantalla. Restringiremos la posición del jugador para mantenerlo dentro de los límites. Además, el reproductor parece un poco lento, así que 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 proyectiles.

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...
};

Agregar más objetos del juego

Proyectiles

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

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 las viñetas a la función de paso de actualización. A fin de evitar que la colección de viñetas se llene de forma indefinida, filtramos la lista para incluir solo las viñetas activas. Esto también nos permite eliminar balas que colisionaron con un enemigo.

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

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

El último paso es marcar las viñetas:

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

Enemigos

Ahora es momento de agregar enemigos de la misma manera en que agregamos las viñetas.

  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();
  });
}

Cómo cargar y dibujar imágenes

Es genial ver todos esos cuadros volando por todos lados, pero tener imágenes para ellos sería aún mejor. Cargar y dibujar imágenes sobre lienzo suele ser una experiencia terrible. Para evitar ese dolor y desdicha, 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

Hay muchas ofertas en pantalla, pero no interactúan entre sí. Para que sepa cuándo explotar, tendremos que agregar algún tipo de detección de colisiones.

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 un par de colisiones que queremos verificar:

  1. Viñetas del jugador => Naves enemigas
  2. Jugador => Naves enemigas

Creemos un método para controlar las colisiones a las que podemos llamar desde el método update.

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 explode al jugador y a los enemigos. Esto los marcará para que se quiten 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 dulces. Los sonidos, al igual que las imágenes, pueden resultar complicados de usar en HTML5, pero, gracias a nuestra fórmula mágica, Sound.js, puede hacer que el sonido sea muy simple.

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

function Enemy(I) {
  ...

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

Aunque la API ahora es sencilla, agregar sonidos es, por el momento, la forma más rápida de hacer fallar tu aplicación. Es común que los sonidos corten o eliminen por completo la pestaña del navegador, así que prepara los pañuelos.

Despedida

Una vez más, esta es la demostración completa del juego. También puedes descargar el código fuente en formato ZIP.

Bueno, espero que hayas disfrutado de aprender los conceptos básicos para crear un juego simple en JavaScript y HTML5. Cuando 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