A jornada, não tão jornada, mas, ainda uma jornada.. você entendeu ;-)

Este espaço é destinado a diversos temas, relacionados a minhas atividades ou mesmo ao cotidiano.

Monday, October 29, 2007

Tutorial de XNA - Parte III

Seguindo a minha série de artigos sobre XNA, que já abordou a estrutura inicial de um jogo, desenho de sprites e moviementação, vou abordar agora um dos tópicos mais importantes dos jogos, colisões.

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:
No caso do retângulo azul, ele é determinado pelas coordenadas X1, X2, Y1 e Y2. Sendo as outras coordenadas pertencentes ao retângulo verde. Para testar a colisão basta verificar essas coordenadas. Portanto usaremos essas linhas para determinar se um retângulo colidiu com o outro.

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.
Obs.: tive de trocar os sinais de maior e menor porque o editor do blog tava "comendo" o texto.
Então, para testar se o objeto colidiu por baixo e pelo lado direito, precisamos apenas checar se as coordenadas dele atendem a essa condição. Mas, isso não seria suficiente para nós que queremos uma colisão mais genérica, então precisamos testar outros casos.

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;
Depois incluímos os vetores de posição para cada um:

//vetor para posição
Vector2 posicao_verde = Vector2.Zero;
Vector2 posicao_azul; //vai ficar parado
Preciso de um lugar para guardar o resultado os testes de colisão:

//objeto para guardar o resultado dos testes de colisao
public bool resultado_colid;

Além disso, para deixar um dos retângulos transparentes, eu preciso definir uma cor e determinar o Alpha dela, usando um objeto do tipo “color” que recebe 4 argumentos, os índices R (red), G (green), B (blue) e o A(alpha) que determina justamente os valores de transparência.

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

Para saber mais sobre isso, leia o artigo de André Furtado no SharpGames.

Agora, vamos ver como fica o código:

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.
A primeira é desenhar o boneco parado e em um outro sprite, desenhar o ataque.
Se você fizer isso, vai testar toda a área do sprite de ataque e isso inclui as áreas abaixo e acima da espada que não devem provocar colisão. Isso geraria falsos ataques e seu boneco seria “invencível” XD

Outra idéia, mais realista, é criar três sprites, um parado, outro de ataque e em separado, criar o sprite da espada que será desenhado na posição do braço do seu guerreiro. Quando for necessário testar a colisão, você testa apenas o sprite da espada e como a espada é pequena, a aproximação da colisão é boa.


Quando você for desenhar os objetos, mande desenhar a espada no local do braço e depois teste a colisão apenas na espada, repare que o sprite da espada sozinho ocupa quase toda a área da imagem (pontilhado vermelho), melhorando bastante a aproximação da colisão.

E por final, vale a pena ressaltar que esse é apenas um método de colisão e que geralmente não é o método mais usado. Para o caso do nosso "guerreiro" você poderia usar um outro método de "colisões por pixel", isso evitaria a necessidade de criação do sprite da espada.
Ufa!! já sabemos como criar a estrutura básica de um jogo me XNA, sabemos desenhar na tela, mover, usar transparência nos nossos sprites e por fim aprendemos um método de colisão. Com o que mostrei nas três partes desse tutorial, você já pode criar diversos joguinhos, espero que a leitura tenha valido a pena, por hoje é só e até o próximo tutorial ;-)

Labels: , , , ,

3 Comments:

  • At 7:08 AM , Blogger José Ferreira said...

    obrigado, brevemente estarei publicando mais uma parte do tutorial ;-)

     
  • At 7:58 AM , Blogger Unknown said...

    José preciso urgentemente falar com você.
    Se possível manda um e-mail com seu hotmail, ou seu telefone para: vin.ottoni@gmail.com

    - sou aluno do cefet, faço sistemas lá, e um dos lideres da célula de lá -

     
  • At 12:32 PM , Blogger Jullian Jesse said...

    Olá Jose.
    Sou Aluno de Sistemas de Informação da UFRPE - Serra Talhada.
    Morro em Recife e estudo aqui. E tenho interesse sobre a area de desenvolvimento de jogos sera que voce poderia esclarecer algumas duvidas sobre o XNA ?

    se possivel mande-me um email ou se possivel me adicione no MSN:
    canibal_1b@hotmail.com

    abraços.

     

Post a Comment

Subscribe to Post Comments [Atom]

<< Home