A state machine says where it starts and who it tells, so the page says it too (#53, #54, #55, #57)

State machines: the start state and how it is set, the two event nodes
with their order, the cap and who hears them per scope, the two reader
nodes, and the highlight in the tab while playing. The old "events and a
highlight are later steps" sentence is gone, as is "a global machine
starts each run in no state at all".

Behaviors: the four new nodes in the node set. Home: the state machines
row. Development: 885 checks, twice.
admin committed 2026-09-15 06:18:37 +02:00
1 parent 5f71a420bc
commit 84df59c535
4 files changed
+68 -12

No files matched your search

+2 -2
@@ -97,7 +97,7 @@ shown on the node and editable in the inspector.
deliberately not used: its delay and rate are an operating-system setting, not a game's. Both deliberately not used: its delay and rate are an operating-system setting, not a game's. Both
rows stay off the node body while the delay is 0, so a plain key node keeps its single line. rows stay off the node body while the delay is 0, so a plain key node keeps its single line.
- **Node set** (`src/core/graph/nodes/`, one file per node): events `On start`, `On enable`, `On update`, `On disable`, `On destroy` (see *Lifecycle*), `On key down/up/held`, - **Node set** (`src/core/graph/nodes/`, one file per node): events `On start`, `On enable`, `On update`, `On disable`, `On destroy` (see *Lifecycle*), `On key down/up/held`,
`On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner — anywhere when it has none), `On animation finished`, `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End`, and the three that read a state machine, `State branch`, `Set state`, `State transition` (see *State machines*) · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`, `On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner — anywhere when it has none), `On animation finished`, `On state entered`, `On state exited` (one output per state, see *State machines*), `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End`, and the three that move or follow a state machine, `State branch`, `Set state`, `State transition` (see *State machines*) · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`,
`Is asset` (is this object an instance of asset X — how you tell what you collided with), `Is asset` (is this object an instance of asset X — how you tell what you collided with),
`Variables`, `Properties` (every property row of the target's type, the transform rows `scale`, `rotation`, `mirrorX`, `mirrorY` included — how a graph turns a sprite), `Get position` (local and world), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`, `Add/Subtract/Multiply/Divide`, `Variables`, `Properties` (every property row of the target's type, the transform rows `scale`, `rotation`, `mirrorX`, `mirrorY` included — how a graph turns a sprite), `Get position` (local and world), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`, `Add/Subtract/Multiply/Divide`,
`Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`, `Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`,
@@ -107,7 +107,7 @@ shown on the node and editable in the inspector.
`Join lists`, `Index of`, structs `Pack`/`Expand`, `Join lists`, `Index of`, structs `Pack`/`Expand`,
`Length`, `Distance`, `Normalize`, `Axis input` (four keys → a `Length`, `Distance`, `Normalize`, `Axis input` (four keys → a
normalized direction, so diagonals are not faster), `Compare`, `And/Or/Not`, `Key is down`, normalized direction, so diagonals are not faster), `Compare`, `And/Or/Not`, `Key is down`,
`Mouse position`, `Mouse button`, `Is hovered`, `Is touching` · `Mouse position`, `Mouse button`, `Is hovered`, `Is touching`, `Current state` (name and id), `Is in state` (one boolean per state) ·
actions `Move` (dx, dy, scale, dt — connect an event's dt for per-second movement; in the object's own axes, which a turned parent turns too), `Set position`, `Spawn` actions `Move` (dx, dy, scale, dt — connect an event's dt for per-second movement; in the object's own axes, which a turned parent turns too), `Set position`, `Spawn`
(asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Emit` (starts a particle source over, see *Particles*), `Log`, `Stop game`, `Restart game`. A node type is one file exporting a `NodeDef` (asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Emit` (starts a particle source over, see *Particles*), `Log`, `Stop game`, `Restart game`. A node type is one file exporting a `NodeDef`
(ports, settings, `run`), listed in `nodes/index.ts`; palette, ports, validation and (ports, settings, `run`), listed in `nodes/index.ts`; palette, ports, validation and
+2 -2
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
# code is lost (tail's is reported), so a red run reads as green # code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output ./build.sh # the same under `set -o pipefail`, for callers that pipe the output
node tools/seed-e2e.mjs && node tools/e2e.mjs # 868 editor checks, screenshots in /tmp/gm-e2e-*.png node tools/seed-e2e.mjs && node tools/e2e.mjs # 885 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below ./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result ./e2e.sh status # live progress, or the last run's result
@@ -183,4 +183,4 @@ widths to find click points. The **render device is not in this repo** — `/dev
and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which
applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets
WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`, WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`,
WebGL 2.0, and the same 868 checks as here. WebGL 2.0, and the same 885 checks as here.
+1 -1
@@ -16,7 +16,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
| [Textures](Textures) | Getting images in, where they go, tinting, the filter, frames, upload limits | | [Textures](Textures) | Getting images in, where they go, tinting, the filter, frames, upload limits |
| [Fonts](Fonts) | Uploading a typeface, what a text wears, what the editor falls back to | | [Fonts](Fonts) | Uploading a typeface, what a text wears, what the editor falls back to |
| [Animations](Animations) | Animation assets, the Animation tab, how they run | | [Animations](Animations) | Animation assets, the Animation tab, how they run |
| [State machines](State-machines) | The state machine asset, its scope, named transitions, `State branch`, `Set state` and `State transition` | | [State machines](State-machines) | The state machine asset, its scope, named transitions, the start state, `State branch`, `Set state`, `State transition`, the two state events and the live highlight |
| [Shaders](Shaders) | The shader asset, the Shader tab, the node set, the Adjust colors recipe, wearing a shader | | [Shaders](Shaders) | The shader asset, the Shader tab, the node set, the Adjust colors recipe, wearing a shader |
| [Particles](Particles) | The particles asset, a particle as a formula of its age, the source's rows, the Particles tab | | [Particles](Particles) | The particles asset, a particle as a formula of its age, the source's rows, the Particles tab |
| [Containers](Containers) | Editing a template, instances and their edits, screens without scene switching | | [Containers](Containers) | Editing a template, instances and their edits, screens without scene switching |
+63 -7
@@ -1,6 +1,6 @@
A **state machine asset** is a named set of states and the moves allowed between them: its graph 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. Events holds one `State` node per state (a title and a *Name* setting) and one edge per transition. The
like "on state entered" and a highlight of the current state while playing are later steps. The states are edited in the **State machine** tab, which is the graph states are edited in the **State machine** tab, which is the graph
canvas the Behavior tab uses — so placing, dragging, the header's **Snap** checkbox, renaming in canvas the Behavior tab uses — so placing, dragging, the header's **Snap** checkbox, renaming in
the inspector, connecting, `Delete` and undo are the graph's. 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 Double-clicking the asset (or creating it) opens the tab, like every asset with a tab; a click
@@ -13,7 +13,7 @@ A machine's **Scope** is a dropdown in its own inspector, since the machine is t
current state hangs off its id. **Local** (the default) gives every behavior instance a current 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 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 run, so every behavior using the machine sees the same one, and a second behavior can branch on a
state it never set itself. A global machine starts each run in no state at all, like a local one. state it never set itself. Scope also decides who hears a move: see *Start state and events* below.
## Transitions ## Transitions
@@ -46,9 +46,9 @@ is wired to. What the arrows are for at runtime is `State transition`.
unconnected (or takes a `State machine` node, the literal every kind has): unconnected (or takes a `State machine` node, the literal every kind has):
- **`State branch`** grows **one exec output per state**, named after the state, and continues - **`State branch`** grows **one exec output per state**, named after the state, and continues
along the one the machine is in. Before `Set state` has run the machine is in **no state**, and along the one the machine is in. Without a start state, and before `Set state` has run, the
the flow ends there — there is no start state. A state the machine no longer has (deleted, or of machine is in **no state** and the flow ends there. A state the machine no longer has (deleted,
another machine) ends it too — the run ignores a ghost port, whatever the canvas draws. or of another machine) ends it too — the run ignores a ghost port, whatever the canvas draws.
- **`Set state`** is the mirror image: **one exec input per state**, one exec output. The input the - **`Set state`** is the mirror image: **one exec input per state**, one exec output. The input the
flow *arrives through* names the new state — so `OnStart → Set state (Open)` and `On key down → flow *arrives through* names the new state — so `OnStart → Set state (Open)` and `On key down →
Set state (Closed)` are two edges into one node. The interpreter hands every node the exec input Set state (Closed)` are two edges into one node. The interpreter hands every node the exec input
@@ -72,12 +72,68 @@ ports belong to is read off the graph, not off a running value — the inline pi
ports, exactly as `Run behavior` knows its behavior only as a setting. Picking another machine ports, exactly as `Run behavior` knows its behavior only as a setting. Picking another machine
orphans the edges of the old states, the way any setting that decides a node's ports does. orphans the edges of the old states, the way any setting that decides a node's ports does.
Two more read it as **data** rather than as a flow, both pure readers (no exec ports, answered
whenever they are asked):
- **`Current state`** hands out two texts: `name` for showing (`Join`, a `Text` object) and `id`
for 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 state`** grows **one boolean output per state** — the data twin of `State branch`, for
the places a flow cannot go (`Branch`, `And`, a property). Every output is answered, so a state
nobody is in reads `false` rather 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 entered`** runs when the machine enters a state, along that state's output.
- **`On state exited`** runs 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 state` into the state the machine is **already in** is no move: nothing is written and
nothing fires. That is what keeps a `Set state` run every frame from firing `entered` every frame.
- The **start state counts as entered**. A local machine is entered per instance, right after that
instance's `On start`; a `global` one 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 an `entered` of its own.
- Order is **`exited` of the old state, then `entered` of 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 a nested `Run behavior` makes to a *local* machine dies with the call and is not
reported; a move it makes to a global one is a move like any other.
## 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 **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 (`NodeContext.states`, machine id → state id): two crates running the same behavior are in
states of their own, and a behavior run through `Run behavior` starts in no state, as it starts states of their own, and a behavior run through `Run behavior` starts in no state, as it starts
with fresh variables. A **global** one is kept for the run instead (`SharedState.states`, built with fresh variables. 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 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 bag a node touches is decided by the machine's scope, not by the node (`stateBag`). Neither
survives a stop: both start in no state again on the next Play, the way a Variables asset starts 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, 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. a list item (tagged `{"$statemachine": id}`), a port — and a `statemachine` asset is never placed.