On this page
- Usage & Examples
- Core Usage
- Advanced Usage
- 4. Queued Transitions to Avoid Mid-Update Re-entry
- 5. Manual Queue Processing
- 6. Pausing a State Machine
- 7. Using Push/Pop for Overlays (Pause State)
- 8. Per-State Timers for More Complex Timing
- 9. State Change Hook for Logging & Signals
- 10. Declarative transitions
- 11. Transition payloads & ensuring states
- 12. Custom state events (advanced)
- 13. Inspecting or clearing a queued state
- 14. History peek (previous states)
- 15. Resetting / cleaning up a machine
- Gotchas & Best Practices
Usage & Examples
This page shows how to use Statement in practice, split into:
- Core usage - basic patterns.
- Advanced usage - optional patterns for more complex behaviour.
- Gotchas - things to keep in mind.
Core Usage
1. Basic Hello World
Create Event
state_machine = new Statement(self);
var _idle = new StatementState(self, "Idle")
.AddEnter(function() {
EchoDebugInfo("Entered Idle");
})
.AddUpdate(function() {
image_angle += 1;
});
state_machine.AddState(_idle);
Step Event
state_machine.Update();
Draw Event (optional)
state_machine.Draw();
This sets up:
- A
Statementstate machine bound toself. - One state,
Idle, withEnter+Updatehandlers. - The state machine driven from Step (and optionally Draw).
2. Multiple States with Simple Transitions
Create Event
state_machine = new Statement(self);
// Idle
var _idle = new StatementState(self, "Idle")
.AddEnter(function() {
sprite_index = spr_player_idle;
hsp = vsp = 0;
})
.AddUpdate(function() {
if (keyboard_check_pressed(vk_anykey)) {
state_machine.ChangeState("Move");
}
});
// Move
var _move = new StatementState(self, "Move")
.AddEnter(function() {
sprite_index = spr_player_move;
})
.AddUpdate(function() {
var _dx = keyboard_check(ord("D")) - keyboard_check(ord("A"));
var _dy = keyboard_check(ord("S")) - keyboard_check(ord("W"));
hsp = _dx * 4;
vsp = _dy * 4;
x += hsp;
y += vsp;
if (_dx == 0 && _dy == 0) {
state_machine.ChangeState("Idle");
}
});
state_machine
.AddState(_idle)
.AddState(_move);
Step Event
state_machine.Update();
Draw Event
state_machine.Draw();
Now if you press any key while in the Idle state, it will transition to the Move state automatically. If you’re not pressing a movement key while in the Move state, it will transition back to the Idle state.
3. Using GetStateTime() for Behaviour
var _attack = new StatementState(self, "Attack")
.AddEnter(function() {
sprite_index = spr_player_attack;
image_index = 0;
})
.AddUpdate(function() {
// Stay in Attack for 20 frames
if (state_machine.GetStateTime() >= 20) {
state_machine.ChangeState("Idle");
}
});
state_machine.AddState(_attack);
Because state_machine resets state_age automatically on state change, you don’t need to call SetStateTime(0) when changing into/out of a state.
Advanced Usage
4. Queued Transitions to Avoid Mid-Update Re-entry
var _move = new StatementState(self, "Move")
.AddUpdate(function() {
// movement logic...
if (mouse_check_button_pressed(mb_left)) {
// Queue an attack; actual change happens at next Update
state_machine.QueueState("Attack");
}
});
var _attack = new StatementState(self, "Attack")
.AddEnter(function() {
sprite_index = spr_player_attack;
image_index = 0;
})
.AddUpdate(function() {
if (image_index >= image_number - 1) {
state_machine.ChangeState("Idle");
}
});
state_machine
.AddState(_move)
.AddState(_attack);
Because transitions are queued, the rest of the current event runs with the old state, and Attack starts cleanly next frame.
5. Manual Queue Processing
If you want explicit control over when queued transitions happen:
Create Event
state_machine = new Statement(self)
.SetQueueAutoProcessing(false);
Step Event
// Do your own logic...
// When ready to apply queued transitions:
state_machine.ProcessQueuedState();
state_machine.Update();
Now you can insert queue processing at a specific spot in your step pipeline.
6. Pausing a State Machine
You can pause automatic processing (Update/queue/declarative transitions) while still allowing manual transitions.
// e.g., global pause toggle
if (game_paused) {
state_machine.SetPaused(true);
} else {
state_machine.SetPaused(false);
}
// Step Event
// You can run Update unconditionally, but it will only do its work if the state machine is not paused
state_machine.Update();
Pausing does not prevent manual ChangeState calls or Draw(); it just freezes the automatic flow inside Update().
7. Using Push/Pop for Overlays (Pause State)
Create Event
state_machine = new Statement(self);
// Existing states: Idle, Move, Attack... (omitted)
// Pause overlay
var _pause = new StatementState(self, "Pause")
.AddEnter(function() {
hsp = vsp = 0;
})
.AddUpdate(function() {
if (keyboard_check_pressed(vk_escape)) {
state_machine.PopState();
}
})
.AddDraw(function() {
draw_set_alpha(0.5);
draw_rectangle(0, 0, display_get_gui_width(), display_get_gui_height(), false);
draw_set_alpha(1);
draw_text(32, 32, "PAUSED");
});
state_machine.AddState(_pause);
Step Event
if (keyboard_check_released(vk_escape)
&& state_machine.GetStateName() != "Pause") {
state_machine.PushState("Pause");
}
state_machine.Update();
/// Draw
state_machine.Draw();
PushState("Pause")saves the current state and entersPause.PopState()returns to exactly that state.
8. Per-State Timers for More Complex Timing
Create Event
state_machine = new Statement(self);
charge = new StatementState(self, "Charge")
.AddEnter(function() {
charge.TimerStart(); // per-state timer
})
.AddUpdate(function() {
// Charge for at least 60 frames, regardless of how often Update runs
if (charge.TimerGet() >= 60) {
state_machine.ChangeState("Release");
}
});
release = new StatementState(self, "Release")
.AddEnter(function() {
// some effect...
})
.AddUpdate(function() {
// ...
});
state_machine
.AddState(charge)
.AddState(release);
You can pause and restart the per-state timer:
if (game_is_paused) {
charge.TimerPause();
} else {
charge.TimerRestart();
}
If you need to access a state from its own handlers or from other events, store it in an instance variable (like charge and release above), not a local var. Local variables only exist for the duration of the event or function where they are declared, while instance variables live for the lifetime of the instance. Alternatively, if you have access to the state machine itself, you can retrieve specific states via the GetState() method.
Almost all of the time you can rely on GetStateTime() on the machine instead and skip per-state timers entirely, they are implemented for very niche circumstances.
9. State Change Hook for Logging & Signals
state_machine.SetStateChangeBehaviour(method(self, function() {
EchoDebugInfo("State changed to: " + string(state_machine.GetStateName()));
// You could also emit a signal here, update UI, etc.
}));
This runs for every transition, making it useful for:
- Debugging.
- Analytics.
- Emitting events into your own signal/event system.
The function given as the argument for
SetStateChangeBehaviour()is NOT automatically bound to the owner of the state. This is done to allow you greater expression in how you might want it scoped (for instance, scoping it to your debug logger or something). This is why we are usingmethod()explicitly to bind the scope in this example.
10. Declarative transitions
Add transitions to a state that fire automatically when conditions pass. They are evaluated after each Update().
var _run = new StatementState(self, "Run")
.AddUpdate(function() {
// run logic...
})
.AddTransition("Idle", function() {
return abs(hsp) < 0.05; // condition bound to owner via method()
})
.AddTransition("Jump", function() {
return keyboard_check_pressed(vk_space);
});
- Transitions are checked in the order they were added; the first that returns
truefires. - You can attach a payload via
ChangeStateorEvaluateTransitions(payload), then read it in the next state’sEnterviaGetLastTransitionData(). - If you need to disable automatic evaluation (for custom ordering), set
SetQueueAutoProcessing(false)and callEvaluateTransitions()manually.
11. Transition payloads & ensuring states
Carry data across transitions and avoid redundant changes.
// When taking damage, carry a payload into the new state.
state_machine.ChangeState("Hitstun", { damage: last_damage });
// In the Hitstun state's Enter
hitstun.AddEnter(function() {
var _payload = state_machine.GetLastTransitionData();
if (is_struct(_payload)) hp -= _payload.damage;
});
// Only change if not already in Idle
state_machine.EnsureState("Idle");
// Quick checks
if (state_machine.IsInState("Attack")) {
// do something
}
Use GetQueuedStateData() when inspecting queued transitions, and WasPreviouslyInState(name, depth) when reasoning about history.
12. Custom state events (advanced)
You can define your own event index and bind/run it manually. For example, add an ANIMATION_END enum:
scr_statement_macro Script
enum eStatementEvents {
ENTER,
EXIT,
STEP,
DRAW,
ANIMATION_END, // custom
NUM // keep NUM last
}
In Create, bind a handler for that event:
Create Event
state_machine = new Statement(self);
var _attack = new StatementState(self, "Attack")
.AddEnter(function() {
sprite_index = spr_player_attack;
image_index = 0;
})
.AddStateEvent(eStatementEvents.ANIMATION_END, function() {
// Transition when the attack animation finishes
state_machine.ChangeState("Idle");
});
state_machine.AddState(_attack);
In the Animation End event of the object, run the custom event if present:
Animation End Event
if (state_machine.GetState().HasStateEvent(eStatementEvents.ANIMATION_END)) {
state_machine.RunState(eStatementEvents.ANIMATION_END);
}
This pattern lets you hook into additional event points (like Animation End) while keeping logic organized in your states.
13. Inspecting or clearing a queued state
If you’re queuing transitions manually, you can inspect or clear the pending change:
/// Debug overlay
if (state_machine.HasQueuedState()) {
var _queued = state_machine.GetQueuedStateName();
draw_text(16, 16, "Queued state: " + string(_queued));
}
/// Cancel a queued change (e.g., input cancelled)
state_machine.ClearQueuedState();
14. History peek (previous states)
You can check where you’ve been:
// Last state name
var _prev = state_machine.GetPreviousStateName();
// All history entries
var _count = state_machine.GetHistoryCount();
for (var i = 0; i < _count; ++i) {
var _st = state_machine.GetHistoryAt(i);
if (!is_undefined(_st)) {
draw_text(16, 48 + i * 16, "History " + string(i) + ": " + _st.name);
}
}
// Quick debug dump
state_machine.PrintStateHistory(5); // or state_machine.DebugDescribe();
Use SetHistoryLimit(limit) if you want to cap how many entries are kept.
15. Resetting / cleaning up a machine
When restarting or discarding a machine:
// Fully reset states and queues
state_machine.ClearStates();
// Cleanup timers owned by states on this machine (e.g., in Destroy)
state_machine.Destroy();
// Global nuke of all state timers across machines (e.g., when reloading a run)
StatementStateKillTimers();
Gotchas & Best Practices
Core Gotchas
-
Don’t forget to call
Update()
If you don’t callstate_machine.Update()each Step, your states will never run their Update handlers and queued transitions won’t process (unless you callProcessQueuedState()manually). -
Draw is optional
Only callstate_machine.Draw()if you’re using per-stateAddDraw()handlers. Combining regular object drawing and per-state drawing is fine, but be consistent. -
Owner must be valid
Create states from valid contexts (whereselfexists) or pass a struct as the owner. Passing destroyed IDs or invalid values logs a severe warning and returnsundefined. -
Debug helpers are your friend
DebugDescribe()andPrintStateHistory([limit])emit quick summaries to your debug logger, which helps confirm wiring during development.
Advanced Gotchas
-
Locked states (
can_exitflag) gates transitions
If a state is locked viaLockExit();(orSetCanExit(false);), thenChangeState("Other")will do nothing unless theforceargument is true. Make sure to setUnlockExit()(orSetCanExit(true);) again when you actually want to leave. -
Queued transitions are one-shot
Once a queued state is processed (or cleared), it’s gone. If you want repeated attempts, callQueueState()again from your logic. -
History is for introspection, not control flow
previous_statesis great for debugging and analysis.
For actual control (e.g. temporary overlays), preferPushState/PopStateorPreviousState(). -
Per-state timers are independent of
GetStateTime()
Per-state timers useTimer*methods and GMs underlyingtime_sourcefunctionality. Once started, they tick based on the time-source, not on how often you callUpdate().
They do not automatically matchGetStateTime(); in almost all cases you should preferGetStateTime()and only reach for per-state timers when you need that extra flexibility. -
Clean up when resetting
- Use
ClearStates()when you want to fully reset the machine’s states. - Call
state_machine.Destroy()in your instance’s Destroy/Cleanup event when you are done with a machine so that any per-state timers owned by its states are cleaned up. - Use
StatementStateKillTimers()if you want to globally destroy all state timers for all machines (e.g. when restarting a run).
- Use