Quando as Coisas se Tocam — Colisão e o Primeiro Jogo: Pong

Quando as Coisas se Tocam — Colisão e o Primeiro Jogo: Pong

Detectar que dois retângulos se tocam são quatro comparações; responder à colisão é onde moram os bugs. O artigo mostra o AABB, a resposta que faz a bola quicar sem grudar na raquete, e fecha com o Pong inteiro em funcionamento, do movimento das raquetes à pontuação.
Linguagem C++

12 min de leitura

No artigo Entidades de Verdade — Componentes e o Padrão ECS estruturamos entidades com o ECS, mas elas ainda atravessavam umas às outras como fantasmas. Um jogo ganha vida quando as coisas colidem — a bola bate na raquete, o jogador esbarra na parede. Hoje aprendemos detecção e resposta de colisão, e construímos nosso primeiro jogo completo: o Pong, o avô dos videogames, onde uma bola quica entre duas raquetes. É o momento em que tudo o que vimos converge num jogo jogável de verdade: o game loop (Um Novo Território), a janela RAII (O Primeiro Pixel), os componentes (Entidades de Verdade), o delta time, e a colisão de hoje. Ao fim, você terá um Pong funcional rodando na sua tela — dois jogadores, uma bola que quica, placar. O primeiro jogo real da sua vida, construído com o C++ que você dominou ao longo do ano.

Detecção de colisão: o retângulo envolvente (AABB)

A pergunta fundamental da colisão é: dois objetos estão se sobrepondo? A técnica mais simples e mais usada em jogos 2D é a AABB (Axis-Aligned Bounding Box) — tratar cada objeto como um retângulo alinhado aos eixos e verificar se os retângulos se sobrepõem. A lógica é elegante: dois retângulos se sobrepõem se, e somente se, eles se sobrepõem em ambos os eixos simultaneamente:

// Dois retângulos (x, y = canto superior esquerdo; w, h = tamanho).
// Colidem se há sobreposição no eixo X E no eixo Y.
bool colide(float ax, float ay, float aw, float ah,
            float bx, float by, float bw, float bh) {
    return ax < bx + bw &&    // borda esquerda de A antes da direita de B
           ax + aw > bx &&    // borda direita de A depois da esquerda de B
           ay < by + bh &&    // borda superior de A antes da inferior de B
           ay + ah > by;      // borda inferior de A depois da superior de B
}

As quatro condições, todas verdadeiras ao mesmo tempo, significam que os retângulos se interpenetram. Se qualquer uma falhar (por exemplo, A está totalmente à esquerda de B), não há colisão. A raylib, aliás, oferece isso pronto com CheckCollisionRecs, mas implementá-lo você mesmo — são quatro comparações — é entender o que acontece por baixo, e o mesmo raciocínio serve para qualquer biblioteca. A AABB é rápida (só comparações), simples, e suficiente para a esmagadora maioria dos jogos 2D. Para formas rotacionadas ou círculos há técnicas mais elaboradas, mas a AABB é o cavalo de batalha.

Resposta à colisão: o que fazer ao tocar

Detectar a colisão é metade do trabalho; a outra metade é responder a ela. A resposta depende do jogo: no Pong, quando a bola toca uma raquete, ela inverte a direção horizontal (quica). Quando toca o teto ou o chão, inverte a direção vertical. A resposta é simplesmente ajustar a velocidade ou a posição:

// Quando a bola bate na raquete, inverte a direção horizontal:
if (colide(bola.x, bola.y, bola.w, bola.h, raquete.x, raquete.y, raquete.w, raquete.h)) {
    bola.velocidade_x = -bola.velocidade_x;   // quica: inverte o sentido horizontal
}
// Quando a bola toca o topo ou o fundo da tela, inverte a direção vertical:
if (bola.y <= 0 || bola.y + bola.h >= altura_tela) {
    bola.velocidade_y = -bola.velocidade_y;   // quica no teto/chão
}

Inverter o sinal da velocidade faz o objeto voltar por onde veio — o quique. Esta é a "física" do Pong, e é deliciosamente simples: nenhuma equação complexa, só trocar sinais de velocidade no momento certo. É o padrão de resposta a colisão em sua forma mais pura, e a base de jogos muito mais elaborados.

Pong completo

Agora reunimos tudo num Pong jogável. Duas raquetes (uma com W/S, outra com setas), uma bola que quica, e o placar. Note como o game loop, os componentes, o delta time e a colisão convergem:

#include <raylib.h>

int main() {
    Janela janela(800, 600, "Pong");   // janela RAII do O Primeiro Pixel

    // Estado do jogo (componentes soltos, à moda simples).
    float raq1_y = 250, raq2_y = 250;              // posições verticais das raquetes
    const float raq_vel = 400.0f, raq_h = 100, raq_w = 15;
    float bola_x = 400, bola_y = 300;              // posição da bola
    float bola_vx = 300, bola_vy = 200;            // velocidade da bola
    const float bola_tam = 15;
    int placar1 = 0, placar2 = 0;

    while (!janela.deve_fechar()) {
        float dt = GetFrameTime();

        // --- INPUT + UPDATE: raquetes ---
        if (IsKeyDown(KEY_W) && raq1_y > 0)            raq1_y -= raq_vel * dt;
        if (IsKeyDown(KEY_S) && raq1_y < 600 - raq_h)  raq1_y += raq_vel * dt;
        if (IsKeyDown(KEY_UP) && raq2_y > 0)           raq2_y -= raq_vel * dt;
        if (IsKeyDown(KEY_DOWN) && raq2_y < 600-raq_h) raq2_y += raq_vel * dt;

        // --- UPDATE: bola ---
        bola_x += bola_vx * dt;
        bola_y += bola_vy * dt;

        // Colisão com teto e chão: inverte vertical.
        if (bola_y <= 0 || bola_y + bola_tam >= 600) bola_vy = -bola_vy;

        // Colisão com as raquetes: inverte horizontal.
        if (colide(bola_x, bola_y, bola_tam, bola_tam, 20, raq1_y, raq_w, raq_h))
            bola_vx = fabsf(bola_vx);                 // garante que vá para a direita
        if (colide(bola_x, bola_y, bola_tam, bola_tam, 765, raq2_y, raq_w, raq_h))
            bola_vx = -fabsf(bola_vx);                // garante que vá para a esquerda

        // Ponto: bola saiu por um lado. Marca e reinicia no centro.
        if (bola_x < 0)   { placar2++; bola_x = 400; bola_y = 300; bola_vx =  300; }
        if (bola_x > 800) { placar1++; bola_x = 400; bola_y = 300; bola_vx = -300; }

        // --- RENDER ---
        BeginDrawing();
        ClearBackground(BLACK);
        DrawRectangle(20, (int)raq1_y, (int)raq_w, (int)raq_h, WHITE);    // raquete 1
        DrawRectangle(765, (int)raq2_y, (int)raq_w, (int)raq_h, WHITE);   // raquete 2
        DrawRectangle((int)bola_x, (int)bola_y, (int)bola_tam, (int)bola_tam, WHITE);
        DrawText(TextFormat("%d", placar1), 300, 20, 40, WHITE);         // placar
        DrawText(TextFormat("%d", placar2), 480, 20, 40, WHITE);
        EndDrawing();
    }
    return 0;
}

Rode e jogue — dois jogadores, W/S contra as setas, a bola quica, o placar sobe. É um jogo de verdade, completo e jogável, e você o construiu do zero. Contemple a convergência: o game loop (Um Novo Território) estrutura tudo, a janela RAII (O Primeiro Pixel) gerencia a tela, o delta time torna o movimento consistente, a colisão AABB de hoje faz a bola quicar, e cada raquete respeita seus limites (a proteção de invariante do Projetando Abstrações). Um jogo é a soma de peças simples orquestradas pelo loop — e você agora domina essas peças. (Nota: mantive o estado em variáveis soltas para clareza; refatorá-lo para o ECS do Entidades de Verdade seria o passo natural num jogo maior, e o exercício 5 pede exatamente isso.)

A colisão AABB reduz o problema a quatro comparações: dois retângulos se tocam quando se sobrepõem nos dois eixos ao mesmo tempo. Detectar é a parte fácil; responder é onde moram os bugs. Inverter a velocidade ao encostar parece natural e produz a bola que gruda na raquete, invertendo a cada frame enquanto continuar sobreposta — forçar a direção, em vez de invertê-la, resolve isso de uma vez.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Compile e jogue o Pong desta aula (com raylib instalada). Descreva o comportamento da bola ao bater nas raquetes e nas bordas. O que acontece quando ela passa por uma raquete?

Ver resposta

✓ Resposta: Ao jogar, a bola se move em linha reta até tocar algo. Ao bater no teto ou no chão, ela inverte a direção vertical (quica para cima/baixo mantendo o sentido horizontal). Ao bater numa raquete, inverte a direção horizontal, voltando para o outro lado. Quando a bola passa por uma raquete (o jogador não a interceptou a tempo), ela sai pela lateral da tela; nesse momento, o placar do jogador adversário aumenta e a bola reinicia no centro, seguindo em direção a quem perdeu o ponto. O jogo continua indefinidamente, com o placar subindo a cada ponto — é o Pong clássico, funcional.

Exercício 2

Explique a lógica da função colide (AABB): por que as quatro condições, todas verdadeiras, significam sobreposição? Dê um exemplo de dois retângulos que falham em uma condição e, portanto, não colidem.

Ver resposta

✓ Resposta: A função colide verifica sobreposição em ambos os eixos porque dois retângulos alinhados aos eixos se interpenetram se, e somente se, seus intervalos se sobrepõem tanto na horizontal quanto na vertical. As quatro condições traduzem isso: ax < bx + bw (a esquerda de A está antes da direita de B) e ax + aw > bx (a direita de A está depois da esquerda de B) juntas garantem sobreposição horizontal; ay < by + bh e ay + ah > by juntas garantem sobreposição vertical. Todas as quatro verdadeiras = sobreposição nos dois eixos = os retângulos se tocam. Exemplo que falha: um retângulo A em (0,0) de tamanho 10×10 e um B em (50,0) de tamanho 10×10. A condição ax + aw > bx vira 0 + 10 > 5010 > 50falsa: A está totalmente à esquerda de B, sem sobreposição horizontal, logo não colidem — mesmo que se sobreponham verticalmente (ambos no y=0). Basta uma condição falhar para não haver colisão, o que é correto: se estão separados em qualquer eixo, não se tocam.

Exercício 3

No Pong, quando a bola bate numa raquete, usamos bola_vx = fabsf(bola_vx) (força ir para a direita) em vez de simplesmente bola_vx = -bola_vx. Explique por que isso evita um bug em que a bola poderia "grudar" na raquete.

Ver resposta

✓ Resposta: Usar bola_vx = fabsf(bola_vx) (que força a velocidade a ser positiva, ou seja, para a direita) na colisão com a raquete esquerda evita um bug de "grudar" que bola_vx = -bola_vx causaria. O problema: a detecção de colisão AABB dispara enquanto os retângulos se sobrepõem, o que dura vários frames (a bola penetra um pouco antes de sair). Com bola_vx = -bola_vx, no primeiro frame de colisão a velocidade inverte (a bola passa a se afastar), mas se no frame seguinte os retângulos ainda se sobrepõem (a bola ainda não saiu), a condição dispara de novo e a velocidade inverte outra vez — voltando a bola para dentro da raquete. Isso pode se repetir, fazendo a bola "vibrar" presa na raquete. Com bola_vx = fabsf(bola_vx), a velocidade é sempre forçada para a direita (para longe da raquete esquerda) enquanto houver colisão, independentemente de quantos frames a sobreposição dure — a bola sempre se afasta, nunca reinverte para dentro. É uma resposta idempotente: aplicá-la repetidamente dá o mesmo resultado seguro. (A raquete direita usa -fabsf, forçando para a esquerda, pela mesma razão.)

Exercício 4

Adicione ao Pong uma pequena aceleração: cada vez que a bola bate numa raquete, aumente levemente sua velocidade (por exemplo, multiplique bola_vx e bola_vy por 1.05). Explique o efeito no jogo e por que isso o torna mais interessante.

Ver resposta

✓ Resposta: Aceleração progressiva:

// Na colisão com a raquete, após inverter a direção:
bola_vx *= 1.05f;   // aumenta 5% a velocidade horizontal
bola_vy *= 1.05f;   // e vertical

O efeito: a cada rebatida, a bola fica um pouco mais rápida, então o ritmo do jogo acelera progressivamente ao longo de uma troca longa. Isso torna o jogo mais interessante porque cria tensão crescente: no início de um ponto a bola é lenta e fácil, mas conforme os jogadores a rebatem várias vezes, ela acelera até ficar difícil de acompanhar, aumentando o desafio e o drama — quem errar primeiro na velocidade alta perde o ponto. É um exemplo de como uma regra simples (acelerar 5% por rebatida) adiciona dinâmica e progressão de dificuldade, transformando um jogo estático numa disputa que esquenta. (Cuidado prático: convém limitar a velocidade máxima para a bola não ficar rápida demais a ponto de "atravessar" a raquete entre frames — um problema de tunneling que jogos reais tratam.)

Exercício 5

Refatore o estado da bola do Pong para usar o ECS do artigo Entidades de Verdade — Componentes e o Padrão ECS: crie uma entidade bola com componentes Posicao, Velocidade e Aparencia, e mostre como o sistema_movimento a atualizaria. Discuta a vantagem dessa refatoração num jogo com muitos objetos.

Ver resposta

✓ Resposta: Refatoração para ECS:

// A bola como entidade composta de componentes (Artigo Entidades de Verdade):
Entidade bola;
bola.posicao   = {400, 300};
bola.velocidade = {300, 200};
bola.aparencia = {15, 15, WHITE};

// O sistema_movimento a atualiza junto com todas as outras entidades:
void sistema_movimento(std::vector<Entidade>& entidades, float dt) {
    for (auto& e : entidades) {
        e.posicao.x += e.velocidade.dx * dt;
        e.posicao.y += e.velocidade.dy * dt;
    }
}
// No loop: sistema_movimento(entidades, dt) move a bola e tudo mais de uma vez.

A vantagem dessa refatoração aparece num jogo com muitos objetos: em vez de escrever código de movimento separado para a bola, para cada raquete, para cada projétil, etc. (variáveis soltas espalhadas, com lógica duplicada), você tem um único sistema_movimento que atualiza todas as entidades de uma vez, percorrendo o vetor. Adicionar um novo objeto móvel (um segundo projétil, um power-up que se move) é só criar mais uma entidade e adicioná-la ao vetor — o sistema já a move automaticamente, sem código novo. No Pong, com poucos objetos, o ganho é modesto; mas no Breakout (com dezenas de tijolos) ou num jogo com centenas de entidades, o ECS elimina a duplicação e mantém o código organizado e escalável. É a mesma lição de "estruture para crescer" que o Entidades de Verdade estabeleceu, agora aplicada a um jogo concreto: variáveis soltas servem para protótipos minúsculos, mas o ECS é o que sustenta jogos de verdade.

Comentários

Mais em Linguagem C++

Escreva uma Vez, Use com Todo Tipo — Templates de Função
Escreva uma Vez, Use com Todo Tipo — Templates de Função

Escrever a mesma função para int, double e string é o tipo de repetição que o…

Pare de Escrever Laços — a Biblioteca <algorithm>
Pare de Escrever Laços — a Biblioteca <algorithm>

Escrever o laço à mão conta como percorrer; chamar o algoritmo diz o que se…

O Conforto do C++ Moderno — Structured Bindings e Dedução de Tipos
O Conforto do C++ Moderno — Structured Bindings e Dedução de Tipos

Dar nome ao que vinha como .first e .second muda mais a leitura do que parece…