DEV Community

Mr Punk da Silva
Mr Punk da Silva

Posted on

Racing Game Top-Down

Veja mais em: https://mrpunkdasilva.github.io/16Games-in-Cpp/inicio.html


Visão Geral: O Que Vamos Construir?

Nosso objetivo é criar um jogo de corrida onde você controla um carro em uma pista e compete contra outros quatro carros controlados por uma Inteligência Artificial (IA) básica.

Recursos Principais:

  1. Controle do Jogador: Você poderá acelerar, frear e virar seu carro.
  2. Oponentes de IA: Outros carros navegarão pela pista de forma autônoma.
  3. Pista com Checkpoints: A pista não é apenas uma imagem; ela é definida por uma série de pontos de verificação (checkpoints) que os carros devem seguir.
  4. Câmera Dinâmica: A câmera seguirá o seu carro, mantendo a ação sempre no centro da tela.
  5. Interface de Usuário (UI): Mostraremos informações essenciais como sua velocidade, o checkpoint atual e o número de voltas completadas.

Passo 1: A Estrutura Fundamental de um Jogo

Todo jogo, não importa quão complexo, é construído sobre alguns pilares. Vamos começar com eles.

O Game Loop (Laço do Jogo)

O coração de qualquer jogo é o game loop. É um laço while que continua executando enquanto a janela do jogo estiver aberta. Em cada "tick" ou iteração desse laço, o jogo faz três coisas:

  1. Processa Entradas: Verifica se o jogador pressionou alguma tecla, clicou o mouse ou fechou a janela.
  2. Atualiza a Lógica: Move os personagens, verifica colisões, atualiza a pontuação, etc.
  3. Renderiza a Tela: Desenha tudo na tela na sua nova posição.

No nosso código, ele se parece com isto:

while (app.isOpen()) {
    // 1. Processar Entradas (Eventos)
    // ...

    // 2. Atualizar a Lógica do Jogo
    // ...

    // 3. Renderizar a Tela
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Gerenciamento de Estado (GameState)

Nosso jogo tem duas telas principais: o Menu Principal e a Corrida em si. Não podemos ter a lógica da corrida rodando enquanto estamos no menu. Para gerenciar isso, usamos uma máquina de estados. É mais simples do que parece. Usamos uma enum para definir os possíveis estados:

enum GameState { MENU, PLAYING };
GameState gameState = MENU; // O jogo sempre começa no estado MENU
Enter fullscreen mode Exit fullscreen mode

Dentro do nosso game loop, podemos verificar qual é o estado atual e executar apenas a lógica relevante:

if (gameState == MENU) {
    // Lógica e desenho do menu
} else if (gameState == PLAYING) {
    // Lógica e desenho da corrida
}
Enter fullscreen mode Exit fullscreen mode

Passo 2: Construindo o Mundo do Jogo

Agora vamos definir os elementos que compõem nosso mundo de corrida.

A Janela e os Recursos Gráficos

Primeiro, criamos a janela do jogo com um tamanho fixo e um título:

RenderWindow app(VideoMode(640, 480), "Car Racing Game!");
app.setFramerateLimit(60); // Limita o jogo a 60 quadros por segundo (FPS)
Enter fullscreen mode Exit fullscreen mode

Limitar o FPS é importante para que o jogo não rode rápido demais em computadores potentes e para garantir uma experiência consistente.

Em seguida, carregamos nossos recursos (imagens e fontes) usando as classes Texture e Font do SFML. Uma Texture é a imagem em si, carregada na memória da placa de vídeo. Um Sprite é um objeto que pode ser desenhado na tela e que usa uma Texture.

Texture t1, t2;
t1.loadFromFile("images/background.png");
t2.loadFromFile("images/car.png");

Sprite sBackground(t1), sCar(t2);
Enter fullscreen mode Exit fullscreen mode

A Pista de Corrida (points)

Como a IA saberá para onde ir? Nós definimos a pista usando uma série de checkpoints. Pense neles como pontos invisíveis que formam o caminho ideal da corrida. Armazenamos esses pontos em um array 2D:

const int num = 8; // Temos 8 checkpoints
int points[num][2] = {
    {300, 610},
    {1270, 430},
    // ... e assim por diante
};
Enter fullscreen mode Exit fullscreen mode

A IA simplesmente tentará ir do checkpoint 0 para o 1, depois para o 2, e assim por diante, em um ciclo. Esta é uma maneira muito simples e eficaz de criar um comportamento de seguimento de caminho.

A Planta Baixa do Carro (A Estrutura Car)

Para representar cada carro no jogo (tanto o jogador quanto a IA), usamos uma struct. Uma struct é como uma planta baixa que agrupa várias variáveis relacionadas em um único tipo de dado.

struct Car {
    float x, y, speed, angle;
    int n; // Índice do próximo checkpoint alvo
    int lastCheckpoint;

    // Construtor: valores iniciais quando um novo Car é criado
    Car() {
        speed = 2;
        angle = 0;
        n = 0;
        lastCheckpoint = 0;
    }

    void move();
    void findTarget();
};
Enter fullscreen mode Exit fullscreen mode
  • x, y: A posição exata do carro no mundo do jogo (coordenadas de mundo).
  • speed: A velocidade atual.
  • angle: O ângulo para o qual o carro está virado. Importante: Em programação de jogos e matemática, ângulos são quase sempre medidos em radianos, não em graus.
  • n: O índice do próximo checkpoint que este carro está perseguindo. Para o car[0] (jogador), isso nos diz em que parte da pista ele está.
  • lastCheckpoint: O último checkpoint que o carro passou. Isso é crucial para a nossa lógica de contagem de voltas.

Passo 3: As Mecânicas do Jogo - Dando Vida aos Carros

Esta é a parte mais emocionante, onde implementamos a física, a IA e os controles.

Controle e Física do Jogador

O carro do jogador (car[0]) é especial. Ele não usa a IA. Em vez disso, ele responde diretamente às suas teclas.

// Detecta quais teclas estão pressionadas
bool Up = Keyboard::isKeyPressed(Keyboard::Up);
// ...

// 1. Aceleração e Desaceleração
if (Up && speed < maxSpeed) {
    if (speed < 0) speed += dec; // Se estava de ré, freia
    else speed += acc;           // Se estava indo para frente, acelera
}
// ...

// 2. Atrito (Fricção)
if (!Up && !Down) {
    if (speed - dec > 0) speed -= dec; // Perde velocidade gradualmente
    else if (speed + dec < 0) speed += dec;
    else speed = 0; // Para completamente
}

// 3. Virar o Carro
if (Right && speed != 0) angle += turnSpeed * speed / maxSpeed;
Enter fullscreen mode Exit fullscreen mode

Análise detalhada:

  • acc (aceleração) e dec (desaceleração) são pequenas constantes que controlam quão rápido o carro ganha ou perde velocidade. Isso cria uma sensação de inércia.
  • A lógica de virar é interessante: turnSpeed * speed / maxSpeed. Isso faz com que o carro vire mais lentamente em baixas velocidades e mais rapidamente em altas velocidades, o que é o oposto do realismo, mas torna o jogo mais divertido e controlável no estilo arcade.

Movimento Físico (move)

Uma vez que a velocidade e o ângulo são definidos, a função move atualiza a posição do carro.

void move() {
    x += sin(angle) * speed;
    y -= cos(angle) * speed;
}
Enter fullscreen mode Exit fullscreen mode

Isto é trigonometria. sin(angle) e cos(angle) decompõem o movimento diagonal em seus componentes horizontal (X) e vertical (Y).

  • Por que y -= ...? Em muitos sistemas gráficos, incluindo o do SFML, a coordenada (0,0) fica no canto superior esquerdo. O eixo Y aumenta para baixo. No entanto, matematicamente, o eixo Y aumenta para cima. Subtrair o cosseno corrige essa diferença, fazendo o carro se mover "para cima" na tela quando o ângulo é 0.

A Inteligência Artificial (findTarget)

Esta é a função que faz os carros oponentes parecerem vivos. O objetivo deles é simples: virar-se para o próximo checkpoint e avançar.

void findTarget() {
    // 1. Pega as coordenadas do checkpoint alvo
    float tx = points[n][0];
    float ty = points[n][1];

    // 2. Calcula o ângulo exato do carro até o alvo
    float beta = angle - atan2(tx - x, -ty + y);

    // 3. Vira o carro na direção certa
    if (sin(beta) < 0) angle += 0.005 * speed;
    else angle -= 0.005 * speed;

    // 4. Verifica se chegou ao alvo
    if ((x - tx) * (x - tx) + (y - ty) * (y - ty) < 25 * 25) {
        lastCheckpoint = n;
        n = (n + 1) % num; // Passa para o próximo checkpoint
    }
}
Enter fullscreen mode Exit fullscreen mode

Análise detalhada:

  • atan2(delta_x, -delta_y): Esta função é mágica. Ela nos dá o ângulo exato de um ponto a outro. Usamos -ty + y para o componente y para corrigir a inversão do eixo Y que mencionamos antes.
  • beta: É a diferença entre o ângulo atual do carro e o ângulo que ele deveria ter.
  • if (sin(beta) < 0): Este é um truque inteligente. O sinal do seno de beta nos diz se o alvo está à esquerda ou à direita da direção atual do carro. Se for negativo, viramos em uma direção; se for positivo, na outra. Isso garante que o carro sempre tome o caminho mais curto para se alinhar com o alvo.
  • Verificação de Distância: A linha (x-tx)*(x-tx) + ... é a fórmula da distância ao quadrado (d² = dx² + dy²). Usamos a distância ao quadrado para evitar o cálculo da raiz quadrada (sqrt), que é uma operação computacionalmente "cara". Como só queremos saber se a distância é menor que um valor, comparar os quadrados funciona perfeitamente e é mais rápido.
  • n = (n + 1) % num;: O operador módulo (%) é perfeito para criar ciclos. Quando n chega ao último checkpoint, (n + 1) se torna num, e num % num é 0. Isso faz a IA voltar ao primeiro checkpoint e continuar o ciclo.

Sistema de Colisão

A colisão é simples: se dois carros estão muito próximos, nós os empurramos para longe um do outro.

// Loop dentro de loop para comparar cada carro com todos os outros
for (int i = 0; i < N; i++) {
    for (int j = 0; j < N; j++) {
        // ...
        // Calcula a distância ao quadrado
        while (dx * dx + dy * dy < 4 * R * R) {
            // Empurra os carros um pouco para longe um do outro
            car[i].x += dx / 10.0;
            car[j].x -= dx / 10.0;
            // ... e atualiza a distância para a próxima verificação do while
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Este método é chamado de "resolução por impulso" e é muito básico. Ele funciona, mas pode fazer os carros tremerem um pouco quando colidem. Para um jogo simples, é suficiente.


Passo 4: Renderização e Interface

Agora que nossa lógica está pronta, vamos mostrar tudo na tela.

A Câmera que Segue o Jogador

Não movemos a câmera. Em vez disso, movemos o mundo inteiro na direção oposta.

  1. Calculamos um "deslocamento" (offset) para manter o carro do jogador (car[0]) no centro da tela.

    int offsetX = 0, offsetY = 0;
    if (car[0].x > 320) offsetX = car[0].x - 320;
    if (car[0].y > 240) offsetY = car[0].y - 240;
    
  2. Ao desenhar qualquer objeto do mundo (o fundo, os carros), subtraímos esse offset de sua posição.

    sBackground.setPosition(-offsetX, -offsetY);
    sCar.setPosition(car[i].x - offsetX, car[i].y - offsetY);
    

O resultado é que o jogador parece ficar parado no centro enquanto o mundo se move ao seu redor.

A Interface do Usuário (UI)

A UI (texto de velocidade, voltas, etc.) é desenhada por último e sem o deslocamento da câmera. Isso garante que ela permaneça fixa na tela, como um painel de controle.

// Converte a velocidade (float) para uma string
std::string speedStr = "Speed: " + std::to_string(static_cast<int>(car[0].speed * 10));
speedText.setString(speedStr);
app.draw(speedText); // Desenhado em coordenadas de tela fixas
Enter fullscreen mode Exit fullscreen mode

Lógica de Contagem de Voltas

Esta é uma das partes mais importantes da lógica de um jogo de corrida.

if (car[0].n == 0 && car[0].lastCheckpoint == num - 1) {
    laps++;
    car[0].lastCheckpoint = 0; // Reseta para a próxima volta
}
Enter fullscreen mode Exit fullscreen mode

Vamos traduzir esta condição:

  • car[0].n == 0: O próximo alvo do meu carro é o checkpoint inicial (o número 0).
  • car[0].lastCheckpoint == num - 1: O último checkpoint que eu passei foi o último da pista.

Quando ambas as condições são verdadeiras, significa que o jogador acabou de cruzar a linha de chegada, completando uma volta. Então, incrementamos laps e resetamos lastCheckpoint para evitar contar a mesma volta várias vezes.


Conclusão: O Que Você Aprendeu?

Parabéns! Se você seguiu até aqui, você dissecou um jogo completo. Vamos revisar os conceitos-chave:

  • Game Loop: A estrutura fundamental de Entrada -> Lógica -> Renderização.
  • Máquinas de Estado: Como organizar seu jogo em seções lógicas (Menu, Playing).
  • Trigonometria para Movimento: Como usar sin e cos para um movimento suave em 2D.
  • IA Simples: Como fazer um objeto seguir uma série de pontos de forma autônoma usando atan2.
  • Otimização: Como evitar cálculos caros (como sqrt) usando a distância ao quadrado.
  • Câmera 2D: A técnica de mover o mundo para simular uma câmera que segue o jogador.
  • Lógica de Jogo Específica: Como implementar um sistema de contagem de voltas preciso.

Espero que este guia detalhado tenha sido útil. A melhor maneira de aprender é experimentar. Tente mudar os valores de aceleração, a velocidade de curva, ou adicione mais checkpoints à pista. Divirta-se programando!

Top comments (0)