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++.
A state’s life
Section titled “A state’s life”- You construct it, usually with
std::make_unique, and hand it to the stack. - The stack makes it live: it gives the state its context and calls
init(), before the state’s firstupdate()and before it is next drawn. Build what the state draws ininit(): anything that resolves a resource by name needs the content loaded, and a game loads its content beforeapp.runmakes the first state live. - Every frame it is on top, it gets
update(dt), and every frame it is visible it getsdraw(renderer). - 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.
Three operations
Section titled “Three operations”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.
Asking a question, and getting an answer
Section titled “Asking a question, and getting an answer”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:
this->context()->push<bool>( std::make_unique<ConfirmState>(this->app_, L"Really quit?"), [this](const bool& quit) { if (quit) { this->app_->quit(); } });return;samples/minimal/states/hello_state.cpp, lines 129–138 at 862e08b
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:
{ this->context()->pop(true);}samples/minimal/states/confirm_state.cpp, lines 57–60 at 862e08b
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.
What a covered state sees
Section titled “What a covered state sees”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:
// 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; }samples/minimal/states/confirm_state.h, lines 29–31 at 862e08b
and then draws over whatever is already in the view:
{ // 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. this->prompt_->draw(list);}samples/minimal/states/confirm_state.cpp, lines 68–75 at 862e08b
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:
void HelloState::on_suspend(){}samples/minimal/states/hello_state.cpp, lines 184–187 at 862e08b
When the window loses focus
Section titled “When the window loses focus”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.
Patterns
Section titled “Patterns”- A level is a state that owns a scene. Both samples do exactly this:
init()creates aSceneand 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.