States

State machines whose states enter, update and exit, with state changes made safely.

Commands

Import blitz.states As S gives your program these, with S. in front. Each name leads to its part of the reference.

Functions

Types

Interfaces

Guide

States keeps track of what something in your game is doing: a guard patrolling, chasing or heading home; a lamp switched on or off; a game showing its menu, being played or showing Game Over. Each state is a small piece of your code. The module runs only the one that's current, and lets each state get ready when it starts and tidy up when it ends.

An orange creature hopping along in its Walk state, with its energy shown above

Try it

Open creature.bb and run it. The creature stands about for a moment, walks somewhere, and stands about again. After three walks it falls asleep. Press Space while it's asleep to wake it up.

; A little creature stands about, walks somewhere, and falls asleep when it
; runs out of energy. Space wakes it up. Escape quits.
Dialect "modern"
Import blitz.states As S

; What every state needs to know about the creature.
Global petX:Float = 320
Global goalX:Float = 320
Global energy:Int = 3

; Each state is a Type with three methods: OnEnter when the creature starts
; doing it, OnUpdate every frame while it does, and OnExit when it stops.
Type Idling Implements S.State
    Method OnEnter(machine:S.Machine)
    End Method
    Method OnUpdate(machine:S.Machine, seconds:Double)
        If S.StateTime(machine) < 1.5D Then Return ; Stand still for a moment.
        If energy > 0 Then S.ChangeState machine, "Walk" Else S.ChangeState machine, "Sleep"
    End Method
    Method OnExit(machine:S.Machine)
    End Method
End Type

Type Walking Implements S.State
    Method OnEnter(machine:S.Machine)
        goalX = Rnd(80, 560) ; Pick somewhere to go.
        energy = energy - 1
    End Method
    Method OnUpdate(machine:S.Machine, seconds:Double)
        Local stride:Float = 150 * Float(seconds) ; 150 pixels a second.
        If Abs(goalX - petX) <= stride
            petX = goalX
            S.ChangeState machine, "Idle" ; Arrived.
        Else
            petX = petX + Sgn(goalX - petX) * stride
        EndIf
    End Method
    Method OnExit(machine:S.Machine)
    End Method
End Type

Type Sleeping Implements S.State
    Method OnEnter(machine:S.Machine)
    End Method
    Method OnUpdate(machine:S.Machine, seconds:Double)
        If S.StateTime(machine) >= 5D Then S.ChangeState machine, "Idle"
    End Method
    Method OnExit(machine:S.Machine)
        energy = 3 ; However it wakes up, it's rested now.
    End Method
End Type

Graphics 640, 300, 0, 2
SetBuffer BackBuffer()
ClsColor 18, 52, 79

; The machine runs whichever state is current. It borrows the states.
Local machine:S.Machine = S.CreateMachine()
Local idle:Idling = New Idling
Local walk:Walking = New Walking
Local sleep:Sleeping = New Sleeping
S.AddState machine, "Idle", idle
S.AddState machine, "Walk", walk
S.AddState machine, "Sleep", sleep
S.ChangeState machine, "Idle"

Local previous:Int = MilliSecs()
While Not KeyDown(1)
    If KeyHit(57) And S.CurrentState(machine) = "Sleep" Then S.ChangeState machine, "Idle"
    Local now:Int = MilliSecs()
    S.UpdateMachine machine, Double(now - previous) / 1000D ; Runs the current state.
    previous = now

    Cls
    Color 40, 95, 70
    Rect 0, 230, 640, 70, True
    Color 30, 75, 55
    Oval petX - 44, 222, 88, 14, True ; Its shadow.
    Local y:Float = 190 ; Hop while walking.
    If S.CurrentState(machine) = "Walk" Then y = 190 - Abs(Sin(MilliSecs() * 0.6)) * 16
    Color 255, 150, 60
    Oval petX - 48, y - 36, 96, 72, True
    Color 70, 35, 20
    If S.CurrentState(machine) = "Sleep"
        Rect petX - 24, y - 8, 18, 4, True ; Closed eyes.
        Rect petX + 6, y - 8, 18, 4, True
        Color 255, 255, 255
        Text petX + 40, y - 60, "Z z z"
    Else
        Oval petX - 22, y - 18, 12, 16, True ; Open eyes.
        Oval petX + 10, y - 18, 12, 16, True
    EndIf
    Color 255, 255, 255
    Text 16, 16, "State: " + S.CurrentState(machine) + ", for " + Float(Int(S.StateTime(machine) * 10D)) / 10 + " seconds"
    Text 16, 36, "Energy: " + energy + "      Space wakes it up"
    Flip
Wend
Delete machine ; First the machine, then the states it borrowed.
Delete idle
Delete walk
Delete sleep
End

Each state is a Type that Implements S.State. That's a promise to the module that the Type has the three methods every state needs: OnEnter, OnUpdate and OnExit. They can be empty, as several are here. The machine knows each state by its name. Each time you call UpdateMachine, it calls the current state's OnUpdate. When a state changes, it calls the old state's OnExit and then the new one's OnEnter. Sleeping gives the creature its energy back in OnExit, so the creature is rested whether it wakes up by itself or you wake it.

How do I...

Change state when the player does something?

Call ChangeState from your game loop. The old state's OnExit and the new one's OnEnter run straight away, before ChangeState returns.

If KeyHit(31) Then S.ChangeState machine, "Sleep" ; S sends it to sleep.

Do something for a while, then move on?

StateTime is how many seconds the machine has been in its current state, so a state can time itself:

If S.StateTime(machine) >= 3D Then S.ChangeState machine, "Idle"

Show which state something is in?

Text 10, 10, "The creature is in its " + S.CurrentState(machine) + " state"

Pause the game?

Give UpdateMachine 0 seconds. The current state's OnUpdate still runs, but StateTime stands still.

If paused Then S.UpdateMachine machine, 0D Else S.UpdateMachine machine, seconds

Give each enemy a state machine of its own?

Make a machine for each enemy, and a set of states for each machine. Give a state a Field that says which enemy it moves, and use it with Self:

Type Chasing Implements S.State
    Field enemy:Int ; The entity this state moves.
    Method OnEnter(machine:S.Machine)
    End Method
    Method OnUpdate(machine:S.Machine, seconds:Double)
        MoveEntity Self.enemy, 0, 0, 4 * Float(seconds)
    End Method
    Method OnExit(machine:S.Machine)
    End Method
End Type
Local orcMachine:S.Machine = S.CreateMachine()
Local orcChase:Chasing = New Chasing
orcChase.enemy = orc
S.AddState orcMachine, "Chase", orcChase

Two machines can share a state object, but they then share its Fields too.

Find out why a state didn't change?

ChangeState returns False when it refuses, and LastError says why. Most often, the name is misspelt.

If Not S.ChangeState(machine, "Slep") Then Print S.LastError()

Blip's tip

Inside a state's own methods, ChangeState waits until the method has finished, so the lines after it still run: put Return after it if they shouldn't. If a method asks for two different changes, only the first one counts.

Good to know

More examples

Three more complete programs come with the module:

The reference below has every function and rule.

Reference

Needs
No other modules
Examples
game_flow.bb, lamp.bb, patrol.bb

Unpublished 0.1.0 source preview for Blitz3D+ Extended with object-lifecycle-v1. Blitz3D+ comes with this module, so any Extended program can Import blitz.states. No external module, binary, asset or network service is required. Importing does not allocate anything or start a loop.

Start with a lamp

Open examples/lamp.bb and run it. The two Types implement the three callbacks; one second in either state requests the other. Space changes the lamp immediately and Escape closes it. This is the complete basic pattern (the runnable example also draws and cleans up):

Dialect "modern"
Import blitz.states

Type LampOn Implements States.State
    Method OnEnter(machine:States.Machine)
    End Method
    Method OnUpdate(machine:States.Machine, seconds:Double)
        If States.StateTime(machine) >= 1D Then States.ChangeState machine, "Off"
    End Method
    Method OnExit(machine:States.Machine)
    End Method
End Type

Implement LampOff in the same way, requesting "On". Register the caller-owned objects, start a state, and update from your own loop:

Local machine:States.Machine = States.CreateMachine()
Local onState:LampOn = New LampOn
Local offState:LampOff = New LampOff
States.AddState machine, "On", onState
States.AddState machine, "Off", offState
States.ChangeState machine, "Off"
States.UpdateMachine machine, 0.25D
Delete machine
Delete onState
Delete offState

The game chooses seconds, pause and time scale. The module never polls input, reads a clock, renders, updates a world or sleeps. Capture input once, make immediate decisions, update the machine once, then move/update the world and draw.

Public API

All operations use module functions; the only caller-implemented interface is State. Use Delete machine for disposal. Machine has a private constructor and no public representation fields or dispatch methods.

DeclarationContract and short use
CreateMachine():MachineNew caller-owned, empty, stopped machine: Local m:States.Machine = States.CreateMachine()
AddState(m:Machine, name:Str, behaviour:State):IntCopies a non-empty, unique, case-sensitive name; borrows the behaviour. No callback. States.AddState m, "Patrol", patrol
ChangeState(m:Machine, name:Str):IntAccepts an immediate or deferred request; same-state success does nothing. If Not States.ChangeState(m, "Chase") Then Print States.LastError()
UpdateMachine(m:Machine, seconds:Double)Finite non-negative seconds. Advances time, invokes one OnUpdate, then drains requests. States.UpdateMachine m, 0.016D
CurrentState(m:Machine):StrActive name; "" when stopped. Text 10, 10, States.CurrentState(m)
StateTime(m:Machine):DoubleSeconds since entering; zero when stopped. If States.StateTime(m) >= 2D Then States.ChangeState m, "Return"
StopMachine(m:Machine):IntRequests a final exit, retains registrations, discards further transition work; returns acceptance. States.StopMachine m
LastError():StrModule-local main-thread diagnostic; copy immediately after a rejected fallible call.
State.OnEnter(m:Machine)Active name is already installed; time is zero.
State.OnUpdate(m:Machine, seconds:Double)Time already includes this exact delta.
State.OnExit(m:Machine)Queries still describe the departing state and its final time.

Blitz methods/functions without a result annotation are concretely Int-returning procedures; callers ignore callback and UpdateMachine results. Exit is a language keyword, so the plan's callback notation resolves to the consistent legal names OnEnter, OnUpdate, OnExit. StopMachine returns Int because a callback needs to distinguish an accepted Stop from a conflicting request. These are the only changes to the planned public surface beyond its implied LastError. No default object parameters or additional options are needed.

Exact ordering and request arbitration

Outside a callback, changing A to B synchronously delivers A.OnExit, resets time, and delivers B.OnEnter. The entire resulting chain finishes before the outer command returns. Its True result means the request was accepted, not that B necessarily remains current after its callbacks.

Inside a callback, ChangeState and StopMachine use one deferred request slot:

  1. The first accepted request wins. An unknown name returns False without consuming the slot. The same request repeated returns True. A different request returns False with a diagnostic and cannot replace accepted work.
  2. A request for the current state is a successful no-op, including during Exit. It still occupies the slot: a later different request in that callback loses.
  3. After OnUpdate or OnEnter returns, apply the slot. Never recursively invoke another callback on that machine. After an ordinary OnExit, a requested destination replaces the outer destination before entering it. Example: Change A to B; A.OnExit requests C → A.Exit, C.Enter (B never enters). An Exit no-op leaves B as destination. An Exit Stop leaves the machine stopped without delivering A.Exit twice. Requests do not carry across callbacks.
  4. Stop's final Exit and Delete's final Exit reject registration and changes, returning False; Stop inside final Exit is an idempotent True. They cannot restart the machine. Queries keep working until final Exit returns.
  5. At most 64 actual state changes per outer call are allowed, including an initial enter from stopped. A 65th attempt raises RuntimeError with the accumulated name chain and target, instead of hanging. The budget resets on each ordinary outer ChangeState or UpdateMachine. No-op requests do not spend it. This is a bound, not a general graph-cycle detector: a legitimate chain longer than 64 also needs redesign.

OnUpdate gets the supplied delta exactly once. If it switches state, that new state receives no update or leftover time in this call. Large deltas are not clamped or subdivided. Use your own fixed steps if needed. Zero delta still calls OnUpdate; positive subnormal and negative-zero values are legal. Time overflow is rejected before changing the timer. Even stopped updates validate the delta.

Reentrant UpdateMachine on the same machine is a programming error from every callback. Nested operations on a different idle machine are allowed, but if its callback calls back into an active machine, that machine's guards still apply. Registering a new name during a normal callback is safe, immediately visible to ChangeState, and invokes no code. Duplicate registration cannot replace a behaviour. Removal/replacement and an iteration API are intentionally absent.

Ownership and shutdown

ReferenceOwner / lifetime
MachineCaller deletes explicitly after its outer call returns.
Registration records, name Map, ordered List, pending slotMachine owns and releases them in Method Delete. No public mutable views.
State behaviourCaller-owned and borrowed until machine deletion, even while stopped. Shared stateless behaviours are useful.
Game data/entities held by a behaviourThe game owns them. Keep alive through the last Exit, then clean up after machines and behaviours.
Machine received by a callbackBorrowed. Query or request Stop/Change; do not Delete during a normal callback.

Sharing one behaviour between machines shares its own mutable fields. Timers, registrations and requests stay per-machine. To avoid shared counters/targets, create a separate behaviour for each machine.

StopMachine ends the current lifecycle but keeps registration for a later ChangeState. Delete runs final Exit if still active, then releases owned records and containers. Stop followed by Delete does not exit again. Null/deleted aliases are harmless to Delete. Recursive Delete of an alias during destructor final Exit is the language's Destroying-object no-op; it does not run another destructor. Delete during ordinary Enter/Update/Exit (including Stop's Exit) is a fatal programming error. A destructor cannot defer deletion or restore a usable object by returning. Ask the game loop to delete after the outer call instead.

There is no Cancel, Save or Commit command in this flat machine: it has no transaction or persistent snapshot. Stop is an explicit lifecycle action; cleanup never saves, awards points, commits gameplay, or completes a pending transition. A thrown RuntimeError or End aborts the program under normal language rules; callbacks are not exception-recovery boundaries. Keep final Exit short and valid.

Errors and examples

Empty/duplicate registrations, unknown states, conflicting callback requests, and mutation during final Exit return False. AddState, ChangeState and StopMachine clear LastError on entry; nested calls may overwrite it before an outer call returns, so read it at the rejection site. Queries and updates do not clear it. Null/deleted machines or behaviours, negative/non-finite seconds, time overflow, reentrant updates, callback-time Delete, and overlong transition chains are RuntimeErrors. Example of a recoverable rejection:

If Not States.ChangeState(machine, "Unregistered")
    Local diagnostic:Str = States.LastError()
    Print diagnostic
EndIf

Every example accepts --verify: lamp checks four changes, patrol drives real distance decisions through a full return, game flow catches, loses and restarts for two rounds. All paths are finite and explicitly release resources.

Scope

Hierarchies, transition graphs, schedulers, pause stacks, persistence and behaviour-tree adapters are deferred. The core has no dependency.