Skip to content

States and the stack

A state is a screen or a mode of your game: a title screen, a match, a pause menu, a “Really quit?” box. Each is a class implementing State, and the engine keeps them on a stack, StateContext, which Application is. The stack is the only flow machinery the engine has. Where another engine would have a scene graph of menus or a state-machine asset, Labrador has states you push and pop from C++.

  1. You construct it, usually with std::make_unique, and hand it to the stack.
  2. The stack makes it live: it gives the state its context and calls init(), before the state’s first update() and before it is next drawn. Build what the state draws in init(): anything that resolves a resource by name needs the content loaded, and a game loads its content before app.run makes the first state live.
  3. Every frame it is on top, it gets update(dt), and every frame it is visible it gets draw(renderer).
  4. When it is popped or replaced, the stack destroys it. Release what it set up in its destructor; the services it borrowed are still alive at that point, even when the application is shutting down.

All three are called on this->context() from inside a state:

Call What it does
transition_to(state) Replaces the top state. The first state is installed this way by app.run.
push(state) Puts a state above the current one, which is suspended: it stops being updated.
pop() Takes the top state off. The one below resumes.

They are safe to call from inside your own update(), which is where you will almost always call them. Replacing or popping a state destroys it, so taking effect immediately would destroy the object whose update() is still running. Instead the operation is queued and applied once that call returns. You can call pop() and then simply return.

A pushed state can hand a result back to whoever pushed it. The type is named at the push and checked at the pop. This is how the minimal sample asks before quitting:

samples/minimal/states/hello_state.cpp
this->context()->push<bool>(
std::make_unique<ConfirmState>(this->app_, L"Really quit?"),
[this](const bool& quit)
{
if (quit)
{
this->app_->quit();
}
});
return;

The question, ConfirmState, knows nothing about the screen below it. It answers by popping, with true here and false when B or Escape is pressed:

samples/minimal/states/confirm_state.cpp
if (pads.pressed(0, GamepadButton::a) || keyboard.pressed(Key::enter))
{
this->context()->pop(true);
}

The callback runs once, after ConfirmState has been destroyed and HelloState has resumed. There is no flag to poll every frame and no pointer from the question back to its asker. Popping with a type the push did not ask for throws, rather than reinterpreting the value.

The result does not have to be a bool. LineSweeper’s pause menu answers with an enum class PauseChoice { resume, restart, quit }, and the play state’s callback is a switch. There is also a push(state, on_closed) for a screen whose only answer is that it closed.

Only the top state is updated. Whether the states below it are drawn depends on the top state’s covers_screen(), which is true by default. The stack finds the topmost state that covers the screen and draws from there upward, so a box over a running game says false:

samples/minimal/states/confirm_state.h
// A box over the screen, not a replacement for it - so the state below
// keeps drawing and this appears on top of it.
bool covers_screen() const override { return false; }

and then draws over whatever is already in the view:

samples/minimal/states/confirm_state.cpp
void ConfirmState::draw(Renderer& renderer) const
{
// The state below has already drawn into this view, and this draws over it.
// Nothing here has to know that, and it does not set the view count: the
// state that fills the screen decides how many views the frame has.
DrawList list = renderer.view(0);
this->prompt_->draw(list);
}

A state that is covered is told twice: on_suspend() when something is pushed above it, and on_resume() when that has popped. Not being updated stops a state’s simulation and nothing else, so these are where a game does the rest: pauses its music, stops a looping sound, dims itself. The minimal sample dims its greeting:

samples/minimal/states/hello_state.cpp
void HelloState::on_suspend()
{
this->greeting_->set_colour(labrador::Colour::dim_gray);
}

Alt-tab, minimising and the machine going to sleep are a different question from being covered. They mean nobody is looking at any state, so every live state is told, from the top down, through on_deactivated() and later on_activated(). The states below the top need this most: a match under a pause menu is not being updated, but it may still be holding a music track.

While the window is in the background, gamepads read as disconnected, which is a neutral input. A match that ignores on_deactivated() therefore keeps running with nobody playing it. Each change arrives once, however many separate Windows messages caused it, so a state can pause in one and resume in the other without keeping a flag. A state created while the window is already in the background asks context()->active() in its init(), because it missed the change.

  • A level is a state that owns a scene. Both samples do exactly this: init() creates a Scene and fills it, update() steps it, draw() draws it.
  • Menus are states. A main menu at the bottom of the stack, a match pushed above it, and a pause menu pushed above that. Leaving the match pops back to the menu, which was never destroyed.
  • One answer per question. If two screens need to report the same choice, give it a type (an enum class) and let each push name it.

Next: The tick.

Development documentation (unreleased). Built from Labrador 862e08b of 2026-10-08.