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(); };
//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 ); }
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:
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.