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