Table of Contents
A state machine asset is a named set of states and the moves allowed between them: its graph
holds one State node per state (a title and a Name setting) and one edge per transition. The
states are edited in the State machine tab, which is the graph
canvas the Behavior tab uses — so placing, dragging, the header's magnet (Snap), renaming in
the inspector, connecting, Delete and undo are the graph's.
Double-clicking the asset (or creating it) opens the tab, like every asset with a tab; a click
only selects. Right-click on its canvas offers a State and
nothing else, named by its number (State 3 for the third); no behavior palette offers one.
Scope
A machine's Scope is a dropdown in its own inspector, since the machine is the asset and its
current state hangs off its id. Local (the default) gives every behavior instance a current
state of its own — two doors open independently. Global keeps one current state for the whole
run, so every behavior using the machine sees the same one, and a second behavior can branch on a
state it never set itself. Scene shares one the same way, but only for as long as the scene
lasts: every Load scene puts the machine back into its start state, and that entry is announced
again, so an On state entered in the new scene hears it. A global machine stays quiet there,
because it never left the state it was in. Scope also decides who hears a move: see Start state
and events below.
Transitions
A transition is an arrow from one state to another. Right-click a state, choose
Make transition, then click the state it should lead to; anywhere else, or Escape, drops the
attempt. It joins two whole states, so there is nothing to aim at on either node and no pins are
drawn.
Underneath it is an ordinary graph edge, which is the whole point: connecting, undo, the clean-up
when a state is deleted and the save format all come from the graph and needed nothing new. A state
therefore still carries an in and an out port — a port is the only anchor an edge has in this
editor — the editor simply never draws them and never lets them be grabbed. Between two states the
edge is a straight arrow rather than the usual curve, running border to border with a little
air at each end. Two transitions between the same pair are pushed apart, so a there-and-back pair
lies side by side instead of on top of each other.
There may also be several transitions the same way round between two states, alarm and
seen from Patrol to Chase, say. They are told apart only by their name, so a further one is
given a name of its own the moment it is made (Transition 2, Transition 3, skipping any name
already leaving that state), and the inspector refuses a rename that would make two of them alike,
the empty name included. They are drawn side by side like a there-and-back pair. In a behavior
graph a second edge between the same two ports is still refused (Already connected).
A transition may also lead from a state back into itself: after Make transition, click the
same state again. It is drawn as a loop over the top edge of the node, and the inspector lists
it once, under out. Taking it re-enters the state: On state exited and then
On state entered fire for it, which is what it is for (attack again, restart a timer). A state
may have more than one such loop, under different names; each further one is drawn outside the
last. Only states may do this; any other node connected to itself is still refused.
A transition can be named. Select a state and the inspector lists its transitions in two
groups, out and in, each row with the state at the far end, a name field and a button to
remove it. The arrow itself is a handle too (#289): a click on it, or on its name, picks the
transition, the inspector then shows its two states and its name field, and Delete or a
right-click → Delete transition removes it. Ctrl/Shift-click and the rubber band pick several.
The name is drawn on the arrow. Names need not be unique across the machine: what
must not repeat is two transitions of the same name leaving the same state (the inspector warns
when they do). That is the point of naming them — a single State transition node asking for
alarm moves every state that has an alarm.
A transition does not restrict Set state: that node still puts the machine into any state it
is wired to. What the arrows are for at runtime is State transition.
A behavior reads it through four nodes, each with a machine input that is a picker while
unconnected (or takes a State machine node, the literal every kind has):
State branchgrows one exec output per state, named after the state, and continues along the one the machine is in. Below them sitselse, and it carries everything the states do not: no start state and noSet stateyet, a state the machine no longer has (deleted, or of another machine), or a machine that arrived as a value and therefore has no state outputs at all. A flow that enters this node always leaves it somewhere — a branch nobody can see taken is the hardest kind to debug.elseis derived like the states rather than fixed, so it sits under them, the way the way out of aBranchsits under what it is an exception to.Set stateis the mirror image: one exec input per state, one exec output. The input the flow arrives through names the new state — soOnStart → Set state (Open)andOn key down → Set state (Closed)are two edges into one node. The interpreter hands every node the exec input a pulse entered through (run(…, entry)); this is the first node to read it.Set state by namedoes the same job with a text instead of a port, for the machine that arrives as a value and has no ports to offer. It resolves the text as a state id first, then as a name; a name two states share resolves to nothing, the same ruleState transitionfollows for two arrows of one name, and for the same reason. Missing the target is not silence: the node has a second exec outputnot foundthat carries the flow on, so a typo, a deleted state or an empty machine can be logged or answered with a default instead of stopping there.State transitiontakes the transition of a chosen name out of the current state, then continues. The name is picked in the inspector from the transitions of the machine on itsmachineinput — the options depend on another field, which no field kind can express, so that picker is written by hand. Leaving it at (unnamed) follows the one transition nobody named. If the current state carries no transition of that name, or two of it, the machine stays where it is — choosing between two arrows the graph never picked between would be a guess. It reads the machine's transitions through the project lookup the interpreter already carries.
The ports are keyed by the state's node id and labelled by its name: renaming a state keeps
every edge. Deleting one is the awkward case, because the port lives in a behavior while the
state lives in the machine — so nothing cleans up there, and the edge stays behind. It is not
lost, though: the behavior's node shows the key as a ghost port with the edge dashed in the
warning colour, and Disconnect on its right-click menu removes it. Put the state back and the
edge works again. Which machine the
ports belong to is read off the graph, not off a running value — the inline pick, a
State machine literal wired in, or an input of the behavior's Start node of the kind
State machine with a machine picked next to its name in the inspector. A variable of that kind
read by a Variables node works the same way: the machine is picked next to the variable's row.
The pick only names the states, the value the variable holds at run time is not checked against
it. An Item at or anything else feeding the input yields no state ports, exactly as
Run behavior knows its behavior only as a setting. That machine is still not
stuck: Set state by name names the state as text, State branch leaves along else, and
Current state answers as always. The price is spelled out, not hidden — a name is typed, so a
typo is possible where a port made it impossible, which is what not found is there for.
Picking another machine
orphans the edges of the old states, the way any setting that decides a node's ports does.
A Start input with a machine picked has that machine picked in advance on every node running
the behavior. Another pick there is allowed, but the node warns ("machine" expects "Doors"):
the called graph asks for the states of its own machine, so with another one every state would
read as never entered. A local machine handed in is the caller's: the called graph reads and
moves the very state the calling instance holds (see The current state while playing below).
Two more read it as data rather than as a flow, both pure readers (no exec ports, answered whenever they are asked):
Current statehands out two texts:namefor showing (Join, aTextobject) andidfor comparing, since a rename moves the name but never the id. Both are empty while the machine is in no state, and a state the machine does not know keeps its id but has no name.Is in stategrows one boolean output per state — the data twin ofState branch, for the places a flow cannot go (Branch,And, a property). Every output is answered, so a state nobody is in readsfalserather than nothing.
Start state and events
A machine may name the state every instance begins in. Right-click a state and choose
Set as start state; the same entry then reads Remove start state, which is the only switch
there is. The start wears a ring on the right of its title in the tab. It is kept on the asset
(start, a state node's id) and not on the node, because only one state can be the start and a
single field is what makes that structural instead of a rule someone has to keep. A start pointing
at a deleted state reads as no start — the dead id stays in the document and means nothing.
Without a start the old behaviour stands: the machine is in no state until Set state runs.
Two event nodes hear the moves, each with a machine input and one exec output per state,
like State branch:
On state enteredruns when the machine enters a state, along that state's output.On state exitedruns when it leaves one. Coming out of no state there is nothing to leave, so nothing runs.
The rules, all of them worth knowing before wiring a graph on them:
- A
Set stateinto the state the machine is already in is no move: nothing is written and nothing fires. That is what keeps aSet staterun every frame from firingenteredevery frame. A transition back into the same state is the exception:State transitionalong it is a move, soexitedandenteredfire for the state, every time it is taken. - The start state counts as entered. A local machine is entered per instance, right after that
instance's
On start; aglobalone is entered once for the whole run, before the first instance exists, and every instance created with the scene hears that one entry. An instance spawned later joins a global machine where it already is, without anenteredof its own. - Order is
exitedof the old state, thenenteredof the new, and both fire after the exec flow that caused the move has finished — never in the middle of it. Moves the events themselves cause are delivered in the same way, round after round until none are left; a graph whose events keep moving the machine stops the run with an endless-loop error, like the step limit. - Who hears it follows the scope: a local machine is heard by the instance that moved it alone,
a global one by every instance with a matching event node — which is the point of sharing a
state. A move made inside a
Run behavior, however deep, counts as a move of the instance that runs the call: its ownOn state enteredandOn state exitedhear it. The event nodes of the called graph itself stay silent, like every event there.
The current state while playing
While a run is on, the State machine tab marks the states it is in: weakly for every state some
instance holds, and strongly for the state held by the instance of the selected scene
object (the behavior itself or the object it hangs under). Without a selection nothing is strong,
which keeps "many instances, many states" readable instead of picking one of them at random. The
marks are the play colour, inside the border so a selected state still shows both, and they are
gone the moment the run stops. Play repaints the tab the way it repaints the Behavior tab
(PlaySession.tick); an editor run has no session to do it, so the editor's frame does it instead.
A local machine's current state is the behavior instance's own, kept beside its variables
(NodeContext.states, machine id → state id): two crates running the same behavior are in
states of their own. A behavior run through Run behavior has no states of its own: it reads and
moves those of the instance that runs the call, however deep, so a state one call sets is still set
in the next one, and the call begins in the start state like the instance does. Two characters
running the same behavior therefore stay apart, each with its calls. A global one is kept for the run instead (SharedState.states, built
once per run in core/graph/sharedState.ts) and every instance reads and moves that one — which
bag a node touches is decided by the machine's scope, not by the node (stateBag). Neither
survives a stop: both begin the next Play at the start state again, or in no state where there is
none, the way a Variables asset starts
at its declared values. A state machine is a kind like a texture — a variable, a struct field,
a list item (tagged {"$statemachine": id}), a port — and a statemachine asset is never placed.
Using the editor
- Concepts
- Accounts
- Agents (MCP)
- Scenes
- Scene editing
- Behaviors
- Structs
- Colliders
- Physics
- Textures
- Vector art
- Pixel art
- Fonts
- Videos
- Sounds
- Instruments
- Patterns
- Songs
- Animations
- Curves
- Tiles
- Tilemaps
- State machines
- Shaders
- Particles
- Containers
- Compositions
- Keyboard
Working on the code