No artigo Imagem e Som — Recursos de Jogo com RAII, e o Início do Snake montamos a estrutura do Snake e aprendemos a gerenciar recursos com RAII. Hoje completamos o Snake — a cobra vai se mover, comer, crescer e morrer — e, com ele, aprendemos um conceito que todo jogo real precisa: os estados de jogo. Nenhum jogo é só "jogando" — há a tela de menu antes, a tela de "game over" depois, talvez uma pausa no meio. Transitar limpamente entre esses estados é essencial, e a forma elegante de modelá-lo é uma máquina de estados, que construiremos com o enum class (Fase 1) e a clareza do C++ moderno. Ao fim desta aula, você terá um Snake completo e jogável, com menu, jogo e game over — nosso segundo jogo pronto, e a penúltima peça antes do capstone final.
A lógica do Snake: mover, comer, crescer, morrer
Primeiro completamos a jogabilidade que o Imagem e Som deixou em aberto. Usando as operações sobre o std::vector que descrevemos, a cobra se move adicionando uma cabeça e removendo o rabo; cresce quando come; e morre quando bate em si mesma ou na parede:
#include <raylib.h>
#include <vector>
#include <cstdlib>
struct Celula { int x, y; };
bool mesma(Celula a, Celula b) { return a.x == b.x && a.y == b.y; }
const int TAM = 20, COLS = 40, LINS = 30;
// Move a cobra na direção dada. Devolve false se ela morreu (bateu em algo).
bool mover_cobra(std::vector<Celula>& cobra, Celula dir, Celula& comida, bool& cresceu) {
Celula cabeca = { cobra.front().x + dir.x, cobra.front().y + dir.y };
// Morte por parede:
if (cabeca.x < 0 || cabeca.x >= COLS || cabeca.y < 0 || cabeca.y >= LINS)
return false;
// Morte por auto-colisão (cabeça bate no corpo):
for (const auto& seg : cobra)
if (mesma(cabeca, seg)) return false;
cobra.insert(cobra.begin(), cabeca); // adiciona a nova cabeça (Fase 4)
if (mesma(cabeca, comida)) { // comeu?
cresceu = true;
// Reposiciona a comida aleatoriamente (simplificado).
comida = { rand() % COLS, rand() % LINS };
} else {
cobra.pop_back(); // não cresceu: remove o rabo
}
return true; // sobreviveu
}
A elegância do vector aparece: mover é insert no início mais pop_back; crescer é só o insert (sem remover o rabo). A morte é detectada checando se a nova cabeça saiu da grade ou bateu no corpo. Esta é a lógica inteira do Snake — poucas linhas, apoiadas na STL da Fase 4. Mas falta o que envolve o jogo: os estados.
Estados de jogo: a máquina de estados
Um jogo tem momentos distintos — menu, jogando, game over — e em cada um ele se comporta diferente: no menu, espera você apertar Enter; jogando, move a cobra; no game over, mostra a pontuação e espera reiniciar. Modelar isso com ifs soltos vira um emaranhado. A forma limpa é uma máquina de estados: um enum class (Fase 1) que nomeia os estados, e o game loop que se comporta conforme o estado atual:
// Os estados possíveis do jogo — enum class (Fase 1): forte, sem conversões acidentais.
enum class Estado {
Menu,
Jogando,
GameOver
};
O game loop então ramifica pelo estado atual, e as transições entre estados acontecem em resposta a eventos (apertar Enter no menu → Jogando; morrer → GameOver; apertar Enter no game over → Menu). Isso organiza o jogo inteiro em torno de um único Estado:
Estado estado = Estado::Menu; // começa no menu
while (!janela.deve_fechar()) {
switch (estado) {
case Estado::Menu:
// No menu: espera Enter para começar.
if (IsKeyPressed(KEY_ENTER)) {
reinicia_jogo(); // prepara a cobra, a comida, o placar
estado = Estado::Jogando; // TRANSIÇÃO para jogando
}
break;
case Estado::Jogando:
// Jogando: atualiza a cobra; se morrer, vai para game over.
if (!atualiza_jogo()) // devolve false se a cobra morreu
estado = Estado::GameOver; // TRANSIÇÃO para game over
break;
case Estado::GameOver:
// Game over: mostra placar, espera Enter para voltar ao menu.
if (IsKeyPressed(KEY_ENTER))
estado = Estado::Menu; // TRANSIÇÃO de volta ao menu
break;
}
// (o desenho também ramifica pelo estado — cada tela desenha diferente)
}
O enum class Estado (Fase 1) nomeia os estados de forma segura (sem conversões acidentais para inteiro), o switch trata cada um, e as transições são atribuições explícitas de estado. É uma máquina de estados clássica, limpa e extensível — adicionar um estado de "pausa" seria acrescentar um valor ao enum e um caso ao switch. Todo jogo real usa essa estrutura, e ela é a espinha que organiza o fluxo entre as telas. (Para jogos com estados mais complexos, o std::variant da Fase 6 permite que cada estado carregue seus próprios dados, uma evolução natural — mas o enum class basta para o Snake.)
O Snake completo, com as três telas
Juntando a lógica e os estados, temos o Snake inteiro — menu, jogo com cobra que cresce, e game over com placar. Eis a estrutura completa (com as funções auxiliares que esboçamos):
#include <raylib.h>
#include <vector>
#include <cstdlib>
int main() {
Janela janela(800, 600, "Snake Completo"); // RAII (O Primeiro Pixel)
Estado estado = Estado::Menu;
std::vector<Celula> cobra;
Celula direcao{}, comida{};
int placar = 0;
float timer = 0;
const float intervalo = 0.12f; // a cobra move a cada 0.12s (velocidade do jogo)
auto reinicia = [&]() {
cobra = {{20, 15}, {19, 15}, {18, 15}};
direcao = {1, 0};
comida = { rand() % COLS, rand() % LINS };
placar = 0;
timer = 0;
};
while (!janela.deve_fechar()) {
float dt = GetFrameTime();
// --- UPDATE por estado ---
if (estado == Estado::Menu) {
if (IsKeyPressed(KEY_ENTER)) { reinicia(); estado = Estado::Jogando; }
}
else if (estado == Estado::Jogando) {
// Input de direção (impede inverter diretamente sobre si).
if (IsKeyPressed(KEY_UP) && direcao.y == 0) direcao = {0, -1};
if (IsKeyPressed(KEY_DOWN) && direcao.y == 0) direcao = {0, 1};
if (IsKeyPressed(KEY_LEFT) && direcao.x == 0) direcao = {-1, 0};
if (IsKeyPressed(KEY_RIGHT) && direcao.x == 0) direcao = { 1, 0};
// Move em intervalos fixos (não a cada frame — controla a velocidade).
timer += dt;
if (timer >= intervalo) {
timer = 0;
bool cresceu = false;
if (!mover_cobra(cobra, direcao, comida, cresceu))
estado = Estado::GameOver;
if (cresceu) placar += 10;
}
}
else if (estado == Estado::GameOver) {
if (IsKeyPressed(KEY_ENTER)) estado = Estado::Menu;
}
// --- RENDER por estado ---
BeginDrawing();
ClearBackground(BLACK);
if (estado == Estado::Menu) {
DrawText("SNAKE", 320, 200, 60, GREEN);
DrawText("Enter para jogar", 290, 320, 20, WHITE);
}
else if (estado == Estado::Jogando) {
for (const auto& s : cobra)
DrawRectangle(s.x*TAM, s.y*TAM, TAM-1, TAM-1, GREEN);
DrawRectangle(comida.x*TAM, comida.y*TAM, TAM-1, TAM-1, RED);
DrawText(TextFormat("Placar: %d", placar), 10, 10, 20, WHITE);
}
else if (estado == Estado::GameOver) {
DrawText("GAME OVER", 260, 220, 50, RED);
DrawText(TextFormat("Placar: %d", placar), 320, 300, 25, WHITE);
DrawText("Enter para o menu", 280, 360, 20, WHITE);
}
EndDrawing();
}
return 0;
}
Rode e jogue — menu, a cobra come as maçãs vermelhas e cresce, o placar sobe, e ao bater na parede ou em si mesma, game over. Contemple a integração: o game loop (Um Novo Território), a janela RAII (O Primeiro Pixel), o vector da cobra (Fase 4), o enum class dos estados (Fase 1), a lambda reinicia (Fase 5), o delta time controlando a velocidade — todo o curso num jogo completo. Note o detalhe do timer: a cobra não se move a cada frame (seria rápido demais), mas a cada intervalo de tempo, usando o delta time para medir — uma aplicação elegante do Um Novo Território. Você tem agora dois jogos completos, Pong e Snake, e a base para o terceiro.
Um punhado de bool para controlar telas admite estados impossíveis — jogando e em game over ao mesmo tempo — e o bug aparece na transição, não no estado. O enum class com switch torna isso irrepresentável e deixa o acréscimo de uma tela nova quase mecânico. O timer separado do frame é o outro ponto: a cobra anda em intervalos fixos, não a cada quadro, e o delta time é o que mantém isso igual em qualquer máquina.
Fontes e leituras recomendadas
- Robert Nystrom, Game Programming Patterns, capítulo "State": a apresentação canônica da máquina de estados aplicada a jogos.
- cppreference.com/w/cpp/language/enum: a referência do
enum class, usado para modelar os estados de forma segura. - cppreference.com/w/cpp/utility/variant: o
std::variant, para estados que carregam dados próprios — a evolução da máquina de estados. - "Finite State Machines in Game Development": artigos sobre o padrão que estrutura o fluxo de telas de qualquer jogo.
- raylib examples de jogos completos (raylib.com/examples): implementações de Snake e outros para comparar abordagens.
Exercícios
Exercício 1
Compile e jogue o Snake completo. Descreva as três telas (menu, jogando, game over) e as transições entre elas. O que acontece quando a cobra bate na parede?
Ver resposta
✓ Resposta: Ao jogar: a tela de menu mostra o título "SNAKE" e a instrução "Enter para jogar"; apertando Enter, transita para jogando. Na tela jogando, a cobra verde se move continuamente na direção escolhida pelas setas, come as maçãs vermelhas (crescendo e aumentando o placar em 10), e o placar aparece no canto. Quando a cobra bate na parede (ou em si mesma), transita para game over, que mostra "GAME OVER", o placar final, e "Enter para o menu"; apertando Enter, volta ao menu, pronto para uma nova partida. As transições são: Menu →(Enter)→ Jogando →(morte)→ Game Over →(Enter)→ Menu. Ao bater na parede, o mover_cobra detecta que a nova cabeça saiu da grade, devolve false, e o estado muda para GameOver — a cobra para e a tela de fim aparece.
Exercício 2
Explique por que uma máquina de estados (enum class Estado + switch) é mais limpa que gerenciar as telas do jogo com variáveis booleanas soltas (como bool no_menu; bool jogando; bool game_over;). Que problema as booleanas soltas podem causar?
Ver resposta
✓ Resposta: A máquina de estados com enum class Estado é mais limpa porque garante que o jogo está em exatamente um estado a qualquer momento — o estado é uma única variável que vale Menu, Jogando ou GameOver, nunca dois ao mesmo tempo. Com variáveis booleanas soltas (bool no_menu; bool jogando; bool game_over;), nada impede combinações inválidas: você poderia acidentalmente ter no_menu == true e jogando == true simultaneamente (por esquecer de zerar uma ao ligar outra), colocando o jogo num estado incoerente e imprevisível — talvez desenhando o menu e movendo a cobra ao mesmo tempo. Gerenciar N booleanas exige lembrar de, a cada transição, ligar a certa e desligar todas as outras, o que é propenso a erro e piora exponencialmente com mais estados. O enum class torna estados inválidos irrepresentáveis: só um valor por vez, e o switch trata cada um exaustivamente (o compilador pode até avisar se você esquecer um caso). É a filosofia de "tornar estados inválidos impossíveis" — a mesma lição de tornar intenções visíveis e verificáveis no tipo (Fase 6) — aplicada ao fluxo do jogo.
Exercício 3
No Snake, o input de direção tem uma condição extra: IsKeyPressed(KEY_UP) && direcao.y == 0. Explique por que essa checagem (direcao.y == 0) impede um bug em que a cobra se mataria instantaneamente ao inverter a direção.
Ver resposta
✓ Resposta: A checagem direcao.y == 0 (na tecla para cima) impede a cobra de inverter a direção diretamente sobre si mesma — o que a mataria instantaneamente. Considere: a cobra se move para a direita, e seu corpo se estende à esquerda da cabeça. Se o jogador apertasse "esquerda", a cabeça tentaria entrar na célula imediatamente atrás dela, que é ocupada pelo primeiro segmento do corpo — a cobra colidiria consigo mesma e morreria no ato, um comportamento frustrante e não-intencional. A condição impede isso: direcao.y == 0 só permite virar para cima/baixo quando a cobra não está se movendo verticalmente (ou seja, está se movendo horizontalmente), e a checagem análoga direcao.x == 0 só permite virar para esquerda/direita quando não está se movendo horizontalmente. O efeito é bloquear a inversão de 180 graus (virar diretamente para o sentido oposto ao atual), permitindo apenas curvas de 90 graus — que é exatamente como o Snake deve se comportar. A cobra só pode virar "para os lados" em relação à sua direção atual, nunca voltar sobre o próprio pescoço.
Exercício 4
Explique o papel do timer e do intervalo no movimento da cobra. Por que a cobra não se move a cada frame, e como o delta time é usado para controlar sua velocidade de forma independente da taxa de frames?
Ver resposta
✓ Resposta: O timer acumula o tempo decorrido (somando dt a cada frame), e quando atinge o intervalo (0.12s), a cobra dá um passo e o timer zera. Isso controla a velocidade do jogo: a cobra não se move a cada frame porque, a 60 frames por segundo, ela andaria 60 células por segundo — rápido demais para jogar. Em vez disso, ela se move a cada intervalo de tempo (a cada 0.12s, ou seja, cerca de 8 passos por segundo), um ritmo jogável. O delta time é usado para medir esse tempo de forma independente da taxa de frames: como timer += dt acumula segundos reais (não frames), a cobra se move na mesma cadência (8 passos/s) tanto numa máquina de 60fps quanto numa de 144fps — em ambas, o timer atinge 0.12s no mesmo tempo real. Sem o delta time (se contássemos frames), a velocidade da cobra variaria com o hardware, como o erro que o Um Novo Território alertou. O timer/intervalo desacopla a velocidade lógica do jogo da taxa de renderização, permitindo que a tela atualize a 60fps (suave) enquanto a cobra avança a 8 passos/s (jogável) — uma separação elegante entre "quão rápido desenho" e "quão rápido o jogo evolui".
Exercício 5
Adicione um estado Pausa ao Snake: durante o jogo, apertar P pausa (congela a cobra e mostra "PAUSADO"), e apertar P de novo retoma. Descreva as mudanças no enum, no update e no render, e explique como a máquina de estados torna essa adição simples.
Ver resposta
✓ Resposta: Adicionando o estado Pausa:
// No enum:
enum class Estado { Menu, Jogando, Pausa, GameOver };
// No update de Jogando, adiciona a transição para Pausa:
else if (estado == Estado::Jogando) {
if (IsKeyPressed(KEY_P)) { estado = Estado::Pausa; } // pausa
else { /* ...move a cobra normalmente... */ }
}
// Novo caso no update: em Pausa, P retoma.
else if (estado == Estado::Pausa) {
if (IsKeyPressed(KEY_P)) estado = Estado::Jogando; // retoma
// (não atualiza a cobra: ela fica congelada)
}
// No render, adiciona a tela de pausa:
else if (estado == Estado::Pausa) {
// desenha a cobra e a comida congeladas (mesmo do Jogando)...
DrawText("PAUSADO", 300, 260, 40, YELLOW);
}
A máquina de estados torna essa adição simples porque o novo estado se encaixa no padrão existente sem perturbar o resto: acrescenta-se um valor ao enum (Pausa), um caso no update (que trata a transição de/para pausa e simplesmente não move a cobra enquanto pausado), e um caso no render (que desenha o congelamento mais o texto "PAUSADO"). Os outros estados (Menu, Jogando, GameOver) permanecem intocados — a lógica de cada um é isolada em seu próprio caso. É a extensibilidade da máquina de estados: novos estados são adições localizadas, não modificações espalhadas, exatamente como um novo componente ou sistema no ECS. A estrutura que separa os estados claramente é o que permite crescer o jogo sem quebrar o que já funciona — a mesma lição de "estruture para crescer" que atravessa todo o curso.