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:
- Controle do Jogador: Você poderá acelerar, frear e virar seu carro.
- Oponentes de IA: Outros carros navegarão pela pista de forma autônoma.
- 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.
- Câmera Dinâmica: A câmera seguirá o seu carro, mantendo a ação sempre no centro da tela.
- 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:
- Processa Entradas: Verifica se o jogador pressionou alguma tecla, clicou o mouse ou fechou a janela.
- Atualiza a Lógica: Move os personagens, verifica colisões, atualiza a pontuação, etc.
- 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
// ...
}
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
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
}
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)
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);
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
};
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();
};
-
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 ocar[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;
Análise detalhada:
-
acc
(aceleração) edec
(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;
}
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
}
}
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 componentey
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 debeta
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. Quandon
chega ao último checkpoint,(n + 1)
se tornanum
, enum % 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
}
}
}
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.
-
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;
-
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
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
}
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
ecos
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)