Tutorial de SDl – Parte 27 – Detecção de colisão

Continuando nossa série de artigo traduzidos do site lazyfoo, agora veremos como detectar colisões entre dois objetos desenhados na tela.

Em jogos, você frequentemente precisa saber se dois objetos tocaram um no outro. Para jogos simples, isso é normalmente feito com detecção de colisão dos limites de caixas.

Colisão entre caixas é uma maneira padrão de verificar pela colisão entre dois objetos. Dois polígonos estão colidindo quando eles não estão separados.

A seguir temos duas caixas que não estão colidindo. Como você pode ver, suas projeções x estão na parte inferior e suas projeções y no lado esquerdo:

A seguir você pode ver duas caixas que colidiram no eixo y mas estão separadas no eixo x:
Agora temos duas caixas que colidiram no eixo x mas estão separadas no eixo y:
Quando não há separação em nenhuma eixo temos uma colisão:
Essa forma de detecção de colisão é chamada de teste de separação de eixos onde testamos para descobrir onde os objetos estão separados. Se não há separação d eixos, então os objetos se colidiram.
//The dot that will move around on the screen
class Dot
{
 public:
 //The dimensions of the dot
 static const int DOT_WIDTH = 20;
 static const int DOT_HEIGHT = 20;

 //Maximum axis velocity of the dot
 static const int DOT_VEL = 10;

 //Initializes the variables
 Dot();

 //Takes key presses and adjusts the dot's velocity
 void handleEvent( SDL_Event& e );

 //Moves the dot and checks collision
 void move( SDL_Rect& wall );

 //Shows the dot on the screen
 void render();

 private:
 //The X and Y offsets of the dot
 int mPosX, mPosY;

 //The velocity of the dot
 int mVelX, mVelY;

 //Dot's collision box
 SDL_Rect mCollider;
};

Aqui temos o ponto do tutorial de movimento com alguns recursos novos. A função de movimento pega um retângulo que é a caixa de colisão para a parede e o ponto tem uma variável membro chamada mCollider que representa a caixa de colisão.

//Starts up SDL and creates window
bool init();

//Loads media
bool loadMedia();

//Frees media and shuts down SDL
void close();

//Box collision detector
bool checkCollision( SDL_Rect a, SDL_Rect b );

Também declaramos uma função para checar se duas caixas colidiram entre si.

Dot::Dot()
{
 //Initialize the offsets
 mPosX = 0;
 mPosY = 0;

 //Set collision box dimension
 mCollider.w = DOT_WIDTH;
 mCollider.h = DOT_HEIGHT;

 //Initialize the velocity
 mVelX = 0;
 mVelY = 0;
}

O construtor deve certificar que as dimensões de mCollider foram ajustadas.

void Dot::move( SDL_Rect& wall )
{
 //Move the dot left or right
 mPosX += mVelX;
 mCollider.x = mPosX;

 //If the dot collided or went too far to the left or right
 if( ( mPosX < 0 ) || ( mPosX + DOT_WIDTH > SCREEN_WIDTH ) || checkCollision( mCollider, wall ) )
 {
 //Move back
 mPosX -= mVelX;
 mCollider.x = mPosX;
 }

 //Move the dot up or down
 mPosY += mVelY;
 mCollider.y = mPosY;

 //If the dot collided or went too far up or down
 if( ( mPosY < 0 ) || ( mPosY + DOT_HEIGHT > SCREEN_HEIGHT ) || checkCollision( mCollider, wall ) )
 {
 //Move back
 mPosY -= mVelY;
 mCollider.y = mPosY;
 }
}

Aqui temos a nova função de movimento que agora verifica se atingimos a parede. Ela funciona de forma parecida com a versão anterior, apenas que agora faz com que o ponto mova-se para trás se for para fora tela ou atingir a parede.

Primeiro movemos o ponto pelo eixo x, mas também temos que alterar a posição do mCollider. Onde quer que seja que alteremos a posição do ponto, a posição de mCollider tem que ser alterada também. Em seguida verificamos se o ponto foi para fora da tela. Caso afirmativo, movemos o ponto para trás no eixo x. Finalmente, fazemos isso novamente para o movimento no eixo y.

bool checkCollision( SDL_Rect a, SDL_Rect b )
{
 //The sides of the rectangles
 int leftA, leftB;
 int rightA, rightB;
 int topA, topB;
 int bottomA, bottomB;

 //Calculate the sides of rect A
 leftA = a.x;
 rightA = a.x + a.w;
 topA = a.y;
 bottomA = a.y + a.h;

 //Calculate the sides of rect B
 leftB = b.x;
 rightB = b.x + b.w;
 topB = b.y;
 bottomB = b.y + b.h;

Aqui é onde a detecção da colisão acontece. Esse código calcula a projeção superior/inferior e esquerda/direita de cada caixa de colisão.

 //If any of the sides from A are outside of B
 if( bottomA <= topB )
 {
 return false;
 }

 if( topA >= bottomB )
 {
 return false;
 }

 if( rightA <= leftB )
 {
 return false;
 }

 if( leftA >= rightB )
 {
 return false;
 }

 //If none of the sides from A are outside B
 return true;
}

Aqui é onde realizamos nosso teste de separação de eixo x. Primeiro checamos as partes superior e inferior das caixas para verificar se elas estão separadas no eixo y. Em seguida checamos as parte esquerda e direita das caixas para verificar se elas estão separadas pelo eixo x. Se houver alguma separação, então não há colisão e retornamos false. Se não pudermos descobrir qualquer separação, então existe uma colisão e retornamos true.

Observação: o SDL tem algumas funções embutidas de detecção de colisão, mas nesse artigo  estaremos criando nosso própria. Principalmente porque é importante saber como isso é feito e depois porque se você puder criar a sua própria função de detecção de colisão pode usa-la com a renderização do SDL, Direct3D, Mantle, Metal ou qualquer outra API de renderização.

 //Main loop flag
 bool quit = false;

 //Event handler
 SDL_Event e;

 //The dot that will be moving around on the screen
 Dot dot;

 //Set the wall
 SDL_Rect wall;
 wall.x = 300;
 wall.y = 40;
 wall.w = 40;
 wall.h = 400;

Antes de entrarmos no loop principal, declaramos o ponto e definimos a posição de dimensão da parede.

 //While application is running
 while( !quit )
 {
 //Handle events on queue
 while( SDL_PollEvent( &e ) != 0 )
 {
 //User requests quit
 if( e.type == SDL_QUIT )
 {
 quit = true;
 }

 //Handle input for the dot
 dot.handleEvent( e );
 }

 //Move the dot and check collision
 dot.move( wall );

 //Clear screen
 SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
 SDL_RenderClear( gRenderer );

 //Render wall
 SDL_SetRenderDrawColor( gRenderer, 0x00, 0x00, 0x00, 0xFF );
 SDL_RenderDrawRect( gRenderer, &wall );

 //Render dot
 dot.render();

 //Update screen
 SDL_RenderPresent( gRenderer );
 }
Aqui temos no nosso loop principal com os manipuladores de evento do ponto, movendo-o enquanto verifica por colisões contra a parede e finalmente renderizando a parede e o ponto na tela.

As próximas duas seções são para referência futura. As chances são de que se você estiver lendo esse tutorial, é um iniciante e isso é muito avançado. Isso será mais útil mais à frente quando você precisar de uma detecção de colisões mais avançada.

Nesse momento inicial, quando você quer criar algo simples como tetris, esse tipo de detecção de colisão é ok. Para algo mais como um simulador de física para coisas, isso fica muito mais complicado.

Para algo como um simulador de um corpo rígido, temos que criar nossa lógica para fazer essas tarefas a cadas frame:

  1. Aplicar todas as forças em todos os objetos na cena (gravidade, vento, propulsão, etc)
  2. Mover os objetos pela aplicação da aceleração e velocidade à sua posição.
  3. Verificar por colisões para todos os objetos e criar um conjunto de contatos. Uma contato é uma estrutura de dados que tipicamente contem ponteiros para dois objetos que colidiram, um vetor normal do primeiro para o segundo objeto, e a quantidade que os objetos estão penetrando.
  4. Pegue o conjunto de contatos gerado e resolva as colisões. Isso tipicamente envolve verificar os contatos novamente (dentro de um limite) e resolvê-los.

Se você mal aprendeu sobre colisão de detecções, isso tudo está fora de seu alcance. Isso poderia usar todos um conjunto de tutoriais para ser explicado. Não apenas isso, mas envolve matemática de vetores e física que estão além do escopo desse artigo. Apenas tenha em mente que mais tarde quando você precisar programar jogos que tenham grandes quantidades de objetos colidindo e esteja se perguntando como toda a estrutura para um engine de física funciona.

Uma outra coisa: as caixas que temos aqui são AABB ou caixas alinhas pelo eixo. Isso significa que elas tem lados que estão alinhados com os eixos x e y. Se quiser ter caixas que sejam rotacionadas, pode usar o teste de separação de eixos em OBB (oriented bounding boxes, ou caixas de limite orientados). Ao invés de projetar os cantos nos eixos x e y, você projeta todos os cantos das caixas nos eixos I e J para cada caixa. Você então verifica se as caixas estão separadas ao longo de cada eixo. Você pode estender esse conceito para qualquer tipo de polígono projetando todos os cantos de cada eixo em cada eixo do polígono para verificar se há alguma separação. Isso tudo envolve matemática de vetores e como foi mencionado antes está fora do escopo desse artigo.
Baixe os arquivos de mídia e de código fonte para esse artigo aqui.