Skip to content

State Stack

The state stack manages game states (play, pause, menus) with automatic entity lifecycle. Push a state to enter it, pop to leave. Entities spawned during a state are automatically tagged with that state's component and cleaned up when the state is popped.

World methods

These methods are available on every World.

MethodDescription
world:createStateCreate a named state and return its tag component.
world:pushStatePush a state onto the stack.
world:popStatePop the current state from the stack.
world:peekStateReturn the current top state name.

world:createState

Creates a named state with an optional lifecycle policy. Returns a tag component that is auto-added to entities spawned while this state is on top of the stack.

teal
function World:createState(name: string, policy?: StatePolicy): Component

Parameters:

  • name: State name.
  • policy: Optional lifecycle policy for state transitions.

Returns:

  • The tag component for this state.

world:pushState

Pushes a state onto the state stack. Fires the previous top state's onBlur policy and the new state's onEnter policy. Entities spawned after this call automatically receive the state's tag component.

teal
function World:pushState(name: string)

Parameters:

  • name: State name, previously created with world:createState.

world:popState

Pops the current state from the state stack. Fires the current state's onExit policy and the new top state's onFocus policy.

teal
function World:popState()

world:peekState

Returns the name of the current top state, or nil if the stack is empty.

teal
function World:peekState(): string

Creating states

Define states with world:createState(). Each state gets a tag component that is auto-added to entities spawned while the state is active. The returned component can be used in queries.

teal
local GameState = world:createState("game", {
    onBlur = "pause",    -- pause game entities when another state is pushed on top
    onFocus = "resume",  -- resume game entities when this state becomes top again
})

local PausedState = world:createState("paused")
-- Default onExit = "despawn": entities tagged with this state are despawned when popped

Pushing and popping states

teal
-- Start gameplay
world:pushState("game")
-- All entities spawned from here are auto-tagged with the gameState component

-- Pause the game
world:pushState("paused")
-- game.onBlur fires: adds Paused to all gameState entities (they freeze but keep rendering)
-- Entities spawned now are auto-tagged with pausedState

-- Unpause
world:popState()
-- paused.onExit fires: despawns all pausedState entities (pause menu cleaned up)
-- game.onFocus fires: removes Paused from gameState entities (they resume)

Check the current state with peekState():

teal
local current = world:peekState()  -- returns "game", "paused", etc. or nil if stack empty

Lifecycle policies

Each state can define policies for lifecycle events:

PolicyWhen it fires
onEnterWhen this state is first pushed
onExitWhen this state is popped (default: "despawn")
onBlurWhen this state is no longer top (another state pushed)
onFocusWhen this state becomes top again (state above popped)

Policy actions

Each policy can be a string action or a custom function:

ActionEffect
"pause"Adds Paused component to all entities with this state tag
"resume"Removes Paused component from those entities
"despawn"Despawns all entities with this state tag
"disable"Adds Disabled component to those entities
functionCalls the function with the world as argument
teal
world:createState("cutscene", {
    onEnter = function(world)
        -- custom enter logic
    end,
    onExit = "despawn",
})

If no policies are specified, the default is onExit = "despawn".

Auto-tagging

Entities spawned while a state is active automatically receive that state's tag component. This happens transparently in the spawn path.

teal
world:pushState("game")

-- This entity automatically gets the gameState component
world:spawn(
    tecs.builtins.Transform(100, 100),
    gfx.Sprite.fromAseprite("enemy.png", "idle")
)

-- Entities spawned before any pushState have no state tag
-- and persist across all state transitions

Permanent entities

Spawn persistent entities (stars, HUD, cameras) before the first pushState call. They won't have a state tag and will persist through all state transitions without needing any special handling.

Events

The state stack emits events on transitions. Observe them at address 0 (world-level):

teal
world:observe(0, tecs.builtins.StateEnter, function(e)
    print("Entered state:", e.state)
end)

world:observe(0, tecs.builtins.StateExit, function(e)
    print("Exited state:", e.state)
end)

world:observe(0, tecs.builtins.StateBlur, function(e)
    print(e.state, "lost focus, pushed:", e.pushed)
end)

world:observe(0, tecs.builtins.StateFocus, function(e)
    print(e.state, "regained focus, popped:", e.popped)
end)

Querying by state

The state component returned by createState can be used in queries:

teal
local GameState = world:createState("game")

-- Find all entities in the "game" state
local gameEntities = world:query({include = {GameState}})

Conditional systems

Use runIf.inState to conditionally run systems based on the current state:

teal
local runIf = tecs.runif

world:addSystem({
    name = "GameplayUpdate",
    phase = tecs.phases.Update,
    runIf = runIf.inState("game"),
    run = function(dt)
        -- only runs when "game" is the top state
    end
})

Example: full game flow

teal
-- Create states
local GameState = world:createState("game", {
    onBlur = "pause",
    onFocus = "resume",
})
world:createState("paused")
world:createState("dead")

-- Spawn permanent entities (HUD, stars) before pushState
world:spawn(tecs.builtins.Transform(0, 0), gfx.Text(font, "SCORE"))

-- Start game
world:pushState("game")
-- Spawn game entities (auto-tagged)

-- Pause
world:pushState("paused")
-- Game entities freeze, pause menu entities spawned

-- Unpause
world:popState()
-- Pause menu despawned, game entities resume

-- Player dies
world:pushState("dead")
-- Game entities freeze again, death overlay spawned

-- Restart
world:popState()   -- dead entities despawned, game entities resume
world:popState()   -- game entities despawned
world:pushState("game")  -- fresh start