Tutorial de SDL – Parte 28 – Detecção de colisão por pixel

Continuando nossa série de artigos traduzidos do site lazyfoo, veremos que, quando sabemos detectar colisões entre dois retângulo, podemos detectar colisões entre qualquer par e imagens, já que todas as imagens são compostas de retângulos.

Uma vez que você saiba detectar colisão entre dois retângulos, pode verificar por colisões entre qualquer imagens desde que essas imagens sejam compostas de retângulos.

Tudo pode ser feito com retângulo em um jogo, mesmo esse ponto:

Não enxerga? Vamos aumentar o zoom:

Ainda não enxerga? Que tal agora:

Imagens são compostas por pixels, que são quadrados, que são retângulos. Para detecção de colisão por pixels, tudo que temos fazer é adicionar para cada objeto um conjunto de colliders de caixas e verificar por colisão em um conjunto de caixas contra outra dessa forma:

//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 = 1;

 //Initializes the variables
 Dot( int x, int y );

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

 //Moves the dot and checks collision
 void move( std::vector<SDL_Rect>& otherColliders );

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

 //Gets the collision boxes
 std::vector<SDL_Rect>& getColliders();

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

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

 //Dot's collision boxes
 std::vector<SDL_Rect> mColliders;

 //Moves the collision boxes relative to the dot's offset
 void shiftColliders();
};

Aqui temos nosso ponto com detecção de colisão por pixel. Sua velocidade é reduzida para 1 pixel por frame para que a colisão seja mais fácil de ser vista. A função do movimento agora recebe um vetor de caixas de colisão de forma que possamos checar colisão entre dois conjuntos. Como iremos ter dois pontos colidindo, precisaremos ser capazes de obter os colliders então adicionamos uma função para isso.

Ao invés de ter uma única caixa de colisão, teremos um vetor de colliders. Também teremos um função interna para alterá-los de forma que batam com a posição do ponto.
//Starts up SDL and creates window
bool init();

//Loads media
bool loadMedia();

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

//Box set collision detector
bool checkCollision( std::vector& a, std::vector& b );

Aqui temos nosso detector de colisões para checar por colisões entre conjuntos de caixas.

Dot::Dot( int x, int y )
{
 //Initialize the offsets
 mPosX = x;
 mPosY = y;

 //Create the necessary SDL_Rects
 mColliders.resize( 11 );

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

 //Initialize the collision boxes' width and height
 mColliders[ 0 ].w = 6;
 mColliders[ 0 ].h = 1;

 mColliders[ 1 ].w = 10;
 mColliders[ 1 ].h = 1;

 mColliders[ 2 ].w = 14;
 mColliders[ 2 ].h = 1;

 mColliders[ 3 ].w = 16;
 mColliders[ 3 ].h = 2;

 mColliders[ 4 ].w = 18;
 mColliders[ 4 ].h = 2;

 mColliders[ 5 ].w = 20;
 mColliders[ 5 ].h = 6;

 mColliders[ 6 ].w = 18;
 mColliders[ 6 ].h = 2;

 mColliders[ 7 ].w = 16;
 mColliders[ 7 ].h = 2;

 mColliders[ 8 ].w = 14;
 mColliders[ 8 ].h = 1;

 mColliders[ 9 ].w = 10;
 mColliders[ 9 ].h = 1;

 mColliders[ 10 ].w = 6;
 mColliders[ 10 ].h = 1;

 //Initialize colliders relative to position
 shiftColliders();
}

Da mesma forma que antes, temos que ajustar as dimensões da caixa de colisão no construtor. A única diferença é que aqui temos múltiplas caixas de colisão para configurar.

void Dot::move( std::vector<SDL_Rect>& otherColliders )
{
 //Move the dot left or right
 mPosX += mVelX;
 shiftColliders();

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

 //Move the dot up or down
 mPosY += mVelY;
 shiftColliders();

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

Essa função praticamente faz o mesmo que antes. Sempre que movermos os ponto, movemos a caixa de colisão com ele. Depois de movermos o ponto, checamos se ele foi para fora da ela ou atingiu algo. Se sim, movemos o ponto para trás e suas caixas de colisão com ele.

void Dot::shiftColliders()
{
 //The row offset
 int r = 0;

 //Go through the dot's collision boxes
 for( int set = 0; set < mColliders.size(); ++set )
 {
 //Center the collision box
 mColliders[ set ].x = mPosX + ( DOT_WIDTH - mColliders[ set ].w ) / 2;

 //Set the collision box at its row offset
 mColliders[ set ].y = mPosY + r;

 //Move the row offset down the height of the collision box
 r += mColliders[ set ].h;
 }
}

std::vector& Dot::getColliders()
{
 return mColliders;
}

Não se preocupe demais com como shiftColliders funciona. É um atalho para mColliders[ 0 ].x = …, mColliders[ 1 ].x = …, etc e funciona para esse caso específico. Para o seus próprios objetos, você precisará de suas próprias funções.

E depois de shiftCollider, tem uma função acessória para retornar as caixas de colisão.

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

 //Go through the A boxes
 for( int Abox = 0; Abox < a.size(); Abox++ )
 {
 //Calculate the sides of rect A
 leftA = a[ Abox ].x;
 rightA = a[ Abox ].x + a[ Abox ].w;
 topA = a[ Abox ].y;
 bottomA = a[ Abox ].y + a[ Abox ].h;

Aqui temos nossa função de detecção de colisão, onde temos um loop for que calcula a parte superior/inferior/esquerda/direita de cada caixa de colisão do objeto a.

 //Go through the B boxes
 for( int Bbox = 0; Bbox < b.size(); Bbox++ )
 {
 //Calculate the sides of rect B
 leftB = b[ Bbox ].x;
 rightB = b[ Bbox ].x + b[ Bbox ].w;
 topB = b[ Bbox ].y;
 bottomB = b[ Bbox ].y + b[ Bbox ].h;

 //If no sides from A are outside of B
 if( ( ( bottomA <= topB ) || ( topA >= bottomB ) || ( rightA <= leftB ) || ( leftA >= rightB ) ) == false )
 {
 //A collision is detected
 return true;
 }
 }
 }

 //If neither set of collision boxes touched
 return false;
}

Então nós fazemos os cálculos para cada caixa de colisão do objeto b. Em seguida, checamos se não existe separação de eixo. Se não houver, retornamos true. Se passarmos por todos os conjuntos sem um colisão, retornamos false.

 //Main loop flag
 bool quit = false;

 //Event handler
 SDL_Event e;

 //The dot that will be moving around on the screen
 Dot dot( 0, 0 );

 //The dot that will be collided against
 Dot otherDot( SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4 );

Antes de entrarmos no loop principal, declaramos nosso e um outro que iremos colidir contra ele.

 //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( otherDot.getColliders() );

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

 //Render dots
 dot.render();
 otherDot.render();

 //Update screen
 SDL_RenderPresent( gRenderer );
 }
Mais uma vez no loop principal iremos lidar com os eventos do ponto, movendo-o com checagem de colisões, e então finalmente renderizar nossos objetos.

Uma questão que se pergunta bastante é como criar uma função  que carrega uma imagem e gera automaticamente um conjunto de caixas de colisão para detecção por pixel. A resposta é simples:

Não faça isso.

Na maioria dos jogos, você não que 100% de precisão. Quando mais caixas de colisão você tem, mas colisões precisa checar e mais lente será. O que a maioria dos jogos fazem é detectar o mais próximo possível, como em Street Fighter:

Os resultados não são perfeitamente por pixel mas são próximos o suficiente.

Além dessa existe uma otimização que podemos fazer aqui. Podemos ter usado uma caixa para o ponto que encapsule todas outras caixas de colisão e então verificar primeiro essa caixa maior antes de verificar as caixas de colisão por pixel. Isso adiciona uma detecção de colisão a mais, mas como é muito mais provável que dois objetos não colidem, isso provavelmente irá evitar que tenhamos que fazer algumas detecções de colisão. Em jogos, isso é normalmente feito com uma estrutura de árvore que possui níveis diferentes de detalhes para permitir verificações antecipadas e evitar checagens desnecessárias no nível dos pixels. Como nos artigos anteriores, essas estruturas estão fora do escopo desse artigo também.

Baixe os arquivos de mídia e de código fonte para esse artigo aqui.