State machines whose states enter, update and exit, with state changes made safely.
Import blitz.states As S gives your program these, with S. in front. Each name leads to its part of the reference.
LastError:Str()CreateMachine:Machine()AddState:Int(machine:Machine, name:Str, behaviour:State)ChangeState:Int(machine:Machine, name:Str)UpdateMachine(machine:Machine, seconds:Double)CurrentState:Str(machine:Machine)StateTime:Double(machine:Machine)StopMachine:Int(machine:Machine)StateOnEnter(machine:Machine), OnUpdate(machine:Machine, seconds:Double), OnExit(machine:Machine)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.

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.
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.
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"
Text 10, 10, "The creature is in its " + S.CurrentState(machine) + " state"
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
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.
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,
ChangeStatewaits until the method has finished, so the lines after it still run: putReturnafter it if they shouldn't. If a method asks for two different changes, only the first one counts.
"Sleep" and "sleep"
are different. Each name can be added only once.UpdateMachine, the new state's OnEnter runs straight away, but its
first OnUpdate comes with your next call.OnExit one last time. Never delete a
machine in one of its own states' methods: that stops the program.Three more complete programs come with the module:
The reference below has every function and rule.
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.
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.
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.
| Declaration | Contract and short use |
|---|---|
CreateMachine():Machine | New caller-owned, empty, stopped machine: Local m:States.Machine = States.CreateMachine() |
AddState(m:Machine, name:Str, behaviour:State):Int | Copies a non-empty, unique, case-sensitive name; borrows the behaviour. No callback. States.AddState m, "Patrol", patrol |
ChangeState(m:Machine, name:Str):Int | Accepts 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):Str | Active name; "" when stopped. Text 10, 10, States.CurrentState(m) |
StateTime(m:Machine):Double | Seconds since entering; zero when stopped. If States.StateTime(m) >= 2D Then States.ChangeState m, "Return" |
StopMachine(m:Machine):Int | Requests a final exit, retains registrations, discards further transition work; returns acceptance. States.StopMachine m |
LastError():Str | Module-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.
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:
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.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.
| Reference | Owner / lifetime |
|---|---|
| Machine | Caller deletes explicitly after its outer call returns. |
| Registration records, name Map, ordered List, pending slot | Machine owns and releases them in Method Delete. No public mutable views. |
| State behaviour | Caller-owned and borrowed until machine deletion, even while stopped. Shared stateless behaviours are useful. |
| Game data/entities held by a behaviour | The game owns them. Keep alive through the last Exit, then clean up after machines and behaviours. |
| Machine received by a callback | Borrowed. 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.
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
examples/lamp.bb: two concrete Types, timer-based alternation and Space.examples/patrol.bb: generated 3D player and enemy, Patrol/Chase/Return,
EntityDistance detection, bounded movement toward home; arrows move the player.examples/game_flow.bb: Menu/Playing/GameOver, a working catch game, pause,
scoring, losing lives and restarting. One model is deliberately borrowed by
three behaviours; per-state enum decisions remain inside game code.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.
Hierarchies, transition graphs, schedulers, pause stacks, persistence and behaviour-tree adapters are deferred. The core has no dependency.