This content originally appeared on DEV Community and was authored by Mr Punk da Silva
Este tutorial detalha a implementação do clássico jogo Xonix, uma fascinante mistura de ação e estratégia. Vamos explorar passo a passo como o jogo é construído, desde a estrutura básica até o algoritmo inteligente de captura de território, tornando-o um guia útil tanto para iniciantes quanto para desenvolvedores que buscam entender lógicas de jogos mais complexas.
O que é Xonix?
Imagine um campo aberto (o “mar”) com inimigos se movendo livremente. Você controla um cursor que começa na borda segura (a “terra”). Seu objetivo é se aventurar no mar para desenhar linhas e capturar novas porções de terra.
- Captura de Território: Você desenha uma trilha na área vazia. Ao retornar para a terra firme, a área que você cercou (e que não contém inimigos) é preenchida, tornando-se sua.
- Inimigos: Vários inimigos se movem pelo mar. Se eles tocarem sua trilha enquanto você a desenha, você perde.
- Risco e Recompensa: Quanto maior a área que você tenta capturar de uma vez, maior o risco, mas também maior a recompensa.
Este jogo ensina conceitos cruciais como manipulação de grades (arrays 2D), algoritmos de preenchimento (flood fill) e gerenciamento de estados.
Como Organizar o Jogo
Máquina de Estados do Jogo
Para gerenciar as diferentes telas e modos de jogo (menu, jogando, pausado, fim de jogo), usamos uma máquina de estados finitos. Isso nos ajuda a separar a lógica e manter o código organizado.
enum GameState {
MainMenu,
Playing,
Paused,
GameOver
};
GameState gameState = MainMenu; // O jogo sempre começa no menu
O fluxo entre os estados é controlado pelas ações do jogador:
graph LR
A[MainMenu] -->|Clique em Jogar| B(Playing)
B -->|Pressiona 'P'| C{Paused}
C -->|Pressiona 'P'| B
B -->|Inimigo toca trilha| D[GameOver]
B -->|Jogador toca trilha| D
D -->|Clique em Jogar Novamente| B
A Estrutura de Dados Central: A Grade (grid
)
O coração do Xonix é uma matriz 2D que representa o campo de jogo. Cada célula da grade pode ter um de quatro valores, cada um com um significado especial:
-
grid[y][x] = 0
: Representa o “mar” – a área vazia e perigosa onde os inimigos se movem. -
grid[y][x] = 1
: Representa a “terra” – a área segura e já capturada. As bordas começam como terra. -
grid[y][x] = 2
: A trilha do jogador. Esta é a parte vulnerável que está sendo desenhada no mar. -
grid[y][x] = -1
: Um valor temporário usado exclusivamente pelo algoritmo de captura de território.
const int M = 25; // Altura da grade
const int N = 40; // Largura da grade
int grid[M][N] = {0}; // Inicializa tudo como 0 (mar)
As Principais Mecânicas do Jogo
Movimento do Jogador e dos Inimigos
- Jogador: Controlado pelas setas do teclado. Ao se mover a partir da terra (
1
) para o mar (0
), ele deixa uma trilha de2
s. - Inimigos: Cada inimigo (
Enemy
) tem uma posição e velocidade. Eles se movem em linha reta e ricocheteiam nas paredes (grid == 1
), criando um movimento caótico e imprevisível.
struct Enemy {
int x, y, dx, dy; // Posição e velocidade
void move() {
x += dx;
if (grid[y / ts][x / ts] == 1) { // Se bater na terra
dx = -dx; // Inverte a direção horizontal
x += dx;
}
y += dy;
if (grid[y / ts][x / ts] == 1) { // Se bater na terra
dy = -dy; // Inverte a direção vertical
y += dy;
}
}
};
Condições de Fim de Jogo
O jogo termina (estado GameOver
) em duas situações:
- Inimigo Colide com a Trilha: Se um inimigo tocar uma célula com valor
2
. - Jogador Colide com a Própria Trilha: Se o jogador, ao se mover, entrar em uma célula que já faz parte de sua trilha atual (valor
2
).
// No loop de atualização do jogador
if (grid[y][x] == 2) gameState = GameOver;
// No loop de verificação de inimigos
for (int i = 0; i < enemyCount; i++)
if (grid[a[i].y / ts][a[i].x / ts] == 2) gameState = GameOver;
O Algoritmo de Captura de Território (Flood Fill)
Esta é a parte mais engenhosa do jogo. Quando o jogador retorna à terra firme (grid == 1
), o jogo precisa decidir qual área será preenchida.
O processo ocorre em duas fases:
Fase 1: Marcar as áreas dos inimigos
O jogo usa um algoritmo de flood fill (preenchimento de área) para descobrir quais partes do “mar” são alcançáveis pelos inimigos.
- A função
drop(y, x)
é chamada para a posição de cada inimigo. - Esta função é recursiva: se uma célula é mar (
0
), ela a marca como temporária (-1
) e chama a si mesma para todos os vizinhos que também são mar. - Ao final, todas as células do mar que um inimigo pode alcançar estarão marcadas com
-1
.
void drop(int y, int x) {
if (grid[y][x] == 0) grid[y][x] = -1; // Marca a célula
// Chama recursivamente para os vizinhos
if (y > 0 && grid[y - 1][x] == 0) drop(y - 1, x);
if (y < M - 1 && grid[y + 1][x] == 0) drop(y + 1, x);
if (x > 0 && grid[y][x - 1] == 0) drop(y, x - 1);
if (x < N - 1 && grid[y][x + 1] == 0) drop(y, x + 1);
}
Fase 2: Preencher a área capturada
Após marcar as áreas dos inimigos, o jogo percorre toda a grade para tomar a decisão final:
// Este loop é executado após a chamada de drop() para todos os inimigos
for (int i = 0; i < M; i++)
for (int j = 0; j < N; j++)
if (grid[i][j] == -1) grid[i][j] = 0; // Se foi alcançada por um inimigo, volta a ser mar
else grid[i][j] = 1; // Caso contrário, torna-se terra firme!
Qualquer célula que não foi marcada com -1
(ou seja, a trilha do jogador e qualquer porção do mar que ficou isolada dos inimigos) é convertida em terra (1
).
graph TD
A[Jogador retorna à terra] --> B{Iniciar Captura}
B --> C[Para cada inimigo, chamar drop, inimigo.y, inimigo.x]
C --> D{Flood Fill marca áreas alcançáveis com -1}
D --> E[Percorrer toda a grade]
E --> F{Célula == -1?}
F -- "Sim" --> G[Transformar em Mar - 0]
F -- "Não" --> H[Transformar em Terra - 1]
G --> E
H --> E
E --> I[Território Capturado]
Estrutura do Código no main.cpp
O loop principal do jogo é organizado em torno da máquina de estados.
int main() {
// ... Inicialização de janela, texturas, fontes, etc. ...
GameState gameState = MainMenu;
while (window.isOpen()) {
// ... Processamento de eventos (input do jogador) ...
// A lógica de input muda com base no gameState (menu, jogo, etc.)
// Lógica de atualização do jogo
if (gameState == Playing) {
// Mover jogador
// Mover inimigos
// Verificar colisões
// Verificar se o jogador capturou uma área
}
// Lógica de renderização
window.clear();
if (gameState == MainMenu) {
// Desenhar menu
} else {
// Desenhar a grade (terra, mar, trilha)
// Desenhar jogador
// Desenhar inimigos
if (gameState == Paused) {
// Desenhar texto "Pausado"
}
if (gameState == GameOver) {
// Desenhar tela de "Game Over" e menu
}
}
window.display();
}
return 0;
}
Conceitos Importantes Aprendidos
- Manipulação de Grid 2D: Como usar uma matriz para representar um mundo de jogo complexo com diferentes tipos de terreno.
- Algoritmo de Flood Fill: Uma aplicação prática e poderosa de recursão para análise de áreas conectadas. É um algoritmo fundamental em muitos jogos e aplicações gráficas.
- Máquina de Estados Finitos: Um padrão de design essencial para organizar o fluxo de um jogo, tornando o código mais limpo e fácil de gerenciar.
- Lógica de Risco vs. Recompensa: O design do jogo incentiva o jogador a tomar decisões estratégicas, equilibrando o perigo de criar uma trilha longa com a vantagem de capturar mais território.
Extensões Possíveis
O código atual é uma base excelente para adicionar novas funcionalidades:
- Sistema de Pontuação: Adicionar pontos com base no tamanho da área capturada.
- Níveis e Dificuldade: Aumentar o número ou a velocidade dos inimigos a cada nível.
- Vidas: Permitir que o jogador perca mais de uma vez antes do “Game Over”.
- Power-ups: Itens que podem congelar os inimigos ou aumentar a velocidade do jogador temporariamente.
This content originally appeared on DEV Community and was authored by Mr Punk da Silva