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.