Publicado: 2026-07-28 | Por: Carlos Montiel | Lectura: ~4 minutos
Cuando un juego crece más allá de un solo bucle, necesitas una forma ordenada de saber en qué "modo" está en cada momento. Eso es una máquina de estados.
El problema de los `if` anidados
En la lección anterior implementamos guardado persistente. Ahora necesitamos un lugar ordenado desde donde cargar esa partida: un menú principal. El error típico de un principiante es ir agregando banderas booleanas — `bool en_menu`, `bool pausado`, `bool en_juego` — hasta que el código de actualización se llena de condicionales anidados imposibles de seguir.
// Lo que NO queremos: esto escala muy mal.
if (en_menu && !pausado) { /* ... */ }
else if (!en_menu && en_juego && !pausado) { /* ... */ }
else if (!en_menu && en_juego && pausado) { /* ... */ }
// y sigue creciendo con cada pantalla nueva...
Una máquina de estados finita (FSM, Finite State Machine) resuelve esto formalmente: el juego está siempre en exactamente un estado, y las transiciones entre estados están explícitamente definidas.
Definiendo los estados
enum class EstadoJuego {
MENU_PRINCIPAL,
JUGANDO,
PAUSADO,
GAME_OVER
};
EstadoJuego estado_actual = EstadoJuego::MENU_PRINCIPAL;
Una interfaz común para cada estado
Para que cada estado maneje su propio input, actualización y dibujado sin mezclar lógica, definimos una interfaz base. Esto también facilita agregar estados nuevos sin tocar los existentes — un principio importante de diseño que vale la pena pedirle a tu asistente de IA que revise en tu propio código a medida que el juego crece.
class Estado {
public:
virtual ~Estado() = default;
virtual void manejar_input(const Uint8* teclado, SDL_Event& evento) = 0;
virtual void actualizar(float delta) = 0;
virtual void dibujar(SDL_Renderer* renderer) = 0;
};
Implementando el menú principal
class EstadoMenu : public Estado {
public:
void manejar_input(const Uint8* teclado, SDL_Event& evento) override {
if (evento.type == SDL_KEYDOWN && evento.key.keysym.sym == SDLK_RETURN) {
solicito_iniciar_juego = true;
}
}
void actualizar(float delta) override {
// animación del menú, parpadeo del texto "Presiona Enter", etc.
}
void dibujar(SDL_Renderer* renderer) override {
SDL_SetRenderDrawColor(renderer, 20, 20, 40, 255);
SDL_RenderClear(renderer);
// aquí iría el texto "PONG - Presiona Enter para jugar"
}
bool solicito_iniciar_juego = false;
};
Implementando el estado de pausa
El estado de pausa es un buen ejemplo de por qué la FSM ayuda: al pausar, simplemente dejamos de llamar `actualizar()` sobre el estado de juego, sin necesidad de una bandera `pausado` regada por todo el código de física y colisiones.
class EstadoPausa : public Estado {
public:
void manejar_input(const Uint8* teclado, SDL_Event& evento) override {
if (evento.type == SDL_KEYDOWN && evento.key.keysym.sym == SDLK_ESCAPE) {
solicito_reanudar = true;
}
}
void actualizar(float delta) override {
// el mundo del juego no avanza mientras está pausado
}
void dibujar(SDL_Renderer* renderer) override {
// dibuja el juego "congelado" detrás y un overlay semitransparente de "PAUSA"
}
bool solicito_reanudar = false;
};
El gestor de estados y las transiciones
El gestor central se encarga de cambiar de un estado a otro, liberando el estado anterior y creando el nuevo. Aquí usamos `std::unique_ptr` para que la memoria se gestione automáticamente sin `delete` manual — un ejemplo concreto de por qué C++ moderno reduce errores frente a C puro.
#include <memory>
class GestorEstados {
public:
void cambiar_estado(std::unique_ptr<Estado> nuevo_estado) {
estado_actual = std::move(nuevo_estado);
}
void manejar_input(const Uint8* teclado, SDL_Event& evento) {
if (estado_actual) estado_actual->manejar_input(teclado, evento);
}
void actualizar(float delta) {
if (estado_actual) estado_actual->actualizar(delta);
}
void dibujar(SDL_Renderer* renderer) {
if (estado_actual) estado_actual->dibujar(renderer);
}
private:
std::unique_ptr<Estado> estado_actual;
};
Conectando todo en el bucle principal
GestorEstados gestor;
gestor.cambiar_estado(std::make_unique<EstadoMenu>());
while (corriendo) {
SDL_Event evento;
while (SDL_PollEvent(&evento)) {
if (evento.type == SDL_QUIT) corriendo = false;
gestor.manejar_input(SDL_GetKeyboardState(nullptr), evento);
}
// Ejemplo de transición: si el menú pidió iniciar el juego, cambiamos de estado
// (en un diseño más completo, esto se resuelve con callbacks o un patrón observer)
gestor.actualizar(delta);
gestor.dibujar(renderer);
SDL_RenderPresent(renderer);
SDL_Delay(16);
}
Diagrama mental de las transiciones
Antes de escribir código de transición, es buena práctica dibujar (aunque sea en papel) qué estados existen y qué eventos disparan el cambio entre ellos:
MENU_PRINCIPAL --(Enter)--> JUGANDO
JUGANDO --(Escape)--> PAUSADO
PAUSADO --(Escape)--> JUGANDO
JUGANDO --(vidas == 0)--> GAME_OVER
GAME_OVER --(Enter)--> MENU_PRINCIPAL
Este es exactamente el tipo de diagrama que puedes pedirle a tu asistente de IA que convierta directamente en el `switch` o la lógica de transición de tu `GestorEstados` — dale el diagrama en texto plano y pídele el código de transición correspondiente; vas a obtener algo mucho más preciso que describiendo las reglas en prosa.
Con una máquina de estados ordenando el flujo general del juego, en la próxima lección vamos a llenar el estado `JUGANDO` con algo más interesante que una pelota: enemigos con comportamiento propio, capaces de perseguir al jugador usando pathfinding.