Construindo Doodle Jump em C++

Este tutorial ensina como criar o jogo Doodle Jump do zero usando C++ e SFML. Vamos começar com conceitos básicos e construir o conhecimento passo a passo, explicando cada parte de forma clara e detalhada.
O que é Doodle Jump
Imagine um jogo onde você controla um pequeno personagem que precisa pular de uma plataforma para outra, tentando subir o mais alto possível. É como pular de degrau em degrau de uma escada infinita, mas com algumas regras especiais:
- O personagem sempre cai devido à gravidade (como na vida real)
- Ele só pode se mover para esquerda e direita
- Quando toca uma plataforma enquanto está caindo, automaticamente pula para cima
- Se cair muito para baixo, o jogo termina
- O objetivo é alcançar a maior altura possível
Este jogo nos permite aprender vários conceitos importantes de programação de jogos de forma simples e divertida.
Como Organizar um Jogo
Estados do Jogo - Diferentes Telas
Antes de começar a programar, precisamos pensar em como organizar nosso jogo. Todo jogo tem diferentes "telas" ou "estados". Por exemplo:
- Menu: A tela inicial onde o jogador decide se quer jogar
- Jogando: Quando o jogo está realmente acontecendo
- Game Over: Quando o jogador perde e vê sua pontuação
Chamamos isso de "estados do jogo". É como ter diferentes salas em uma casa - você só pode estar em uma sala por vez, mas pode se mover entre elas.
graph LR
A[MENU] -->|Espaço| B[PLAYING]
B -->|Queda| C[GAME_OVER]
C -->|R| B
C -->|M| A
A -->|ESC| D[EXIT]
Este diagrama mostra como o jogador navega entre as telas:
- Do Menu, apertar Espaço leva para o jogo
- Durante o Jogo, se o jogador cair, vai para Game Over
- No Game Over, pode apertar R para jogar de novo ou M para voltar ao menu
Para implementar isso no código, usamos algo chamado "enum" - que é uma forma de dar nomes para números:
enum GameState {
MENU, // Valor 0 - Tela inicial do jogo
PLAYING, // Valor 1 - Quando estamos jogando
GAME_OVER // Valor 2 - Quando o jogo termina
};
Guardando Informações - Variáveis e Estruturas
Em qualquer jogo, precisamos guardar informações. Por exemplo, onde está o jogador? Onde estão as plataformas? Qual é a pontuação atual?
Posição das Plataformas
Para cada plataforma, precisamos saber sua posição na tela. Uma posição tem duas coordenadas: X (horizontal) e Y (vertical). Criamos uma estrutura para isso:
struct point {
int x, y; // x = posição horizontal, y = posição vertical
};
Pense nisso como um endereço: "A plataforma está na posição X=100, Y=200".
Informações do Jogador
Para o jogador, precisamos guardar várias informações importantes:
int x = 100, y = 100; // Onde o jogador está na tela
int h = 200; // Uma altura especial (explicaremos depois)
float dx = 0, dy = 0; // Velocidade do jogador
int score = 0; // Pontos que o jogador fez
int height = 0; // Maior altura que o jogador alcançou
Vamos entender cada uma:
- x, y: A posição do jogador na tela (como coordenadas de um mapa)
- dx, dy: A velocidade do jogador (dx = velocidade horizontal, dy = velocidade vertical)
- h: Uma altura de referência especial que usamos para o sistema de câmera
- score: Os pontos que o jogador conquistou
- height: A maior altura que o jogador já alcançou no jogo
As Principais Mecânicas do Jogo
Como Funciona a Gravidade
Na vida real, quando você pula, a gravidade te puxa para baixo. No nosso jogo, precisamos simular essa gravidade de forma simples.
Imagine a gravidade como uma força que está sempre puxando o jogador para baixo. A cada momento do jogo (a cada "frame"), a gravidade faz o jogador cair um pouco mais rápido.
graph TD
A[A cada frame do jogo] --> B[Aumentar velocidade para baixo: dy += 0.2]
B --> C[Mover jogador: y += dy]
C --> D[Verificar se tocou plataforma]
D --> E{Tocou plataforma?}
E -->|Sim| F[Pular para cima: dy = -10]
E -->|Não| G[Continuar caindo]
F --> A
G --> A
Vamos entender isso passo a passo:
dy += 0.2; // A cada frame, o jogador cai um pouco mais rápido
y += dy; // Mover o jogador baseado na velocidade atual
Como funciona:
- dy é a velocidade vertical do jogador
- Quando dy é negativo (exemplo: -10), o jogador se move para cima
- Quando dy é positivo (exemplo: +5), o jogador se move para baixo
- A gravidade sempre adiciona +0.2 ao dy, fazendo o jogador cair mais rápido
- Quando o jogador toca uma plataforma, definimos dy = -10, fazendo ele pular para cima
Movimento Horizontal - Esquerda e Direita
O jogador pode se mover para esquerda e direita usando as setas do teclado. Mas há um truque especial: quando o jogador sai de um lado da tela, ele aparece do outro lado.
Imagine que a tela é como um cilindro - se você andar para a direita e sair da tela, você aparece do lado esquerdo. Isso cria a sensação de um mundo infinito.
if (Keyboard::isKeyPressed(Keyboard::Right)) {
x += 3; // Move 3 pixels para a direita
if (x > 400) x = -50; // Se saiu da direita, aparece na esquerda
}
if (Keyboard::isKeyPressed(Keyboard::Left)) {
x -= 3; // Move 3 pixels para a esquerda
if (x < -50) x = 400; // Se saiu da esquerda, aparece na direita
}
Por que -50 e 400?
- A tela tem 400 pixels de largura (de 0 a 400)
- Usamos -50 e 400 para criar uma transição suave
- O jogador desaparece gradualmente de um lado antes de aparecer do outro
O Truque da Câmera - A Parte Mais Inteligente
Esta é a parte mais interessante do jogo. Em vez de fazer o jogador subir na tela quando ele pula alto, fazemos o contrário: mantemos o jogador no mesmo lugar e movemos todo o mundo para baixo!
Imagine que você está em uma esteira rolante que se move para baixo. Você está sempre na mesma posição na esteira, mas o mundo ao seu redor está se movendo.
sequenceDiagram
participant Jogador
participant Câmera
participant Plataformas
Jogador->>Câmera: Subiu muito alto!
Câmera->>Plataformas: Mover todas para baixo
Plataformas->>Plataformas: Recriar plataformas que saíram da tela
Câmera->>Jogador: Manter na mesma altura visual
Como isso funciona no código:
if (y < h) { // Se o jogador subiu acima da altura de referência
int heightGain = h - y; // Calcular quanto subiu
height += heightGain; // Contar para estatísticas
score += heightGain / 5; // Dar pontos por subir
// Mover TODAS as plataformas para baixo
for (int i = 0; i < 10; i++) {
plat[i].y = plat[i].y - dy;
// Se uma plataforma saiu da tela por baixo, criar uma nova no topo
if (plat[i].y > 533) {
plat[i].y = -50; // Nova posição no topo
plat[i].x = rand() % 332; // Posição horizontal aleatória
}
}
y = h; // Colocar o jogador de volta na altura de referência
}
Por que fazer assim?
- O jogador sempre fica visível na tela
- Podemos criar plataformas infinitamente
- É mais fácil de programar
- O jogo nunca "acaba" - sempre há mais plataformas aparecendo
Como Detectar se o Jogador Tocou uma Plataforma
Para saber se o jogador tocou uma plataforma, precisamos verificar se eles estão "se sobrepondo" na tela. É como verificar se dois retângulos estão se tocando.
Mas há uma regra especial: só detectamos a colisão quando o jogador está caindo (não quando está subindo). Isso permite que o jogador passe através das plataformas quando está subindo, mas "aterrisse" nelas quando está descendo.
for (int i = 0; i < 10; i++) { // Verificar todas as 10 plataformas
if ((x + 25 > plat[i].x) && // Jogador não está muito à esquerda
(x + 25 < plat[i].x + 68) && // Jogador não está muito à direita
(y + 70 > plat[i].y) && // Jogador não está muito acima
(y + 70 < plat[i].y + 14) && // Jogador não está muito abaixo
(dy > 0)) { // Jogador está caindo (não subindo)
dy = -10; // Fazer o jogador pular para cima
score += 10; // Dar pontos por conseguir pular
}
}
Entendendo as condições:
- x + 25: Usamos x + 25 porque queremos verificar o centro do jogador
- plat[i].x + 68: 68 é a largura da plataforma
- y + 70: 70 é aproximadamente a altura do jogador
- plat[i].y + 14: 14 é a altura da plataforma
- dy > 0: Só detecta colisão quando o jogador está caindo
Por que só quando está caindo?
- Se o jogador está subindo, ele deve passar através da plataforma
- Se o jogador está descendo, ele deve "aterrissar" na plataforma
- Isso evita que o jogador fique "grudado" na plataforma
A Matemática Por Trás do Jogo
Calculando a Altura dos Pulos
Vamos descobrir algumas coisas interessantes sobre o nosso jogo usando matemática simples.
No nosso jogo:
- A gravidade adiciona 0.2 à velocidade a cada frame
- Quando o jogador pula, sua velocidade inicial é -10
Qual é a altura máxima que o jogador pode alcançar?
Podemos calcular isso! Quando o jogador pula, ele começa com velocidade -10 e a gravidade vai diminuindo essa velocidade até chegar a 0 (quando ele para de subir).
Altura máxima = (velocidade inicial)² ÷ (2 × gravidade)
Altura máxima = 10² ÷ (2 × 0.2) = 100 ÷ 0.4 = 250 pixels
Quanto tempo o jogador fica no ar?
Tempo para subir = velocidade inicial ÷ gravidade = 10 ÷ 0.2 = 50 frames
Tempo total no ar = 2 × tempo para subir = 100 frames
Se o jogo roda a 60 fps, isso significa que cada pulo dura cerca de 1.67 segundos.
Por que as Plataformas Estão a 80 Pixels de Distância?
As plataformas estão espaçadas de 80 pixels verticalmente. Como o jogador pode pular até 250 pixels de altura, ele sempre consegue alcançar as próximas plataformas. Isso mantém o jogo desafiador mas sempre possível de jogar.
graph TB
A[Plataforma Atual] -->|80px| B[Próxima Plataforma]
B -->|80px| C[Plataforma Seguinte]
D[Altura Máxima: 250px] -.->|Alcançável| B
D -.->|Alcançável| C
Como Criar Plataformas Infinitas
Uma das partes mais legais do jogo é que as plataformas nunca acabam. Isso é chamado de "geração procedural" - o computador cria novo conteúdo automaticamente conforme você joga.
Criando as Primeiras Plataformas
Quando o jogo começa, criamos 10 plataformas:
for (int i = 0; i < 10; i++) {
plat[i].x = rand() % 332; // Posição horizontal aleatória
plat[i].y = i * 80 + 100; // Espaçamento vertical de 80 pixels
}
Por que usar 332?
- Nossa tela tem 400 pixels de largura
- Cada plataforma tem 68 pixels de largura
- Para a plataforma caber completamente na tela: 400 - 68 = 332
- Então podemos colocar a plataforma em qualquer posição de 0 a 332
Reciclando Plataformas
Quando uma plataforma sai da parte de baixo da tela, não a jogamos fora. Em vez disso, a "reciclamos" criando uma nova plataforma no topo:
if (plat[i].y > 533) { // Se a plataforma saiu da tela
plat[i].y = -50; // Colocar no topo da tela
plat[i].x = rand() % 332; // Nova posição horizontal aleatória
}
Isso significa que sempre temos exatamente 10 plataformas na tela, mas elas estão sempre mudando de posição.
Como Funciona a Pontuação
O jogo tem três formas de ganhar pontos:
1. Pontos por Subir
score += heightGain / 5; // 1 ponto a cada 5 pixels que subir
Conforme você sobe no jogo, ganha pontos automaticamente. Quanto mais alto, mais pontos!
2. Pontos por Pular em Plataformas
score += 10; // 10 pontos cada vez que toca uma plataforma
Cada vez que você consegue pular em uma plataforma, ganha 10 pontos extras.
3. Bônus Especiais
if (height % 1000 == 0 && height > 0) {
score += 500; // 500 pontos a cada 1000 unidades de altura
}
A cada 1000 unidades de altura, você ganha um bônus especial de 500 pontos!
Melhorando a Aparência do Texto
Texto com Contorno
Para que o texto seja sempre visível (independente da cor do fundo), criamos uma função especial que desenha um contorno ao redor das letras:
void drawTextWithOutline(RenderWindow& window, Text& text, Color outlineColor) {
Vector2f originalPos = text.getPosition();
Color originalColor = text.getFillColor();
// Desenhar "sombra" do texto em 8 posições ao redor
text.setFillColor(outlineColor);
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx != 0 || dy != 0) {
text.setPosition(originalPos.x + dx, originalPos.y + dy);
window.draw(text);
}
}
}
// Desenhar o texto principal por cima
text.setFillColor(originalColor);
text.setPosition(originalPos);
window.draw(text);
}
Esta função desenha o texto em 8 posições ligeiramente diferentes ao redor da posição original, criando um efeito de contorno que torna o texto sempre legível.
Por que o Jogo Funciona Bem
Usando Apenas 10 Plataformas
O jogo só precisa de 10 plataformas ativas ao mesmo tempo. Isso é inteligente porque:
- Economiza memória do computador
- O jogo funciona sempre na mesma velocidade
- É mais fácil de programar e debugar
Verificações Inteligentes
Só verificamos colisões quando o jogador está caindo, não quando está subindo. Isso torna o jogo mais rápido e evita problemas.
Como o Jogo Funciona - O Loop Principal
Todo jogo tem um "loop principal" - um ciclo que se repete muitas vezes por segundo. A cada repetição (chamada de "frame"), o jogo:
flowchart TD
A[Começar o frame] --> B[Ver o que o jogador apertou]
B --> C{Em que tela estamos?}
C -->|MENU| D[Mostrar menu]
C -->|PLAYING| E[Jogar o jogo]
C -->|GAME_OVER| F[Mostrar game over]
D --> G[Desenhar tudo na tela]
E --> H[Mover jogador e plataformas]
F --> G
H --> I[Mover a câmera se necessário]
I --> G
G --> J[Mostrar o frame na tela]
J --> K{Continuar jogando?}
K -->|Sim| A
K -->|Não| L[Fechar o jogo]
O que Cada Estado Faz
- MENU: Mostra o título do jogo e espera o jogador apertar Espaço para começar
- PLAYING: Roda toda a lógica do jogo (física, colisões, pontuação)
- GAME_OVER: Mostra a pontuação final e permite reiniciar ou voltar ao menu
Como Executar o Jogo
Compilar
cd build
make doodle_jump
Jogar
make run_doodle_jump
Arquivos Necessários
O jogo precisa destes arquivos para funcionar:
images/background.png: A imagem de fundoimages/platform.png: A imagem da plataformaimages/doodle.png: A imagem do personagemfonts/Carlito-Regular.ttf: A fonte para os textos
Ideias para Melhorar o Jogo
Novas Mecânicas
- Power-ups como pulo duplo ou jetpack
- Plataformas especiais (que se movem, quebram, ou dão super pulo)
- Inimigos para evitar
- Efeitos sonoros e música
Melhorias Técnicas
- Mais plataformas na tela
- Gráficos mais bonitos
- Animações suaves
- Sistema de save para lembrar da maior pontuação
Código do Jogo Completo
Conclusão
Parabéns! Você aprendeu como funciona um jogo completo. Doodle Jump pode parecer simples, mas ele ensina conceitos muito importantes:
- Estados de jogo: Como organizar diferentes telas
- Física básica: Como simular gravidade e movimento
- Detecção de colisão: Como saber quando objetos se tocam
- Geração procedural: Como criar conteúdo infinito
- Sistema de câmera: Como fazer o mundo se mover em vez do jogador
Estes conceitos são usados em jogos muito mais complexos. Agora que você entende como funciona, pode experimentar modificar os valores no código para ver o que acontece, ou até mesmo criar suas próprias mecânicas!