Não há guia de lágrimas para jogos HTML5

Daniel X. Moore
Daniel X. Moore

Introdução

Então você quer criar um jogo usando Canvas e HTML5? Siga este tutorial e você estará no caminho certo em pouco tempo.

O tutorial pressupõe que você tenha, pelo menos, um nível intermediário de conhecimento de JavaScript.

Primeiro, jogue o jogo ou vá diretamente para o artigo e confira o código-fonte do jogo.

Como criar a tela

Para desenhar coisas, precisamos criar uma tela. Como este é um guia sem lágrimas, usaremos o 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');

Loop de jogo

Para simular a aparência de uma jogabilidade suave e contínua, queremos atualizar e redesenhar a tela com mais rapidez do que a mente humana e os olhos podem perceber.

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

Por enquanto, podemos deixar a atualização e os métodos de desenho em branco. O importante é saber que setInterval() faz a chamada periódicas deles.

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

Hello World

Agora que temos um loop de jogo, vamos atualizar nosso método de desenho para desenhar um texto na tela.

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

Isso é muito legal para texto fixo, mas como já temos um loop de jogo configurado, devemos conseguir mover com facilidade.

var textX = 50;
var textY = 50;

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

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

Agora vamos tentar. Se você estiver acompanhando, ele deve estar se movendo, mas também deixando as vezes anteriores em que foi desenhado na tela. Reserve um momento para adivinhar por que isso pode acontecer. Isso acontece porque não estamos limpando a tela. Então, vamos adicionar um código de limpeza de tela ao método de desenho.

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

Agora que um texto está se movendo na tela, você está a meio caminho de um jogo de verdade. Basta reforçar os controles, melhorar a jogabilidade, retoque os gráficos. Talvez um sétimo passo do caminho para ter um jogo de verdade, mas a boa notícia é que há muito mais no tutorial.

Criar o player

Crie um objeto para armazenar os dados do jogador e ser responsável por coisas como desenhar. Aqui, criamos um objeto de jogador usando um literal de objeto simples para armazenar todas as informações.

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

Estamos usando um retângulo colorido simples para representar o jogador por enquanto. Quando desenharmos o jogo, limparemos a tela e desenharemos o jogador.

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

Controles do teclado

Como usar as teclas de atalho do jQuery

O plug-in de teclas de atalho jQuery facilita muito o gerenciamento de teclas em diferentes navegadores. Em vez de lamentar por problemas indecifráveis de keyCode e charCode entre navegadores, podemos vincular eventos da seguinte maneira:

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

Não ter que se preocupar com os detalhes de quais chaves têm quais códigos é uma grande vitória. Queremos apenas poder dizer coisas como "quando o jogador pressiona o botão para cima, faça algo". As teclas de atalho do jQuery permitem isso muito bem.

Movimento do jogador

A maneira como o JavaScript lida com eventos de teclado é totalmente orientada a eventos. Isso significa que não há uma consulta integrada para verificar se uma chave está inativa, portanto, teremos que usar a nossa própria.

Você pode estar se perguntando: “Por que não usar uma forma orientada a eventos de lidar com chaves?” Isso ocorre porque a taxa de repetição do teclado varia de acordo com o sistema e não está vinculada ao tempo do loop de jogo. Por isso, a jogabilidade pode variar muito de um sistema para outro. Para criar uma experiência consistente, é importante que a detecção de eventos do teclado seja totalmente integrada ao loop de jogo.

A boa notícia é que incluí um wrapper JS de 16 linhas que disponibilizará a consulta de eventos. Ele é chamado de key_status.js, e você pode consultar o status de uma chave a qualquer momento verificando keydown.left etc.

Agora que podemos consultar se as teclas estão desativadas, podemos usar esse método de atualização simples para mover o jogador.

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

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

Vá em frente e dê uma chance.

Você pode notar que o player pode ser retirado da tela. Vamos fixar a posição do jogador para que ele fique dentro dos limites. Além disso, o jogador parece lento, então vamos aumentar a velocidade também.

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

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

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

Adicionar mais entradas também será fácil, então vamos adicionar alguns tipos de projéteis.

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

Adicionar mais objetos de jogo

Projéteis

Agora, vamos adicionar os projéteis de verdade. Primeiro, precisamos de uma coleção para armazenar todos eles em:

var playerBullets = [];

Em seguida, precisamos de um construtor para criar instâncias de marcador.

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

Quando o jogador atira, precisamos criar uma instância de bala e adicioná-la à coleção 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
  };
};

Agora precisamos adicionar a atualização dos marcadores à função da etapa de atualização. Para evitar que a coleção de marcadores fique cheia indefinidamente, filtramos a lista para incluir apenas os marcadores ativos. Isso também nos permite remover balas que colidiram com um inimigo.

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

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

O passo final é desenhar os marcadores:

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

Inimigos

Agora é hora de adicionar inimigos da mesma forma que adicionamos as 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();
  });
}

Como carregar e desenhar imagens

É legal observar todas aquelas caixas voando, mas ter imagens para elas seria ainda mais legal. Carregar e desenhar imagens na tela geralmente é uma experiência difícil. Para evitar essas dificuldades, podemos usar uma classe de utilitários simples.

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

  ...
}

Detecção de colisão

Temos todos esses objetos voando pela tela, mas eles não estão interagindo entre si. Para que tudo saiba quando explodir, precisamos adicionar algum tipo de detecção de colisão.

Vamos usar um algoritmo retangular simples de detecção de colisão:

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

Há algumas colisões que queremos verificar:

  1. Balas do jogador => Naves do inimigo
  2. Jogador => Naves do inimigo

Vamos criar um método para lidar com as colisões que podemos chamar pelo método de atualização.

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

Agora, precisamos adicionar os métodos de explosão ao jogador e aos inimigos. Isso os sinalizará para remoção e adicionará uma explosão.

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

Áudio

Para completar a experiência, vamos adicionar alguns efeitos sonoros legais. Sons, como imagens, podem ser difíceis de usar no HTML5, mas graças à nossa fórmula mágica sem lágrimas sound.js, o som pode ser muito simples.

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

function Enemy(I) {
  ...

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

A API agora não tem interrupções, mas adicionar sons é a maneira mais rápida de causar falhas no app. Não é incomum que os sons sejam cortados ou derrubados em toda a guia do navegador, então prepare os tecidos.

Adeus

Aqui está a demonstração completa do jogo em funcionamento. Também é possível fazer o download do código-fonte como um zip.

Espero que você tenha gostado de aprender o básico da criação de um jogo simples em JavaScript e HTML5. Ao programarmos no nível de abstração certo, podemos nos isolar das partes mais difíceis das APIs, além de ser resilientes diante de mudanças futuras.

Referências