Tutorial: Jogos em HTML5 – Parte 4: Tratamento de Colisão

No último tutorial, criamos a bolinha e mostramos como fazê-la se movimentar pelo campo. Entretanto, logo que ela começa a se mover, ela vai embora para o infinito e além. Além disso, melhoramos alguns aspectos do tratamento das teclas para evitarmos futuros problemas de desempenho no site.

Hoje vamos fazer com que a bolinha fique dentro da área, fazendo com que ela rebata nas bordas e na raquete. A única exceção vai ser a borda inferior, pois ela vai ser a condição de que o jogador perdeu o jogo.

Para que isso seja possível, devemos fazer o tratamento de colisão entre a bola e os outros elementos.

O que é Tratamento de Colisão?

Como podemos ver, ao testar o jogo do jeito que está, a bola vai em direção sorteada e vai para o além da borda. Mesmo que tivesse a raquete, a bola passaria pela raquete como se ela não existisse. Para evitar que a bola transpasse as bordas e a raquete, precisamos avisar o computador que ela está colidindo com a borda/raquete e que precisamos mudar a direção dela. Esse ato de avisar e modificar a trajetória é o que chamamos de Tratamento de Colisão.

Ele se dá em duas partes: a detecção e a resposta de colisão. A primeira nós utilizamos o nosso conhecimento em geometria analítica para verificar se dois objetos estão ou não colidindo. Caso esteja colidindo, nós realizamos cálculos para modificar o movimento dos objetos devido a colisão. Isso é o que chamamos de Resposta de Colisão.

A resposta de colisão é algo que deve-se avaliar cuidadosamente. Neste exemplo de Breakout, nós vamos simplesmente mudar a direção da bola. Entretanto, em jogos mais realistas, pode-se aplicar cálculos físicos bem avançados, que envolvam, por exemplo, atrito e forças de ventos.

 

Vá para outro lado

A parte mais fácil aqui vão ser as respostas de colisão, então vamos já implementá-las. Na definição da classe Bola, vamos adicionar mais duas classes: inverterX e inverterY:

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    this.inverterX = function(){
  20        this.dirX *= -1;
  21     }
  22   
  23     this.inverterY = function(){
  24        this.dirY *= -1;
  25     }
  26   }
Na linha 19, adicionamos o método inverterX(). que contém apenas um comando: multiplique a direção de X por -1. Esse método será usado quando a bola bater nas bordas direita e esquerda. Para a bola bater na borda esquerda, ela obrigatoriamente deve estar indo para esquerda, ou seja, o dirX será negativo (exemplo: -3). Quando ele atingir a borda, o inverterX() multiplicará por -1 e, com isso, dirX ficará positivo ((-3)*(-1)= 3), e fará a bola ir para o lado direito.

O oposto ocorrerá para a borda direita, onde a multiplicação tornará o dirX positivo em negativo. Para o inverterY() é usada a mesma lógica, com a diferença que, ao invés de esquerda e direita, será para cima (dirY negativo) e para baixo (dirY positivo). A imagem abaixo mostra as quatros possibilidades de combinação com dirX, dirY (em vermelho) e para onde a bola irá de acordo com as mesmas (em verde):

Não saia da área!

Agora que colocamos já a resposta, vamos começar a dar os limites para a bola. Para isso, vamos dar uma olhada neste esboço da área de jogo:

Nesta imagem, a área em cinza é a área de jogo. As bordas em preto são os limites que acompanham o eixo x e y. As bordas laranja são os limites que estão paralelos a ambos os eixos. Temos nesta imagem os seguintes segmentos de reta que limitam a área de jogo: os segmentos AB, BC, CD e DA. No nosso caso, os segmentos DA e BC são as bordas que, quando a bola atingi-las, vão inverter a direção da bola no eixo X. O segmento AB inverterá a direção da bola no eixo Y. Finalmente, caso a bola atinja o segmento CD, aqui vai ter uma resposta diferente, o jogador perderá o jogo.

Então vamos começar a trabalhar com a detecção de colisão, começando pelo segmento AD. O segmento AD tem como principal característica o fato de que ele está sempre no eixo Y, ou seja, qualquer ponto do segmento vai possuir um valor de x igual 0. A detecção aqui é fácil, pois como AD limita todo o campo, podemos simplesmente dizer que se a parte mais a esquerda da bola chegar ao um ponto onde X=0, é para rebater a bola. Vamos colocar isso no código, adicionando um método verificaColisao na classe Bola:

Classe Bola:
  01   function Bola(x,y,raio){
  25   …
  26   
  27     this.verificaColisao = function(){
  28       if((this.x-this.raio)<=0){
  29      this.inverterX();
  30    }
  31     }
  32   }
  
Aqui no código, estamos indicando para o computador que (vírgula) se o ponto mais a esquerda da bolinha (x – raio = ponto mais a esquerda, ver a imagem abaixo) estiver antes ou no ponto 0, então invertemos o sentido da direção X.

Essa lógica pode ser aplicada nas outras duas bordas que rebatem a bolinha. Para o segmento BC, é o ponto mais a direita (x+raio) tem que ultrapassar a reta, cujo x será sempre 400 (lembre-se que definimos o tamanho 400×600). Para o segmento AB, é o ponto mais alto (y-raio) que deve passar a linha de Y=0. O código ficará assim:

Classe Bola:
  01   function Bola(x,y,raio){
  25   …
  26   
  27     this.verificaColisao = function(){
  28       if((this.x-this.raio)<=0 || (this.x+this.raio)>=400){
  29      this.inverterX();
  30    }
  31   
  32    if((this.y-this.raio)<=0){
  33      this.inverterY();
  34    }
  35     }
  36   }
  
Como a consequência da colisão com os segmentos AD e BC é o mesmo, então juntamos em uma única condição na linha 28: Se o ponto mais a esquerda for menor ou igual a 0 OU o ponto mais a direita for maior ou igual a 400, então inverta X. Na linha 32, trato do segmento AB da mesma forma, mas como a consequência da colisão é a inversão do sentido no eixo Y, então fica separado da condição anterior.

E enquanto o segmento CD? Neste caso, precisamos detectar a colisão, mas a consequência desta colisão não é um efeito na física do jogo, e sim um alerta. Estes tipos de detecção são conhecido como sensores. Para dar um efeito legal, vamos fazer com que ativemos o sensor quando a bolinha passar totalmente pelo segmento CD. A lógica ficará a mesma, só que para dar a impressão da bolinha saindo do campo, faremos com que a bola passe totalmente pela reta, ou seja, o topo da bola deve ser maior que o valor de Y na altura do segmento, que no nosso caso é 600. Veja o código:

Classe Bola:
  01   function Bola(x,y,raio){
  25   …
  26   
  27     this.verificaColisao = function(){
  28       if((this.x-this.raio)<=0 || (this.x+this.raio)>=400){
  29      this.inverterX();
  30    }
  31   
  32    if((this.y-this.raio)<=0){
  33      this.inverterY();
  34    }
  35   
  36    if((this.y-this.raio)>=600){
  37      estado=2;
  38    }
  39     }
  40   }
  
Na linha 36 adicionamos a condição: Se o ponto mais alto da bola passar de 600, então a variável estado vai para 2. Mas peraí? O que significa estado=2? Então, acabamos de adicionar mais um estado para o nosso jogo: a que o jogo foi perdido. Então a nossa máquina de estados atual fica dessa maneira:

Onde:

  •   0: Jogo em pausa;
  •   1: Jogo em andamento;
  •   2: Jogo perdido;

Agora, antes de testar, falta adicionarmos a invocação do método no mainloop, que ficará assim:

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

Simplesmente invocamos o método na linha 15. Agora sim, pode testar.

 

Não perca o Jogo!

Agora a bolinha não sai mais da área de jogo, a não ser quando sai pela parte inferior, o que resulta no fim de jogo. Você pode mover a raquete, só que nada adianta porque a bolinha simplesmente passa direto. Os métodos que vimos para detectar colisão não vai funcionar, devido a natureza diferente da raquete com as bordas: elas podem ser considerada sem limites, uma linha que vem de -infinito até +infinito. Os quatros pontos que usamos definem os segmentos que dão limite para a área do jogo. A raquete é realmente um segmento de reta, tem um início e um final e para dificultar mais ainda, ela é móvel. Mas isso, nada que a nossa amada Geometria Analítica não possa resolver.

Esse caso é aquele que precisamos verificar se um círculo (a bola) está sobrepondo um retângulo (raquete). O primeiro passo é verificar o canto do retângulo mais próximo do círculo. Para isso, vamos ter que implementar uma função clamp, que apesar de muitas linguagens possuírem, o Javascript não tem. O código é o seguinte:

Função Clamp:
  01   function clamp(val, min, max){
  02      return Math.max(min, Math.min(max,val));
  03   }
  
Como podem ver, é uma função simples onde o min e o max são os valores na qual queremos ver qual é o mais próximo de val. Com essa função, podemos iniciar a construção da função que verificará a colisão entre a bola e a raquete. Vamos chamá-la de detectarColisaoRaquetexBola:

Função detectarColisaoRaquetexBola:
  01   function detectarColisaoRaquetexBola(){
  02      var xMaisProximo = clamp(bola.x, jogador.x, (jogador.x+jogador.largura));
  03     var yMaisProximo = clamp(bola.y, jogador.y, (jogador.y+jogador.altura));
  04   
  05   }
  
O valor de base é o centro da bola. A partir disso, nós pegamos os valores de x da bola e verificamos qual dos lados horizontais do retângulo é o mais próximo. Faremos o mesmo para os valores de y para o lado vertical mais próximo. Na imagem abaixo temos três exemplos que mostram quais pontos serão pegos:

Agora, calculamos a distância entre o centro do círculo e o canto mais próximo. Para isso, usamos o nosso conhecidíssimmo Teorema de Pitágoras. Como assim, veja a imagem abaixo:

A imagem acima mostra que podemos criar um triângulo retângulo entre o centro do círculo e o canto escolhido como o mais próximo, cuja hipotenusa é a distância entre os dois pontos e os catetos são formados pela diferença entre o x do círculo e o x do ponto mais próximo (C1) e a diferença do y de ambos (C2). E o que diz o teorema de Pitágoras? A soma dos quadrados dos catetos é igual ao quadrado da hipotenusa. Ou seja:

H² = C1² + C2²

Adaptando a fórmula para os dados que nós temos, a fórmula fica:

H² = (PontoRetangulo.x – circulo.x)²+(PontoRetangulo.y – circulo.y)²

Aplicando no código, a nossa função ficará:

Função detectarColisaoRaquetexBola:
  01   function detectarColisaoRaquetexBola(){
  02      var xMaisProximo = clamp(bola.x, jogador.x, (jogador.x+jogador.largura));
  03     var yMaisProximo = clamp(bola.y, jogador.y, (jogador.y+jogador.altura));
  04   
  05     var distanciaX = bola.x – xMaisProximo;
  06     var distanciaY = bola.y – yMaisProximo;
  07     var distancia = (distanciaX*distanciaX)+(distanciaY*distanciaY);
  08   }
  
Na linha 05 nós calculamos o primeiro cateto (C1 da imagem), na linha 06, calculamos o segundo cateto (C2) e na linha 07, calculamos o quadrado da hipotenusa. Note que não tirei a raiz quadrada para dar a distância real, isso porque a operação da raiz quadrada é uma função que exige bastente do processador.

Agora, o último passo é verificar se a distância é menor que o raio do círculo. Se isso for afirmativo, os dois objetos estão colidindo. Vamos adicionar mais um comando no nosso código:

Função detectarColisaoRaquetexBola:
  01   function detectarColisaoRaquetexBola(){
  02      var xMaisProximo = clamp(bola.x, jogador.x, (jogador.x+jogador.largura));
  03     var yMaisProximo = clamp(bola.y, jogador.y, (jogador.y+jogador.altura));
  04   
  05     var distanciaX = bola.x – xMaisProximo;
  06     var distanciaY = bola.y – yMaisProximo;
  07     var distancia = (distanciaX*distanciaX)+(distanciaY*distanciaY);
  08   
  09     return distancia < (bola.raio*bola.raio);
  10   }
  
Aqui, na linha 09, eu mando retornar o resultado da comparação. Note que aqui eu estou comparando o quadrado da distância com o quadrado do raio. Isso é para compensar o fato de nós não tirarmos a raiz quadrada da distância, e entre tirar a raiz quadrada da distância ou elevar ao quadrado o raio, o que possui o melhor desempenho é o último.
Agora, vamos finalizar adicionando a função do tratamento de colisão na classe bola:

Classe Bola:
  01   function Bola(x,y,raio){
  25   …
  26   
  27     this.verificaColisao = function(){
  28       if((this.x-this.raio)<=0 || (this.x+this.raio)>=400){
  29      this.inverterX();
  30    }
  31   
  32    if((this.y-this.raio)<=0 || detectarColisaoRaquetexBola()){
  33      this.inverterY();
  34    }
  35   
  36    if((this.y-this.raio)>=600){
  37      estado=2;
  38    }
  39     }
  40   }

Aqui, invocamos a função na linha 32 juntamente com a verificação de colisão com a borda superior. Se um dos dois ocorrer, a bola terá o sentido do eixo Y invertido. O único possível problema é a impressão que a bola causará caso ela bater na lateral da raquete, mas esse problema é diminuído pela fina altura do retângulo que forma a raquete.

 

Considerações Finais

Neste tutorial foi visto tratamento de colisão. Ele pode ser dividido em duas partes: a detecção e a resposta. A detecção consiste em verificar se dois objetos estão colidindo, ou seja, um está sobrepondo o outro no plano cartesiano. Neste tutorial, vimos detecção para a borda de tela (colisão círculo x Reta alinhada ao um eixo) e para ver se a bola atingiu a raquete (colisão círculo x Retângulo).

A resposta de colisão é responsável pela física do jogo e, com algumas exceções, como quando usamos uma detecção de colisão como sensor, quando ocorre tal colisão, modifica algum estado do jogo. No nosso caso, ocorre quando a bola sai pela borda inferior, significando que o jogador perdeu o jogo.

No nosso próximo tutorial, vamos continuar o nosso jogo, onde definiremos a condição de vitória dele. E, para isso, vamos precisar rever tudo o que foi visto até agora. Então pessoal, até a próxima.

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