Tutorial de XNA - Parte III
Também aproveitei para incluir umas "coisinhas novas" no código dos exemplos anteiores, como transparência no sprite.
-------------------------------
Para o melhor entendimento deste material é necessário que o leitor tenha conhecimentos prévios de C# e de orientação a objetos e que tenha lido a primeira e a segunda parte do tutorial.
Até agora nós descobrimos como é a estrutura de um jogo no XNA, como movimentamos os nossos desenhos e como recebemos as teclas pressionadas no teclado. Nesta parte do tutorial eu vou falar sobre um tópico muito importante dentro da área de jogos, a colisão.
Antes de falar da colisão usando o XNA propriamente dito, vou explicar a teoria por traz desse método de colisão, ai sim, eu mostrarei um exemplo desse tipo de colisão usando o XNA.
Mas, o que é “colisão”?
Dentro dos jogos, você precisa muitas vezes testar se dois objetos estão colidindo, como no mundo real. Por exemplo, se um carro do seu jogo bateu na parede, se um dos jogadores foi atingido por uma bala, se a sua espada “ultra-mega-hiper-poderosa” atingiu o inimigo, ou simplesmente se sua bolinha atingiu o chão.
Mas, lembre, nos jogos 2D todos esses objetos são imagens e para testar se eles colidiram, você não pode testar apenas as suas coordenadas de desenho. Ai é que entram as técnicas de colisão.
Um ponto importante é pensar primeiro que para cada caso existe uma técnica mais adequada, então o que eu estou mostrando aqui, pode não ser adequado ao seu jogo simplesmente pelo modo como você o implementou, ou então porque torna jogo mais lento, geralmente não há uma regra, cada caso é um caso.
Bounding Box (caixa delimitadora)
A idéia é bastante simples, nós vamos associar a cada objeto da tela uma área retangular, dada pelos limites horizontais e verticais da sua imagem. Daí, testaremos se a intersecção desses limites, resultando ou não na colisão. Ai você diz: “hein??!!” e eu digo, “ta bom vou explicar melhor :-)”
Repare na imagem abaixo, onde temos dois retângulos desenhados na tela:
Cada um deles ocupa uma área da tela delimitada por suas linhas de contorno, que são dadas pelos limites horizontais e verticais, veja a imagem abaixo:
Vamos pegar o mesmo caso da figura anterior e estudar as coordenadas, na imagem anterior X1 e X2 são menores que X3 o que significa que o segundo retângulo está ao lado direito do primeiro. Caso X1 e X2 fossem maiores que X3 o retângulo verde estaria à esquerda do azul.
A mesma comparação pode ser feita com as coordenadas Y das imagens, como Y1 e Y2 são menores que Y3, então o retângulo azul está acima do verde, em caso contrário, estaria abaixo.
Mas, para nós, o mais importante é detectar a colisão e não a localização. Essa colisão acontece quando as linhas que determinam nossos retângulos formam uma área em comum aos dois retângulos. Por exemplo:
Nesse caso, a área amarela é formada pela intersecção dos dois retângulos e nesse caso temos certeza de que os dois objetos colidiram. No exemplo temos "X1 menor que X3 menor que X2 e Y1 menor que Y3 menor que Y2” o que indica uma colisão por baixo e do lado direito.
Bom, no meu caso vou criar um método muito simples que testa se existe uma intersecção entre as “caixas delimitadoras” formadas pelas linhas do exemplo anterior, ele vai informar apenas se os objetos colidem ou não, não me preocupei com a localização relativa dos objetos (acima, abaixo, etc).
Ai vai o código do método “testar_colisao” que retorna true em caso de colisão e false em caso contrário.
protected bool testar_colisao(Texture2D box1, Vector2
posi1, Texture2D box2, Vector2 posi2)
{
//por padrão os objetos não
colidem
bool status = false;
//coloque as posições em nomes mais
fáceis :-)
float x1 = posi1.X;
float x2 = posi1.X + box1.Width;
float x3 = posi2.X;
float x4 = posi2.X + box2.Width;
float
y1 = posi1.Y;
float y2 = posi1.Y +
box1.Height;
float y3 = posi2.Y;float y4 = posi2.Y + box2.Height;
//teste os
limites e veja se os objetos colidiram
if ((((x3 <= x1) && (x1 <= x4)) ((x3 <= x2) && (x2 <= x4))) && (((y3 <= y1) && (y1 <= y4)) ((y3 <= y2) && (y2 <= y4)))){
//achei uma colisão!!
status = true;
}
//devolva o resultado
return status;
}
Agora que já sabemos como testar a colisão, vamos ver um exemplo de código que usa esse método para testar a colisão. O nosso exemplo vai usar todo o código dos tutoriais anteiores, só que no lugar de uma bola, teremos os dois retângulos que usei nos exemplos deste tutorial para explicar como funciona a colisão. Repare que para achar as coordenadas eu usei as propriedade Width (largura) e Heigth (altura) das texturas.
O software é simples, temos dois retângulos, um fixo e outro que vai se mover de acordo com os as setas do teclado, quando esses dois retângulos colidirem, um deles ficará transparente.
Inclui duas texturas, uma para cada retângulo:
//texturas
Texture2D retanguloazul;
Texture2D retanguloverde;
//vetor para posição
Vector2 posicao_verde = Vector2.Zero;
Vector2 posicao_azul; //vai ficar parado
//objeto para guardar o resultado dos testes de colisao
public bool resultado_colid;
Esses valores variam de 0 a 255 e a mistura deles gera as cores no padrão RGB. Quando você aplica o valor 0 ao A, indica transparência total e 255 indica sem transparência.
//cor transparente
//branco com transparencia
Color my_color = new Color(255, 255, 255, 150);
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using
Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
//executando o jogo
namespace meu_jogo
{
class novo_jogo
{
static public void Main() //método principal da aplicação
{
jogo teste = new jogo();
teste.Run();
}
}
}
//o
código da classe jogo, herdado da classe pai "Game"
class jogo : Game
{
//permite a configuração do ambiente
GraphicsDeviceManager
config_am;
ContentManager recursos;
//texturas
Texture2D
retanguloazul;
Texture2D retanguloverde;
//sprite
SpriteBatch
s_azul;
SpriteBatch s_verde;
//vetor para posição
Vector2
posicao_verde = Vector2.Zero;
Vector2 posicao_azul; //vai ficar parado
//objeto para guardar o resultado dos testes de colisao
public bool
resultado_colid;
//branco com transparencia
Color my_color = new
Color(255, 255, 255, 150);
//construtor da classe
public jogo()
{
config_am = new GraphicsDeviceManager(this);
recursos = new
ContentManager(Services);
}
//inicializa outros itens que não
requerem a inicalização do dispositivo gráfico
protected override void
Initialize()
{
posicao_azul.X = 200;
posicao_azul.Y = 200;
base.Initialize();
}
//método para carregar a textura.
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
retanguloazul =
recursos.Load(@"imagens\rect_azul");
retanguloverde =
recursos.Load(@"imagens\rect_verde");
s_azul = new
SpriteBatch(config_am.GraphicsDevice);
s_verde = new
SpriteBatch(config_am.GraphicsDevice);
//preciso inicializar as texturas
antes de usar a colisão
resultado_colid = testar_colisao(retanguloazul,
posicao_azul, retanguloverde, posicao_verde);
}
}
protected
override void Update(GameTime gameTime)
{
//um objeto para guardar o
status do teclado
KeyboardState teclado = Keyboard.GetState();
//veja se
algum objeto colidiu
resultado_colid = testar_colisao(retanguloazul,
posicao_azul, retanguloverde, posicao_verde);
//testando a qual tecla
foi pressionada e
//incrementando a coordenada correta
if
(teclado.IsKeyDown(Keys.Right))
{
posicao_verde.X++; //mover para a
direita
}
else if (teclado.IsKeyDown(Keys.Left))
{
posicao_verde.X--; //mover para a esquerda
}
else if
(teclado.IsKeyDown(Keys.Down))
{
posicao_verde.Y++; //mover para baixo
}
else if (teclado.IsKeyDown(Keys.Up))
{
posicao_verde.Y--;
//mover para cima
}
else if (teclado.IsKeyDown(Keys.Escape))
{
this.Exit(); //sair da aplicação
}
}
protected
override void Draw(GameTime gameTime)
{
config_am.GraphicsDevice.Clear(Color.Black);
//desenhe o retangulo
azul em uma posicão fixa e sempre com a mesma cor.
s_azul.Begin();
s_azul.Draw(retanguloazul, posicao_azul, Color.White);
s_azul.End();
if (resultado_colid) //se colidiu, desenhe com transparencia
{
s_azul.Begin();
s_azul.Draw(retanguloverde, posicao_verde, my_color);
s_azul.End();
}
else //em caso contrário desenhe normal
{
s_azul.Begin();
s_azul.Draw(retanguloverde, posicao_verde, Color.White);
s_azul.End();
}
}
//testa a colisao entre dois objetos
//recebe como argumentos as texturas e os vetores de posições de cada objeto
protected bool testar_colisao(Texture2D box1, Vector2 posi1, Texture2D box2,
Vector2 posi2)
{
//por padrão os objetos não colidem
bool status =
false;
//coloque as posições em nomes mais fáceis :-)
float x1 =
posi1.X;
float x2 = posi1.X + box1.Width;
float x3 = posi2.X;
float x4 = posi2.X + box2.Width;
float y1 = posi1.Y;
float y2 =
posi1.Y + box1.Height;
float y3 = posi2.Y;
float y4 = posi2.Y +
box2.Height;
//teste os limites e veja se os objetos colidiram
if
((((x3 <= x1) && (x1 <= x4)) ((x3 <= x2) && (x2 <= x4))) && (((y3 <= y1) && (y1 <= y4)) ((y3 <= y2) && (y2 <= y4)))) { //achei uma colisão!! status = true;
}
return status; //devolva o resultado
}
}
Agora vamos aos pontos fortes e fracos desse método de colisão, primeiro os pontos fortes:
- Fácil de implementar para objetos que estejam alinhados aos eixos X e Y.
- Tem bom desempenho para objetos alinhados com os eixos, se comparado a outros métodos.
Depois os pontos fracos:
- Não atende a maioria dos casos, visto que os objetos nem sempre são retangulares.
- Em objetos de formas não retangulares, provoca “falsas colisões” visto que as texturas nem sempre preenchem toda a área das “caixas delimitadoras”.
Um exemplo simples, onde as texturas provocariam “falsas” colisões, seria o mesmo código, usando “círculos no lugar de retângulos”.
Agora vou dar umas dicas de utilização desse método nos seus jogos:
- Se for usar esse método, desenhe os objetos de forma que eles ocupem o máximo da área da imagem, isso vai reduzir os espaços em branco na imagem e por tabela reduz os erros nas colisões.
- Divida as imagens em imagens menores e anime via software, na hora de testar a colisão, você testa as partes individualmente, isso melhora a aproximação e diminui muito o erro nas colisões por “bounding box”. Por exemplo, se você tem um personagem do tipo medieval, um guerreiro com uma espada e quer testar a colisão do sprite de ataque do guerreiro com um monstro, você tem duas opções, veja o exemplo abaixo.