Tutorial de SDL – Parte 29 – Detecção de colisão circular

Continuando nossa série de artigos traduzido do site lazyfoo, neste artigo veremos como detectar colisões entre dois círculos e entre um circulo e uma caixa retangular; colisão entre círculos é, de fato, uma das forma mais comuns de colisão.

Verificar por uma colisão entre dois círculos é fácil. Tudo o que você precisa fazer é checar se a distância entre os centros dos círculos é menor do que a soma de seus raios.
Para a colisão entre um caixa e um círculo, você precisará achar o ponto da caixa de colisão que está mais próximo do centro do círculo. Se esse ponto estiver a uma distância menor do que o raio do círculo, há uma colisão.

//A circle stucture
struct Circle
{
 int x, y;
 int r;
};

O SDL possui uma estrutura de retângulo embutida, mas teremos que criar nossa próprio estrutura de círculo com sua posição e raio.

//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( SDL_Rect& square, Circle& circle );
 //Shows the dot on the screen
 void render();
 //Gets collision circle
 Circle& getCollider();
 private:
 //The X and Y offsets of the dot
 int mPosX, mPosY;
 //The velocity of the dot
 int mVelX, mVelY;
 //Dot's collision circle
 Circle mCollider;
 //Moves the collision circle relative to the dot's offset
 void shiftColliders();
};

Aqui temos a classe para o ponto do artigo de detecção de colisão que vimo antes, com algumas adições. O função de movimento recebe um círculo e um retângulo para verificar por colisões quando eles se movem. Também temos agora um círculo de colisão ao invés de uma caixa de colisão.

//Circle/Circle collision detector
bool checkCollision( Circle& a, Circle& b );
//Circle/Box collision detector
bool checkCollision( Circle& a, SDL_Rect& b );
//Calculates distance squared between two points
double distanceSquared( int x1, int y1, int x2, int y2 );

Para esse tutorial, teremos nossas funções de detecção de colisão entre dois círculos e entre um círculo e um retângulo. Também teremos uma função que calcula o quadrado da distância entre dois pontos.
Usar o quadrado da distância entre dois pontos ao invés da distância é uma otimização que iremos ver em detalhes mais tarde.

Dot::Dot( int x, int y )
{
 //Initialize the offsets
 mPosX = x;
 mPosY = y;
 //Set collision circle size
 mCollider.r = DOT_WIDTH / 2;
 //Initialize the velocity
 mVelX = 0;
 mVelY = 0;
 //Move collider relative to the circle
 shiftColliders();
}

O construtor recebe uma posição e inicializa as caixas/círculos de colisão e velocidade.

void Dot::move( SDL_Rect& square, Circle& circle )
{
 //Move the dot left or right
 mPosX += mVelX;
 shiftColliders();
 //If the dot collided or went too far to the left or right
 if( ( mPosX - mCollider.r < 0 ) || ( mPosX + mCollider.r > SCREEN_WIDTH ) || checkCollision( mCollider, square ) || checkCollision( mCollider, circle ) )
 {
 //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 - mCollider.r < 0 ) || ( mPosY + mCollider.r > SCREEN_HEIGHT ) || checkCollision( mCollider, square ) || checkCollision( mCollider, circle ) )
 {
 //Move back
 mPosY -= mVelY;
 shiftColliders();
 }
}

Como nos artigos de detecção de colisão anteriores, nós movemos o objeto pelo eixo x, checamos se houve colisão contra as bordas da tela, e depois contra os objetos da cena. Se o ponto atingir algo movemos ele para trás. Como sempre, o ponto move sua caixa de colisão junto com ele.
Depois fazemos isso novamente em relação ao eixo y.

void Dot::render()
{
 //Show the dot
 gDotTexture.render( mPosX - mCollider.r, mPosY - mCollider.r );
}

O código de renderização é uma pouco diferente. O SDL_Rects possui sua posição no canto superior esquerdo enquanto nosso círculo no centro. Isso significa que precisamos deslocar a posição da renderização para a parte superior esquerda do círculo pela subtração do raio da posição x e y.

bool checkCollision( Circle& a, Circle& b )
{
 //Calculate total radius squared
 int totalRadiusSquared = a.r + b.r;
 totalRadiusSquared = totalRadiusSquared * totalRadiusSquared;
 //If the distance between the centers of the circles is less than the sum of their radii
 if( distanceSquared( a.x, a.y, b.x, b.y ) < ( totalRadiusSquared ) )
 {
 //The circles have collided
 return true;
 }
 //If not
 return false;
}

Aqui temos o detector de colisão de nosso círculo. Ele simplesmente verifica se a distância ao quadrado entre os centros é menor do que a soma da raio ao quadrado. Se for, há uma colisão.
Por quê usamos a distância ao quadrado ao invés da distância? Porque para calcular a distância envolve uma operação de raiz quadrada e essa operação é relativamente custosa. Felizmente, se x>y, então x^2 > y^2, de forma que podemos evitar a operação de calcular a raiz quadrada somente comparando a distância ao quadrado.

bool checkCollision( Circle& a, SDL_Rect& b )
{
 //Closest point on collision box
 int cX, cY;
 //Find closest x offset
 if( a.x < b.x )
 {
 cX = b.x;
 }
 else if( a.x > b.x + b.w )
 {
 cX = b.x + b.w;
 }
 else
 {
 cX = a.x;
 }

Para verificar se uma caixa e um círculo colidiram,  precisamos encontrar o ponto mais próximo da caixa.
Se o centro do círculo está à esquerda da caixa, a posição x do ponto mais próximo está no lado esquerdo da caixa.

Se o centro do círculo está à direita da caixa, a posição x do ponto mais próximo está no lado direito da caixa.

Se o centro do círculo está dentro da caixa, a posição x do ponto mais próximo está na mesma posição x do círculo.

 //Find closest y offset
 if( a.y < b.y )
 {
 cY = b.y;
 }
 else if( a.y > b.y + b.h )
 {
 cY = b.y + b.h;
 }
 else
 {
 cY = a.y;
 }
 //If the closest point is inside the circle
 if( distanceSquared( a.x, a.y, cX, cY ) < a.r * a.r )
 {
 //This box and the circle have collided
 return true;
 }
 //If the shapes have not collided
 return false;
}

Aqui encontramos a posição y da mesma forma que fizemos com a posição y. Se a distância ao quadrado entre o ponto mais próximo da caixa e o centro do círculo é menor do que o radio do círculo ao quadrado, então houve uma colisão.

double distanceSquared( int x1, int y1, int x2, int y2 )
{
 int deltaX = x2 - x1;
 int deltaY = y2 - y1;
 return deltaX*deltaX + deltaY*deltaY;
}

Aqui temos a função de cálculo da distância ao quadrado, que pé somente um cálculo de distância (squareRoot(x^2 + y^2)) sem a raiz quadrada.

 //The dot that will be moving around on the screen
 Dot dot( Dot::DOT_WIDTH / 2, Dot::DOT_HEIGHT / 2 );
 Dot otherDot( SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4 );
 //Set the wall
 SDL_Rect wall;
 wall.x = 300;
 wall.y = 40;
 wall.w = 40;
 wall.h = 400;

Antes de entrarmos no loop principal definimos os objetos da cena.

 //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, otherDot.getCollider() );
 //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 dots
 dot.render();
 otherDot.render();
 //Update screen
 SDL_RenderPresent( gRenderer );
 }
Finalmente em nosso loop principal lidamos com a entrada, movemos os ponto com a detecção de colisão e renderizamos os objetos da cena na tela.
Baixe os arquivos de mídia e o código fonte desse artigo aqui.