Máquinas de estado — menús, niveles y pausas

By Carlos Montiel | Especialista en IA Empresarial
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.

Carlos Montiel
Arquitecto de Soluciones IA Empresarial
Especialista en LLMs, Agentes y Orquestación
guatemalia.com/#contacto · info@guatemalia.com

¿Necesitas implementar IA en tu empresa?

Carlos Montiel es arquitecto de soluciones IA empresarial. Implementa LLMs, Agentes, RAG y orquestadores en empresas de Guatemala y Latinoamérica. Contáctalo para una consultoría.

Contactar a Carlos Montiel

info@guatemalia.com