Tutorial 18: Game States — Menus and Scenes
Every non-trivial game has multiple screens: a title menu, settings, gameplay, a pause menu, a game-over screen. A state machine keeps these isolated and composable. This tutorial builds a stack-based state manager with fade-to-black transitions, demonstrating the pattern that scales from small jam games to full productions.
Why game states?
Without explicit state management you tend to end up with a jungle of booleans (isInMenu, isPaused, isGameOver) and branching if/else chains inside Update() and Draw(). This becomes hard to reason about and error-prone. A state machine separates concerns: each state owns its own update logic, draw logic, and asset lifetime.
State enum pattern
For small games a single enum plus a switch statement is often enough:
enum class AppState { MainMenu, Gameplay, Paused, GameOver };
AppState currentState_ = AppState::MainMenu;
void Update(GameTime& gt) override {
switch (currentState_) {
case AppState::MainMenu: UpdateMenu(gt); break;
case AppState::Gameplay: UpdateGameplay(gt); break;
case AppState::Paused: UpdatePaused(gt); break;
case AppState::GameOver: UpdateGameOver(gt); break;
}
}
This is sufficient for 2–4 states. For more complex games use the object-oriented pattern below.
GameState base class
// GameState.hpp
#pragma once
#include "Microsoft/Xna/Framework/GameTime.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
class StateManager; // forward declaration
class GameState {
public:
virtual ~GameState() = default;
// Called once when the state becomes active
virtual void Enter() {}
// Called once when the state is popped off the stack
virtual void Exit() {}
// Called every frame while active
virtual void Update(Microsoft::Xna::Framework::GameTime& gt) = 0;
// Called every frame — draw below any overlay states
virtual void Draw(Microsoft::Xna::Framework::Graphics::SpriteBatch& sb) = 0;
// Reference back to manager so states can push/pop siblings
void setManager(StateManager* m) { manager_ = m; }
protected:
StateManager* manager_ = nullptr;
};
Stack-based StateManager
// StateManager.hpp
#pragma once
#include <memory>
#include <vector>
#include "GameState.hpp"
class StateManager {
public:
void Push(std::unique_ptr<GameState> state) {
if (!stack_.empty()) stack_.back()->Exit();
state->setManager(this);
state->Enter();
stack_.push_back(std::move(state));
}
void Pop() {
if (stack_.empty()) return;
stack_.back()->Exit();
stack_.pop_back();
if (!stack_.empty()) stack_.back()->Enter();
}
void Replace(std::unique_ptr<GameState> state) {
Pop();
Push(std::move(state));
}
void Update(Microsoft::Xna::Framework::GameTime& gt) {
if (!stack_.empty()) stack_.back()->Update(gt);
}
void Draw(Microsoft::Xna::Framework::Graphics::SpriteBatch& sb) {
// Draw all states from bottom up (allows transparent overlays)
for (auto& s : stack_) s->Draw(sb);
}
bool Empty() const { return stack_.empty(); }
private:
std::vector<std::unique_ptr<GameState>> stack_;
};
Transition effects — fade to black
Wrap state transitions in a FadeTransitionState that draws a black overlay whose alpha ramps from 0 → 1 → 0, then activates the target state at the midpoint:
// FadeState.hpp
#pragma once
#include "GameState.hpp"
#include "StateManager.hpp"
class FadeState : public GameState {
public:
FadeState(std::unique_ptr<GameState> next,
Single duration = 0.5f)
: next_(std::move(next)), halfDuration_(duration / 2.0f) {}
void Enter() override { timer_ = 0.0f; switched_ = false; }
void Update(Microsoft::Xna::Framework::GameTime& gt) override {
using namespace Microsoft::Xna::Framework;
timer_ += (Single)gt.getElapsedGameTime().TotalSeconds();
if (!switched_ && timer_ >= halfDuration_) {
// Switch to target state at peak opacity
switched_ = true;
manager_->Replace(std::move(next_)); // replaces *this*
}
}
void Draw(Microsoft::Xna::Framework::Graphics::SpriteBatch& sb) override {
using namespace Microsoft::Xna::Framework;
Single alpha;
if (!switched_)
alpha = timer_ / halfDuration_; // fade in (0→1)
else
alpha = 1.0f - ((timer_ - halfDuration_) / halfDuration_); // fade out
alpha = std::clamp(alpha, 0.0f, 1.0f);
Color overlay(0, 0, 0, (bytecs)(alpha * 255.0f));
// Draw a full-screen black quad using a 1x1 white pixel texture
sb.Draw(*pixel_, Rectangle(0, 0, 1920, 1080), overlay);
}
private:
std::unique_ptr<GameState> next_;
Single halfDuration_;
Single timer_ = 0.0f;
bool switched_ = false;
// pixel_ must be injected or accessed via a shared resource system
Microsoft::Xna::Framework::Graphics::Texture2D* pixel_ = nullptr;
};
Complete example: MainMenuState and GameplayState
// MainMenuState.hpp
#pragma once
#include "GameState.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
class GameplayState; // forward
class MainMenuState : public GameState {
public:
void Enter() override {
// Start menu music, etc.
}
void Update(Microsoft::Xna::Framework::GameTime& gt) override {
using namespace Microsoft::Xna::Framework::Input;
auto kb = Keyboard::GetState();
if (kb.IsKeyDown(Keys::Enter) && !prevKb_.IsKeyDown(Keys::Enter)) {
// Transition to gameplay
manager_->Push(std::make_unique<GameplayState>());
}
prevKb_ = kb;
}
void Draw(Microsoft::Xna::Framework::Graphics::SpriteBatch& sb) override {
// Draw menu background, title, "Press Enter" text
}
private:
Microsoft::Xna::Framework::Input::KeyboardState prevKb_;
};
// GameplayState.hpp
#pragma once
#include "GameState.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
class GameplayState : public GameState {
public:
void Enter() override {
// Reset score, load level, play game music
score_ = 0;
}
void Exit() override {
// Stop game music, save high score, etc.
}
void Update(Microsoft::Xna::Framework::GameTime& gt) override {
using namespace Microsoft::Xna::Framework::Input;
auto kb = Keyboard::GetState();
// Escape → push pause menu on top (not replace)
if (kb.IsKeyDown(Keys::Escape) && !prevKb_.IsKeyDown(Keys::Escape)) {
// manager_->Push(std::make_unique<PauseState>());
}
// ... update player, enemies, score ...
prevKb_ = kb;
}
void Draw(Microsoft::Xna::Framework::Graphics::SpriteBatch& sb) override {
// Draw world, HUD, score
}
private:
intcs score_ = 0;
Microsoft::Xna::Framework::Input::KeyboardState prevKb_;
};
// MyGame.cpp — wiring it all together
class MyGame final : public Microsoft::Xna::Framework::Game {
public:
MyGame() : graphics_(this) {}
protected:
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
states_.Push(std::make_unique<MainMenuState>());
}
void Update(GameTime& gt) override {
states_.Update(gt);
if (states_.Empty()) Exit(); // all states popped = quit
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::Black);
spriteBatch_->Begin();
states_.Draw(*spriteBatch_);
spriteBatch_->End();
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
StateManager states_;
};
Design notes
- Push vs Replace — use
Pushfor overlays (pause menu over gameplay). UseReplacefor linear navigation (menu → gameplay). - Asset ownership — states should load their assets in
Enter()and release them inExit(), or use a shared resource cache to avoid redundant loads. - State communication — states can communicate via a shared context object rather than global variables. Pass it to state constructors.
- Deep stacks — if you need the gameplay state to still update while the pause menu is displayed, have
StateManager::Update()walk the stack bottom-up and callUpdateBelow()on states that opt in.