saddle-ai-state-machine uses a split design:
- Static definition: reusable machine structure, hierarchy, regions, transition graph, declaration order, blackboard schema, and debug defaults.
- Runtime instance: entity-local active states, timers, stack, pending transitions, queued signals, blackboard contents, and recent trace entries.
This keeps authoring data reusable across many entities while runtime state stays compact and serializable.
Update Pipeline
IntakeSignals
-> AdvanceTimers
-> EvaluateTransitions
-> ExecuteTransitions
-> UpdateStates
-> DebugVisualizeIntakeSignals
-> AdvanceTimers
-> EvaluateTransitions
-> ExecuteTransitions
-> UpdateStates
-> DebugVisualizeThe pipeline is explicit on purpose:
IntakeSignalsqueuesStateMachineSignalmessages onto instances, aligns runtime storage with the definition schema, and applies per-instance blackboard overrides before first initialization.AdvanceTimersadvances active-state elapsed timers and transition cooldown timers.EvaluateTransitionscomputes deterministic winners or blocked reasons without mutating the machine.ExecuteTransitionsapplies the winning transition, runs hooks, emits messages, and appends trace entries.UpdateStatesruns active-stateon_updatehooks after topology changes settle.DebugVisualizedraws line-based annotations fromAiDebugAnnotationsusingAiDebugGizmos.
Deterministic Arbitration
For every active machine, candidate transitions are ranked by:
- Deepest active source state first
- Higher explicit priority first
- Higher utility score first
- Earlier declaration order last
Any-state transitions participate in the same deterministic policy. Runtime evaluation never relies on HashMap iteration order.
Blocked candidates keep a reason when debug tracing is enabled:
- guard false
- utility below threshold
- cooldown active
- debounce active
- exit not ready
- pending exit
- stack empty / stack overflow
- invalid transition semantics
Hierarchy And Regions
- Atomic states own no child regions.
- Compound states enter their initial child region state, or restore history if configured.
- Parallel states activate all enabled child regions together.
- Child transitions preempt parent transitions because depth wins arbitration.
- Depth outranks priority and utility, so a valid deeper transition wins over a parent transition in the same frame. Parent transitions still fire when no deeper candidate wins.
Parallel-region cross transitions that would jump between sibling root regions of the same parallel state are explicitly rejected by validation in v0.1.
Stack Semantics
TransitionOperation controls topology changes:
Replaceexits the affected active path and enters the new target path.Pushsaves the current active regions/history/timers on the stack, clears the active state, then enters the target.Popexits the current pushed state and restores the previous frame.
The runtime enforces max_stack_depth during transition evaluation so overflow becomes an explicit blocked-transition reason instead of a silent runtime failure.
History Restore
History is recorded when states exit:
HistoryMode::Shallowrestores the last direct child per region.HistoryMode::Deeprestores recorded leaf-region activity under the exited subtree.
History only applies to compound or parallel parents. Validation rejects history configuration on atomic/final/transient states.
Delayed And Done Transitions
TransitionTrigger::AfterSecondschecks the source state's elapsed time.- Leaving a state discards the active path that the timer belonged to, so the timer effectively stops applying.
TransitionTrigger::Donebecomes valid when every active child region of the source compound/parallel state reaches a final state.
Transient states are supported, and validation detects unconditional transient-only cycles that would never quiesce.
Blackboard Model
The blackboard is intentionally simple:
- keys are declared in the machine definition
- keys resolve to stable dense IDs at build time
- values use a compact
BlackboardValueenum for hot-path reads - writes bump a revision counter and track dirty keys
- schema-aware writes reject mismatched value types once the schema is installed
Supported value types in v0.1:
f32i32boolEntityVec2Vec3String
Asset Definitions
Machine definitions can now enter the runtime through StateMachineDefinitionAssetLoader as well as pure Rust builders. Loaded assets still register into StateMachineLibrary, so the runtime keeps one definition path after load time: built and asset-authored machines share the same validation, IDs, and execution pipeline.
Callbacks
Guard, action, and scorer callbacks are registered once in StateMachineCallbacks and referenced by stable IDs from the definition:
- Guards: read world state and decide whether a transition is valid
- Actions: perform world-facing side effects on enter, update, exit, or transition
- Scorers: compute utility scores without changing machine topology
This keeps definitions data-driven without string dispatch in hot loops.
Trace And Debug Data
Every instance carries a StateMachineTrace with bounded capacity. The trace records:
- entered states
- exited states
- triggered transitions
- pending transitions waiting on exit readiness
- blocked transitions
The runtime data needed for BRP/save-load stays directly on StateMachineInstance and Blackboard, so inspecting:
- active path
- active leaf states
- stack contents
- pending transition
- queued signals
- blackboard values
- recent trace entries
does not require hidden editor-only state.