State Machines — Menus, Levels, and Pauses

By Carlos Montiel | Enterprise AI Specialist
Leer en español →
Published: 2026-07-28 | By: Carlos Montiel | Reading time: ~4 minutes

When a game grows past a single loop, you need an orderly way to know what 'mode' it's in at any given moment. That's a state machine.

The Problem with Nested `if`s

In the previous lesson we implemented persistent saving. Now we need an orderly place to load that save from: a main menu. The typical beginner mistake is to keep adding boolean flags — `bool in_menu`, `bool paused`, `bool in_game` — until the update code fills up with nested conditionals that are impossible to follow.

// What we DON'T want: this scales terribly. if (in_menu && !paused) { /* ... */ } else if (!in_menu && in_game && !paused) { /* ... */ } else if (!in_menu && in_game && paused) { /* ... */ } // and it keeps growing with every new screen...

A finite state machine (FSM) formally solves this: the game is always in exactly one state, and the transitions between states are explicitly defined.

Defining the States

enum class GameState { MAIN_MENU, PLAYING, PAUSED, GAME_OVER }; GameState current_state = GameState::MAIN_MENU;

A Common Interface for Each State

So each state handles its own input, updates, and drawing without mixing logic, we define a base interface. This also makes it easy to add new states without touching existing ones — an important design principle worth asking your AI assistant to check for in your own code as the game grows.

class State { public: virtual ~State() = default; virtual void handle_input(const Uint8* keyboard, SDL_Event& event) = 0; virtual void update(float delta) = 0; virtual void draw(SDL_Renderer* renderer) = 0; };

Implementing the Main Menu

class MenuState : public State { public: void handle_input(const Uint8* keyboard, SDL_Event& event) override { if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_RETURN) { requested_start_game = true; } } void update(float delta) override { // menu animation, "Press Enter" text blink, etc. } void draw(SDL_Renderer* renderer) override { SDL_SetRenderDrawColor(renderer, 20, 20, 40, 255); SDL_RenderClear(renderer); // "PONG - Press Enter to play" text would go here } bool requested_start_game = false; };

Implementing the Pause State

The pause state is a great example of why the FSM helps: to pause, we simply stop calling `update()` on the gameplay state, with no need for a `paused` flag scattered across the physics and collision code.

class PauseState : public State { public: void handle_input(const Uint8* keyboard, SDL_Event& event) override { if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) { requested_resume = true; } } void update(float delta) override { // the game world doesn't advance while paused } void draw(SDL_Renderer* renderer) override { // draw the "frozen" game behind it plus a semi-transparent "PAUSED" overlay } bool requested_resume = false; };

The State Manager and Transitions

The central manager handles switching from one state to another, freeing the old state and creating the new one. Here we use `std::unique_ptr` so memory is managed automatically without manual `delete` — a concrete example of why modern C++ reduces bugs compared to plain C.

#include <memory> class StateManager { public: void change_state(std::unique_ptr<State> new_state) { current_state = std::move(new_state); } void handle_input(const Uint8* keyboard, SDL_Event& event) { if (current_state) current_state->handle_input(keyboard, event); } void update(float delta) { if (current_state) current_state->update(delta); } void draw(SDL_Renderer* renderer) { if (current_state) current_state->draw(renderer); } private: std::unique_ptr<State> current_state; };

Wiring It All into the Main Loop

StateManager manager; manager.change_state(std::make_unique<MenuState>()); while (running) { SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) running = false; manager.handle_input(SDL_GetKeyboardState(nullptr), event); } // Transition example: if the menu requested starting the game, switch state // (in a more complete design, this is handled with callbacks or an observer pattern) manager.update(delta); manager.draw(renderer); SDL_RenderPresent(renderer); SDL_Delay(16); }

A Mental Diagram of the Transitions

Before writing transition code, it's good practice to sketch (even on paper) what states exist and which events trigger the switch between them:

MAIN_MENU --(Enter)--> PLAYING PLAYING --(Escape)--> PAUSED PAUSED --(Escape)--> PLAYING PLAYING --(lives == 0)--> GAME_OVER GAME_OVER --(Enter)--> MAIN_MENU

This is exactly the kind of diagram you can hand your AI assistant to turn directly into the `switch` or transition logic of your `StateManager` — give it the diagram in plain text and ask for the corresponding transition code; you'll get something far more precise than describing the rules in prose.

With a state machine organizing the game's overall flow, in the next lesson we'll fill the `PLAYING` state with something more interesting than a ball: enemies with their own behavior, able to chase the player using pathfinding.

Carlos Montiel
Enterprise AI Solutions Architect
Specialist in LLMs, Agents, and Orchestration
guatemalia.com/en/#contact · info@guatemalia.com

Need to implement AI at your company?

Carlos Montiel is an enterprise AI solutions architect. He implements LLMs, Agents, RAG, and orchestrators for companies across Guatemala and Latin America. Reach out for a consultation.

Contact Carlos Montiel

info@guatemalia.com