O Capstone dos Jogos — Breakout e o Fim da Jornada

O Capstone dos Jogos — Breakout e o Fim da Jornada

O último jogo reúne tudo o que a trilha construiu: recursos em RAII, entidades compostas, colisão AABB, máquina de estados e movimento por delta time. Nenhuma dessas peças é específica de jogos — são as ideias das fases anteriores num domínio onde errar aparece na tela imediatamente.
Linguagem C++

16 min de leitura

Chegamos ao fim de tudo. No artigo Menu, Jogo, Fim — Estados de Jogo e o Snake Completo completamos o Snake e dominamos os estados de jogo; agora construímos o terceiro e mais completo jogo — o Breakout, o quebra-tijolos —, o capstone que integra cada peça da trilha: a raquete e a bola vêm do Pong (Quando as Coisas se Tocam), as dezenas de tijolos se organizam com o ECS (Entidades de Verdade), a colisão AABB (Quando as Coisas se Tocam) faz tudo interagir, os estados de jogo (Menu, Jogo, Fim) envolvem as telas, e o RAII (O Primeiro Pixel, Imagem e Som) gerencia os recursos. É a coroação da trilha de jogos e, mais que isso, o ponto final de um ano inteiro de C++. Depois do Breakout, faremos a retrospectiva desta segunda extensão e, então, o balanço da jornada completa — das nove fases ao capstone, das duas trilhas-extensão a este adeus. Vamos construir um último jogo, e fechar o ciclo.

A anatomia do Breakout

O Breakout reúne, num só jogo, tudo o que construímos separadamente. Há uma raquete que o jogador move na horizontal (como no Pong, mas só num eixo). Há uma bola que quica pelas paredes, pela raquete e pelos tijolos (a física de colisão do Quando as Coisas se Tocam). E há uma parede de tijolos — dezenas deles, dispostos em linhas —, cada um destruído quando a bola o atinge. O jogador tem vidas (perde uma quando a bola cai) e um placar (sobe a cada tijolo quebrado). O objetivo: destruir todos os tijolos sem perder todas as vidas. Modelamos os tijolos como entidades do ECS (Entidades de Verdade) — muitos objetos do mesmo tipo, o cenário perfeito para o padrão:

#include <raylib.h>
#include <vector>

// Um tijolo é uma entidade simples: posição, tamanho, cor e se está vivo.
struct Tijolo {
    float x, y, largura, altura;
    Color cor;
    bool vivo;   // false quando destruído
};

// Cria a parede de tijolos: várias linhas e colunas.
std::vector<Tijolo> cria_tijolos() {
    std::vector<Tijolo> tijolos;
    const int linhas = 6, colunas = 10;
    const float lt = 70, at = 25, esp = 5;   // largura, altura, espaçamento
    const Color cores[] = { RED, ORANGE, YELLOW, GREEN, SKYBLUE, PURPLE };

    for (int lin = 0; lin < linhas; ++lin)
        for (int col = 0; col < colunas; ++col)
            tijolos.push_back({
                col * (lt + esp) + 35,   // x, com margem
                lin * (at + esp) + 50,   // y
                lt, at,
                cores[lin],              // cor por linha
                true                     // começa vivo
            });
    return tijolos;   // 60 tijolos, gerados por composição (Fase 4 + ECS)
}

Sessenta tijolos, gerados por dois laços e guardados num std::vector (Fase 4) — o ECS do Entidades de Verdade em ação: cada tijolo é uma entidade composta de dados (posição, cor, estado), sem hierarquia. Adicionar um tijolo especial (que resiste a dois toques, digamos) seria só mais um campo no struct, não uma nova classe. A parede inteira é uma coleção uniforme que os sistemas de colisão e render percorrem.

A colisão da bola com os tijolos

O coração do Breakout é a bola atingindo os tijolos. A cada frame, verificamos a colisão AABB (Quando as Coisas se Tocam) entre a bola e cada tijolo vivo; ao acertar, o tijolo "morre", a bola quica, e o placar sobe:

// Verifica colisão da bola com cada tijolo vivo; destrói e quica.
void colisao_tijolos(float bx, float by, float btam,
                     float& bvy, std::vector<Tijolo>& tijolos, int& placar) {
    for (auto& t : tijolos) {
        if (!t.vivo) continue;                        // ignora os já destruídos
        if (colide(bx, by, btam, btam, t.x, t.y, t.largura, t.altura)) {
            t.vivo = false;                            // destrói o tijolo
            bvy = -bvy;                                // a bola quica na vertical
            placar += 10;                              // pontos
            break;   // trata um tijolo por frame (evita quicar múltiplo)
        }
    }
}

Percorremos os tijolos (a STL da Fase 4), pulando os mortos, e ao detectar colisão com um vivo, marcamos vivo = false, invertemos a velocidade vertical da bola (o quique do Quando as Coisas se Tocam), e somamos pontos. O break trata só um tijolo por frame, evitando que a bola quique de forma errática ao tocar dois de uma vez. É a colisão AABB do Pong, agora aplicada a dezenas de alvos — a mesma técnica, escalada. Os tijolos destruídos permanecem no vetor (apenas marcados como mortos), o que é simples e eficiente; alternativamente, poderíamos removê-los com o idioma erase-remove do artigo Pare de Escrever Laços — a Biblioteca <algorithm>.

O Breakout completo

Reunindo tudo — raquete, bola, tijolos, vidas, placar e estados —, eis o jogo inteiro, a integração final de toda a trilha:

#include <raylib.h>
#include <vector>

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

    Estado estado = Estado::Menu;           // máquina de estados (Menu, Jogo, Fim)
    std::vector<Tijolo> tijolos;
    float raq_x = 350, bola_x = 400, bola_y = 400, bola_vx = 250, bola_vy = -250;
    const float raq_l = 100, raq_a = 20, raq_y = 550, bola_tam = 12;
    int placar = 0, vidas = 3;

    auto reinicia = [&]() {
        tijolos = cria_tijolos();
        raq_x = 350; bola_x = 400; bola_y = 400; bola_vx = 250; bola_vy = -250;
        placar = 0; vidas = 3;
    };

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

        if (estado == Estado::Menu) {
            if (IsKeyPressed(KEY_ENTER)) { reinicia(); estado = Estado::Jogando; }
        }
        else if (estado == Estado::Jogando) {
            // --- Raquete: move na horizontal com as setas ---
            if (IsKeyDown(KEY_LEFT)  && raq_x > 0)          raq_x -= 500 * dt;
            if (IsKeyDown(KEY_RIGHT) && raq_x < 800 - raq_l) raq_x += 500 * dt;

            // --- Bola: move ---
            bola_x += bola_vx * dt;
            bola_y += bola_vy * dt;

            // Quique nas paredes laterais e no teto:
            if (bola_x <= 0 || bola_x + bola_tam >= 800) bola_vx = -bola_vx;
            if (bola_y <= 0) bola_vy = -bola_vy;

            // Quique na raquete:
            if (colide(bola_x, bola_y, bola_tam, bola_tam, raq_x, raq_y, raq_l, raq_a))
                bola_vy = -fabsf(bola_vy);   // sempre para cima (evita grudar, Quando as Coisas se Tocam)

            // Colisão com os tijolos:
            colisao_tijolos(bola_x, bola_y, bola_tam, bola_vy, tijolos, placar);

            // Bola caiu: perde uma vida.
            if (bola_y > 600) {
                vidas--;
                bola_x = 400; bola_y = 400; bola_vx = 250; bola_vy = -250;
                if (vidas <= 0) estado = Estado::GameOver;
            }

            // Vitória: todos os tijolos destruídos.
            bool venceu = true;
            for (const auto& t : tijolos) if (t.vivo) { venceu = false; break; }
            if (venceu) estado = Estado::GameOver;
        }
        else if (estado == Estado::GameOver) {
            if (IsKeyPressed(KEY_ENTER)) estado = Estado::Menu;
        }

        // --- RENDER por estado ---
        BeginDrawing();
        ClearBackground(BLACK);
        if (estado == Estado::Menu) {
            DrawText("BREAKOUT", 260, 200, 60, WHITE);
            DrawText("Enter para jogar", 290, 320, 20, GRAY);
        }
        else if (estado == Estado::Jogando) {
            for (const auto& t : tijolos)               // desenha tijolos vivos
                if (t.vivo)
                    DrawRectangle((int)t.x, (int)t.y, (int)t.largura, (int)t.altura, t.cor);
            DrawRectangle((int)raq_x, (int)raq_y, (int)raq_l, (int)raq_a, WHITE);   // raquete
            DrawRectangle((int)bola_x, (int)bola_y, (int)bola_tam, (int)bola_tam, WHITE); // bola
            DrawText(TextFormat("Placar: %d", placar), 10, 10, 20, WHITE);
            DrawText(TextFormat("Vidas: %d", vidas), 680, 10, 20, WHITE);
        }
        else if (estado == Estado::GameOver) {
            DrawText("FIM", 350, 220, 50, WHITE);
            DrawText(TextFormat("Placar final: %d", placar), 290, 300, 25, WHITE);
            DrawText("Enter para o menu", 280, 360, 20, GRAY);
        }
        EndDrawing();
    }
    return 0;
}

Rode e jogue — mova a raquete, rebata a bola, destrua os sessenta tijolos coloridos, cuide das suas três vidas. É o jogo mais completo da trilha, e ele é a soma de tudo: o game loop (Um Novo Território) orquestra; a janela RAII (O Primeiro Pixel) gerencia a tela; a raquete e a bola reúnem o movimento e a colisão do Pong (Quando as Coisas se Tocam); os tijolos são entidades do ECS (Entidades de Verdade) num std::vector (Fase 4); a máquina de estados (Menu, Jogo, Fim) governa menu, jogo e fim; o delta time torna tudo consistente; e a lambda reinicia (Fase 5) prepara cada partida. Da janela vazia do O Primeiro Pixel a este quebra-tijolos completo, a trilha inteira convergiu num jogo — assim como o mini banco de dados foi a convergência do curso principal. Você construiu três jogos, do zero, com o C++ que dominou.

O Breakout reúne o que a trilha construiu: janela e recursos em RAII, entidades compostas, colisão AABB, máquina de estados e movimento por delta time. Nenhuma dessas peças é específica de jogos — são as mesmas ideias das fases anteriores, encontrando um domínio em que a consequência de errar aparece na tela, imediatamente.

Três jogos depois, o que fica não é a raylib, que pode ser trocada por outra biblioteca sem alterar o raciocínio. É a arquitetura: separar dados de comportamento, dar dono a cada recurso, tornar estados impossíveis irrepresentáveis e manter a simulação independente da máquina onde roda.

A palavra final

Um ano. Cinquenta e nove artigos. Nove fases, um capstone, e duas trilhas-extensão que levaram o C++ a bancos de dados, à web e aos jogos. Você começou como um programador C, curioso e talvez intimidado, e termina dominando C++ moderno em toda a sua amplitude — capaz de projetar sistemas, persistir dados, servir na web, e criar jogos, tudo com a segurança do RAII, a expressividade da STL, a clareza dos tipos modernos, e a honestidade sobre custos e trade-offs que buscamos cultivar em cada aula. O caminho foi longo, mas foi de degraus firmes, cada um apoiado no anterior, cada promessa plantada e paga. E se o C++ deixou de ser uma montanha intimidante para virar uma ferramenta poderosa a serviço das suas ideias — em sistemas, na web ou em jogos —, então esta jornada inteira cumpriu tudo o que prometeu no primeiro "Olá, C++!". Obrigado por percorrê-la até o último pixel. O domínio que você construiu é seu, para sempre. Agora vá construir mundos — de dados, de páginas, ou de jogos. A jornada de aprender terminou; a de criar, essa não tem fim. Boa sorte, e divirta-se — afinal, você acabou de aprender a fazer jogos.

Fontes e leituras recomendadas

  • raylib.com e seus exemplos: continue com a biblioteca que usamos; há exemplos de Breakout, plataformas, e muito mais para estudar e expandir.
  • Robert Nystrom, Game Programming Patterns (gameprogrammingpatterns.com): releia-o agora, do início — os padrões farão pleno sentido com os três jogos que você construiu.
  • "Handmade Hero" (handmadehero.org): uma série que constrói um jogo completo em C/C++ do absoluto zero, para quem quer ir fundo.
  • learnopengl.com: se os gráficos o fascinaram, o caminho para o 3D e o controle total da renderização começa aqui.
  • A comunidade de game dev (r/gamedev, itch.io, game jams): construa, publique, participe — é assim que game developers nascem.

Exercícios

Exercício 1

Compile e jogue o Breakout completo. Descreva a experiência: mover a raquete, rebater a bola, destruir tijolos, perder vidas. O que acontece ao destruir todos os tijolos, e ao perder as três vidas?

Ver resposta

✓ Resposta: Ao jogar, você move a raquete branca na base da tela com as setas esquerda/direita, rebatendo a bola para cima. A bola quica nas paredes laterais, no teto e na raquete, e ao atingir um tijolo, o destrói (o tijolo some), quica de volta, e o placar sobe 10 pontos. Se a bola passar pela raquete e cair pela base, você perde uma vida (das três iniciais), e a bola reinicia. Ao destruir todos os sessenta tijolos, o jogo vai para a tela de fim (vitória) mostrando o placar final. Ao perder as três vidas, o jogo também vai para a tela de fim (derrota). Em ambos os casos, Enter volta ao menu para jogar de novo. A progressão de cores dos tijolos (vermelho no topo, roxo embaixo) dá um visual agradável, e o desafio está em não deixar a bola cair enquanto se limpa a parede.

Exercício 2

Identifique, no código do Breakout, um lugar concreto onde cada uma das seguintes peças aparece: (a) RAII; (b) o ECS/composição; (c) colisão AABB; (d) a máquina de estados; (e) delta time. Para cada uma, cite o artigo (da trilha ou do curso) que a ensinou.

Ver resposta

✓ Resposta: As peças no Breakout: (a) RAII — na criação Janela janela(800, 600, "Breakout"), que abre a janela no construtor e a fecha no destrutor (ensinado no O Primeiro Pixel, com raiz na Fase 2). (b) ECS/composição — os tijolos como std::vector<Tijolo>, cada Tijolo uma entidade composta de dados (posição, cor, vivo) sem herança (ensinado no Entidades de Verdade, com raiz na Fase 3). (c) Colisão AABB — nas chamadas a colide(...) entre a bola e a raquete, e na colisao_tijolos entre a bola e cada tijolo (ensinado no Quando as Coisas se Tocam). (d) Máquina de estados — na variável Estado estado e nos ramos if (estado == ...) que tratam Menu, Jogando e GameOver (ensinado no Menu, Jogo, Fim, com o enum class da Fase 1). (e) Delta time — em todo movimento multiplicado por dt, como bola_x += bola_vx * dt e raq_x -= 500 * dt (ensinado no Um Novo Território). Cada peça vem de um artigo específico, e juntas mostram a integração completa da trilha e do curso.

Exercício 3

Adicione ao Breakout um efeito de "quique melhorado": faça o ângulo de rebatida da bola na raquete depender de onde ela bate (mais para a borda = ângulo mais acentuado). Descreva conceitualmente como você calcularia a nova velocidade horizontal com base na posição do impacto relativa ao centro da raquete.

Ver resposta

✓ Resposta: Para o quique melhorado, a ideia é que a bola não apenas inverta a direção vertical ao bater na raquete, mas ganhe uma velocidade horizontal proporcional a quão longe do centro da raquete ela bateu — batendo no centro, sobe reto; batendo perto das bordas, sai em ângulo. Conceitualmente: calcula-se a posição do impacto relativa ao centro da raquete, normalizada para o intervalo [-1, +1]. Se o centro da bola está no centro exato da raquete, o valor é 0; na borda esquerda, -1; na direita, +1. A fórmula seria algo como float rel = (bola_centro_x - (raq_x + raq_l/2)) / (raq_l/2); — isso dá o quão descentrado foi o impacto, de -1 a +1. Então define-se a nova velocidade horizontal como esse valor vezes uma velocidade máxima: bola_vx = rel * VELOCIDADE_MAX;, e mantém-se a bola indo para cima (bola_vy = -fabsf(bola_vy)), possivelmente ajustando a magnitude total para manter a velocidade constante. O efeito: bater com a pontinha da raquete manda a bola em diagonal acentuada; bater no meio, sobe quase reto. Isso dá ao jogador controle sobre a direção da bola — uma mecânica clássica do Breakout que transforma a raquete de um simples anteparo numa ferramenta de mira, aumentando muito a profundidade do jogo. (Usa a matemática de vetores e proporção, e a lógica de colisão do Quando as Coisas se Tocam.)

Exercício 4

O Breakout mantém os tijolos destruídos no vetor (marcados com vivo = false). Discuta o trade-off entre essa abordagem e removê-los do vetor com o idioma erase-remove (artigo Pare de Escrever Laços — a Biblioteca <algorithm>). Em que situação cada escolha seria preferível?

Ver resposta

✓ Resposta: Manter os tijolos destruídos no vetor (marcados vivo = false) versus removê-los com erase-remove: - Manter (marcar como morto): vantagens — simplicidade (só mudar um booleano), estabilidade de índices e posições (os tijolos vivos não mudam de lugar no vetor, nem de posição na tela), e nenhum custo de realocação. Desvantagem — o laço de colisão e render continua percorrendo os tijolos mortos (pulando-os com if (!vivo) continue), um pequeno desperdício que cresce se houver muitíssimos objetos mortos. Para o Breakout, com 60 tijolos, isso é irrelevante. - Remover (erase-remove, Pare de Escrever Laços): vantagem — o vetor encolhe, e os laços só percorrem tijolos vivos, sem desperdício. Desvantagens — o erase desloca elementos (custo O(n)), invalida índices/iteradores (cuidado se algo referencia tijolos por posição), e a remoção no meio de um frame de jogo pode complicar a lógica se outras partes assumem posições fixas. Situações: manter é preferível quando o número de objetos é modesto (como aqui) e a simplicidade e estabilidade importam mais que o micro-desempenho — o caso do Breakout. Remover é preferível quando há um número enorme de objetos e muitos são destruídos, tornando o desperdício de percorrer mortos significativo, ou quando a memória importa e você quer liberar o espaço dos destruídos. A escolha segue a mesma disciplina da Fase 4: para poucos objetos, prefira a simplicidade; para muitos, meça e otimize se o percurso dos mortos virar gargalo. No nosso caso, marcar como morto é a escolha certa — simples e suficiente.

Exercício 5

Escolha um dos três jogos (Pong, Snake ou Breakout) e projete três melhorias que você gostaria de implementar, indicando, para cada uma, quais conceitos do curso (fases principais ou trilhas) você usaria. Por exemplo: "adicionar som ao quebrar tijolos usaria a classe Som RAII do Imagem e Som".

Ver resposta

✓ Resposta: Resposta pessoal de projeto — uma boa resposta escolhe um jogo e propõe melhorias com os conceitos certos. Exemplo (Breakout): - Melhoria 1 — som ao quebrar tijolos e rebater: usaria a classe Som RAII do Imagem e Som (LoadSound/PlaySound envolvidos em RAII), tocando um efeito na colisão com tijolo e na raquete. Conceitos: RAII (Fase 2, Imagem e Som), gerenciamento de recursos. - Melhoria 2 — power-ups que caem de alguns tijolos (raquete maior, bola extra): quando um tijolo especial é destruído, cria-se uma entidade "power-up" que cai; se a raquete a pega (colisão AABB, Quando as Coisas se Tocam), aplica-se o efeito. Conceitos: ECS/composição (Entidades de Verdade) para os power-ups como entidades, colisão AABB (Quando as Coisas se Tocam), enum/variant (Fase 6) para os tipos de power-up. - Melhoria 3 — múltiplos níveis com layouts diferentes de tijolos: carregar cada nível de um arquivo (a persistência da extensão anterior, Extensão/Extensão com streams) ou de dados embutidos, e avançar de nível ao limpar a parede, adicionando um estado ProximoNivel à máquina de estados (Menu, Jogo, Fim). Conceitos: máquina de estados (Menu, Jogo, Fim), streams/arquivos (Fase 1, extensão), std::vector (Fase 4) para os layouts. O valor do exercício é o aluno perceber que cada melhoria se apoia em conceitos que ele já domina — que o curso inteiro, das fases às trilhas, é a caixa de ferramentas para construir o que quiser. E esse é o fecho perfeito: você não terminou de aprender para parar, mas para começar a criar.

Fim da série completa — obrigado por percorrer toda a jornada, do primeiro "Olá, C++!" ao último pixel. 🎮🎯

Comentários

Mais em Linguagem C++

A Jornada Completa — Retrospectiva de Um Ano de C++
A Jornada Completa — Retrospectiva de Um Ano de C++

Cinquenta e dois artigos depois, o que mudou não foi a lista de recursos…

A Vida Começa Aqui — Inicialização de Membros a Fundo
A Vida Começa Aqui — Inicialização de Membros a Fundo

Inicializar membros parece assunto resolvido até aparecerem os casos em que…

Projetando Abstrações — Encapsulamento, Invariantes e o Ponteiro this
Projetando Abstrações — Encapsulamento, Invariantes e o Ponteiro this

A palavra class não é o que distingue um tipo projetado de um punhado de…