Tutorial: Jogos em HTML5 – Parte 3: Animação da Bolinha

HTML 5Começamos no tutorial passado a criar um jogo de Breakout e a fazer a raquete do jogo, podendo mover de um lado para o outro. Para fazê-la se mover, nós utilizamos de eventos.

Hoje faremos o comportamento da bolinha: a bolinha vai movendo diagonalmente para qualquer um dos lados. Para criar essa impressão de movimento, devemos aplicar um princípio básico de animação, que é exibir várias imagens com a bolinha de um lado rapidamente, criando a ilusão do movimento.

É importante informar que neste tutorial, vamos apenas trabalhar com a ilusão de movimento da bolinha, e que não veremos animação de sprites ou de personagens agora, ficando para um tutorial futuro.

Objetivo do Dia

Com a raquete já implementada, hoje nós vamos adicionar a bolinha do nosso jogo de Breakout. Assim como cada elemento do jogo, nós precisamos definir antes seu comportamento. A bolinha vai estar inicialmente parada. Quando o jogador apertar a tecla do Espaço, ela começará a subir em uma direção diagonal para um dos lados. Normalmente, quando a bola alcança uma das bordas, ela é rebatida, entretanto, essa rebatida ficará para o próximo tutorial. Vamos aproveitar e fazer uma nova modificação, as teclas das setas ficarão bloqueadas até o usuário apertar a tecla espaço. Então, podemos dizer que o jogo começará somente quando o usuário apertar a tecla espaço, evitando assim que o usuário perca porque não prestou atenção no jogo.

A Bolinha

A criação da bola vai ser semelhante ao da criação da raquete, vista no último tutorial. Começaremos criando uma nova classe chamada Bola:

Objeto Bola:
01 function Bola(x,y,raio){
02   this.x = x;
03   this.y = y;
04   this.raio = raio;
05   this.dirX = (Math.random()>0.5)?3:-3;
06   this.dirY = -3;
07 }

Vamos explicar esse código: na linha 1 nós estamos dizendo que ao criarmos um objeto Bola, precisamos de uma posição x e y, que será a posição do centro da bola. Também precisaremos do tamanho do raio, lembrando que quanto maior o raio, maior vai ser a bola. Nas linhas 5 e 6, estamos definindo dois atributos que determinam a direção inicial que a bola irá. Na linha 6, estamos indicando que a bola estará subindo 3 pixels a cada atualização da imagem. Na linha 5, nós estamos sorteando um número entre 0 e 1. Se este número for maior que 0.5, a bola andará 3 pixels para direita, senão, andará 3 pixels para a esquerda, conforme a imagem abaixo:

Nesta imagem, a bola azul é a bola na posição atual. A seta laranja é o atributo dirY, ou seja, ele indica o quanto a bola moverá na direção Y. A seta roxa é o dirX quando um número sorteado é menor que 0.5 e junto com o dirY, fará o movimento indicado pela seta vermelha. Caso o número sorteado for maior que 0.5, o dirX será a seta marrom e o movimento será indicado pela seta verde. Algo bem básico da geometria analítica.

Agora que temos os dados definidos, vamos criar um método pintar para o nossa classe Bola e fazer com que desenhe a bola na canvas:

Objeto Bola:
01 function Bola(x,y,raio){
02   this.x = x;
03   this.y = y;
04   this.raio = raio;
05   this.dirX = (Math.random()>0.5)?3:-3;
06   this.dirY = -3;
07
08   this.pintar = function(){
09     contexto.beginPath();
10     contexto.arc(this.x,this.y,this.raio,0,2*Math.PI);
11     contexto.fill();
12   }
13 }

Na linha 8 nós definimos o método pintar. Recordando o primeiro tutorial, nós desenhamos um círculo completo nas linhas 9 até 11. Indicamos que começaremos a desenhar na linha 9. Usamos os atributos x, y e raio para definir o círculo a ser desenhado na linha 10 e mandamos desenhar totalmente preenchido na linha 11.

Para podermos testar, agora vamos adicionar a seguinte linha nas definições globais:

Variáveis Globais
01 var bola = new Bola(200,570,10);

Criamos aqui uma instância da nossa classe Bola. Neste caso, nossa bola será posicionada com o centro no ponto (200, 570) com 10 pixels de raio.

Agora na função pintar(), vamos adicionar mais uma linha:

Função pintar():
01 function pintar(){
02   contexto.clearRect(0,0,400,600);
03   jogador.pintar();
04   bola.pintar();
05 }

Na linha 04 adicionamos o método pintar do nosso objeto bola. Se nós testarmos o código agora, a nossa bola é para estar igual a esta imagem:

Movendo a Bolinha
Já temos a capacidade de desenhar a bolinha e também já temos todas as características necessárias para representá-la no nosso grande plano cartesiano que é a nossa canvas e até mesmo de como ela vai se movimentar no começo. Pera aí! Eu disse que definimos como ela vai se mover? Quase, o que definimos com o dirX e o dirY é a direção que a bola se locomove, mas nós não dizemos nada de como vai se mover. Vamos preencher essa lacuna adicionando mais um método, que chamaremos de “mover”, na nossa classe Bola:
Classe Bola:
01 function Bola(x,y,raio){
02   this.x = x;
03   this.y = y;
04   this.raio = raio;
05   this.dirX = (Math.random()>0.5)?3:-3;
06   this.dirY = -3;
07
08   this.pintar = function(){
09     contexto.beginPath();
10     contexto.arc(this.x,this.y,this.raio,0,2*Math.PI);
11     contexto.fill();
12   }
13
14   this.mover = function(){
15     this.x+=this.dirX;
16     this.y+=this.dirY;
17   }
18
19 }

No método “mover”, diremos como a bola se moverá. Interprete o código da seguinte forma: a cada desenho que fizer, mova a posição x da bola dirX pixels e a posição y moverá dirY pixels, ou seja, a combinação (dirX, dirY) é determinante para a velocidade da bolinha. Isso é suficiente para definir a locomoção da bola.
Agora nós precisamos é criar um controlador, uma função que controlará a animação da canvas. Vamos chamar essa função de “mainloop”. Damos esse nome porque nas animações em geral, o “mainloop” é a função responsável em:

  1. Verificar e atualizar a posição de todos os elementos envolvidos;
  2. Desenhar o cenário refletindo o estado de todos os elementos;
  3. Repetir o passo 1;
  4. Repetir o passo 2;
  5. E continue repetindo os passos 1 e 2 até a animação ser encerrada.

No nosso caso, já fizemos um método para ser executado no passo 1 (método mover) e também já fizemos o método para ser executado no passo 2 (função pintar). Agora é criar o mainloop e fazer a função ser executada diversas vezes:
Função mainloop:
01 function mainloop(){
02   bola.mover();
03   pintar();
04 }

Como podemos ver, a linha 2 faz a bola se mover e a linha 3 faz a bola ser pintada no local atual. Estamos quase terminando, falta nós fazermos essa função ser várias vezes. Para isso, podemos usar um temporizador do Javascript. Para fazer isso, primeiro precisamos criar uma variável global que será responsável pelo temporizador. Adicione a seguinte linha nas definições globais:
Definições Globais:
01 var temporizador;

Agora, na função iniciar(), que criamos no tutorial passado, vamos adicionar mais uma linha:
Função iniciar:
1 function iniciar(){
2   var canvas = document.getElementById(“game”);
3   contexto = canvas.getContext(“2d”);
4   jogador.pintar();
5  temporizador = setInterval(mainloop,33);
6 }

Adicionamos a linha 5 na função iniciar, que fará com que a função mainloop seja executada a cada 33 milissegundos. Por que 33 milissegundos? Esse segundo parâmetro definirá o Frame Rate, FPS, Quadros por segundos, ou seja lá como você conhece a unidade de medida do quanto é atualizada a imagem por segundo. No caso, eu quero que a imagem seja atualizada a 30FPS, então como a medida é feita em milissegundos, temos 1 segundo = 1000 milissegundos, 1000/30 = 33.3333333333… milissegundos. Arredondando para 33, a imagens será atualizada a cada 33 milissegundos. Uma coisa importante é que quanto menor esse valor, mais potente o seu computador deve ser. Caso o computador não dê conta de processar tudo que precisa antes do tempo que você especificou, então o sistema começará a ficar lento. Outra coisa importante é que o movimento da bola está dependente do FPS do jogo, ou seja, se nós fizermos o jogo em 60FPS, a bolinha andará mais rápido, se fizer menor, a bola ficará mais devagar. Isto ocorre porque a bolinha percorrerá sempre (dirX*FPS) pixels por segundo no eixo X e (dirY*FPS) pixels no eixo Y. Algo semelhante foi mencionado na Análise do Final Fantasy IX, onde a versão Europeia do jogo tem um FPS menor e fez com que as animações dos personagens ficassem mais lentas e uma das missões que dependia de chegar a um local em até 12 horas acabou sendo praticamente impossível.
Enfim, agora podemos testar: a bolinha vai subindo em diagonal para um dos lados e vai sumir para fora da tela. Isso nós veremos no próximo tutorial. Temos mais coisas para ajeitarmos antes disso.
Dicas de Otimização
Antes de prosseguir, existe outro problema que pode acontecer: a concorrência de processos. Quando um evento ocorre, abre-se um processo para executá-lo. Só que se o evento está tratando algo que pode ser exaustivo para o computador, o mainloop pode demorar a ser executado novamente e causar problemas de lentidão no jogo. Por isso, vamos dar uma otimizada nas leituras das teclas, onde temos um vetor de teclas que guardará o estado dela. Vamos adicionar essa definição global:
Definições Globais:
01 var mapaTecla = new Array();

Agora vamos modificar a função teclaPressionada, removendo todo o código anterior e substituindo por este código:
Função teclaPressionada:
01 function teclaPressionada(evento){
02  mapaTecla[evento.keyCode] = true;
03 }

Pode-se ver que este é um código muito mais elegante que anteriormente. Nós criamos um vetor na definição global e usamos como índice o código da tecla e setamos como verdadeiro quando ela é apertada.
Só que agora precisamos fazer a função reversa desse código. Vamos chama-la de teclaSolta:
Função teclaSolta:
01 function teclaSolta(evento){
02  mapaTecla[evento.keyCode] = false;
03 }

Bem simples, a única coisa que mudou é o que era verdadeiro virou falso. Agora só precisamos associar a função  ao evento onKeyUp nas definições globais:
Definições Globais:
01 window.onkeyup = teclaSolta;

E para encerrar, quem vai cuidar do tratamento das teclas vai ser a função mainloop, que vai ficar assim:
Função mainloop:
01 function mainloop(){
02   if(mapaTecla[37]==true){
03     jogador.mover(-2);
04   }
05
06   if(mapaTecla[39]==true){
07     jogador.mover(2);
08   }
09
10   bola.mover();
11   pintar();
12 }

Adicionamos aqui as linhas 02 até 08 e que é simples de entender: se a tecla 37 (seta para esquerda) estiver apertada, a raquete do jogador vai mover 2 pixels para esquerda. Se a tecla 39 (Seta para direita) foi apertada, o jogador move-se dois pixels para direita. Neste caso, se as duas teclas forem pressionadas, ambos vão ser executada e vão se anular. Dessa forma, deixamos os eventos bem rápidos não tendo perigo que o mainloop se atrase devido aos processos de evento.

Aperte Espaço para Começar
Um dos requisitos que coloquei é que o jogo deve começar somente quando apertar a tecla Espaço. Isso é facilmente tratável se nós trabalharmos o jogo como uma máquina de estados.

O estado inicial 0 é o estado de “Pausa”, e qualquer interação que não seja a tecla Espaço é ignorada. Quando a tecla Espaço é apertada, então ele vai para o estado 1 que é o estado de “Em Jogo”. Vamos implementar isso, colocando primeiramente mais uma variável global:

Definições Globais:
01 var estado = 0;

Definímo-la e já a colocamos no estado inicial “0”. Agora, vamos modificar todos os códigos do mainloop que são afetados de acordo com o estado:

Função mainloop:
01 function mainloop(){
02   if(mapaTecla[37]==true && estado==1){
03     jogador.mover(-2);
04   }
05
06   if(mapaTecla[39]==true && estado==1){
07     jogador.mover(2);
08   }
09
10   if(mapaTecla[32]==true && estado==0){
11     estado = 1;
12   }
13
14   if(estado==1){
15     bola.mover();
16   }
17
18   pintar();
19 }

Nas linhas 2 e 6, nós modificamos as condicionais para dizer que além das teclas estarem apertadas, para a raquete do jogador se mover deve-se estar no estado 1 (em jogo). Também, nas linhas 14 até 16, dizemos que a bola só vai se mover se o estado do jogo é 1 também. E, finalmente, nas linhas 10 até 12, nós adicionamos um novo comando: se a tecla 32 (Espaço) foi pressionada e o estado for 0 (pausa), então o estado passa a ser 1 (em jogo). É esse comando que dará o início da partida. Se testarmos agora o jogo, poderá ver que o jogo ficará parado enquanto não apertamos o espaço primeiro. Quando apertar espaço, a bolinha sairá para um dos lados do jogo, já que trabalharemos no próximo tutorial para que a bola fique na área de jogo.
Considerações Finais
Hoje nós vimos como fazer com que a bolinha se locomova, uma animação bem simples de translação. Para que tenhamos esse efeito, nós precisamos da posição atual dela e de um parâmetro que determine a direção e a quantidade de pixels a ser locomovida (velocidade da bolinha). Também criamos o mainloop, que é bastante usado em jogos, uma função que controlará tudo no jogo. E fizemos otimizações quanto aos botões, além de criarmos uma máquina de estados para termos um controle melhor do fluxo do jogo. Então pessoal, nos vemos no próximo tutorial!

Thalisson Christiano de Almeida

Thalisson Christiano de Almeida

Formado em Ciência da Computação (UDESC). Foi Programador da Céu Games e professor do Técnico em Informática do SENAI-SC. Atualmente, trabalha na empresa By Seven. Já foi jogador de xadrez e praticou kung-fu, ambos por 4 anos. Hoje é praticante do Jiu-jitsu, esperando que não fique nos 4 anos. Não tem preferência de tipos de jogos em especifico, variando desde jogos casuais de Facebook até jogos mais hardcore.

Send this to a friend