A transition is a named arrow between two states, drawn without pins (#52)

State machines: the asset is no longer "a set of states and nothing more" -- it
holds the moves between them. How a transition is drawn (right-click, Make
transition, click the target), why a state still carries ports it never shows,
what the arrow looks like border to border, and that the inspector's out/in lists
are the only way to delete one. Names need not be unique across a machine, only
among the transitions leaving the same state, and that is exactly what makes one
`State transition` node usable from several states. The node itself is the third
way a behavior reads a machine, beside `State branch` and `Set state`.

Concepts, Home: the summary lines name `State transition` too.

Development: 844 -> 862 checks.
root committed 2026-09-14 14:06:07 +02:00
1 parent 6e524c262e
commit 7b5974b37c
4 files changed
+44 -11

No files matched your search

+1 -1
@@ -3,7 +3,7 @@
| **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`, `shader`, `font`, `particles`) and typed properties. Assets have **no position**. | | **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`, `shader`, `font`, `particles`) and typed properties. Assets have **no position**. |
| **Texture** | An uploaded image. Scenes, textures, animations, state machines, shaders and fonts are the six types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). | | **Texture** | An uploaded image. Scenes, textures, animations, state machines, shaders and fonts are the six types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). |
| **Font** | An uploaded typeface, worn by a text through the ordinary property `font`; no font means the editor's own. Uploaded and picked exactly like a texture, and stored the same way. See [Fonts](Fonts). | | **Font** | An uploaded typeface, worn by a text through the ordinary property `font`; no font means the editor's own. Uploaded and picked exactly like a texture, and stored the same way. See [Fonts](Fonts). |
| **State machine** | A named set of **states**, edited as nodes in its own tab. A behavior puts it into a state with `Set state` and branches on it with `State branch`; each behavior instance keeps its own current state. See [State machines](State-machines). | | **State machine** | A named set of **states**, edited as nodes in its own tab. A behavior puts it into a state with `Set state`, moves it along a named transition with `State transition` and branches on it with `State branch`; each behavior instance keeps its own current state. See [State machines](State-machines). |
| **Shader** | A node graph that becomes the GLSL a sprite is drawn with, edited in its own tab. A sprite wears it through `shader`; the numbers the graph declares are rows below that dropdown. See [Shaders](Shaders). | | **Shader** | A node graph that becomes the GLSL a sprite is drawn with, edited in its own tab. A sprite wears it through `shader`; the numbers the graph declares are rows below that dropdown. See [Shaders](Shaders). |
| **Particles** | A node graph that says what *one* particle is as a function of its age and of a random number drawn once for it — no forces, no simulation. Unlike the other graph assets it **is** placeable: the object made from it is a source, whose rows say how many go out and for how long they live. Because a particle is a formula, the same second always draws the same picture and the tab's slider can jump to any of them exactly. See [Particles](Particles). | | **Particles** | A node graph that says what *one* particle is as a function of its age and of a random number drawn once for it — no forces, no simulation. Unlike the other graph assets it **is** placeable: the object made from it is a source, whose rows say how many go out and for how long they live. Because a particle is a formula, the same second always draws the same picture and the tab's slider can jump to any of them exactly. See [Particles](Particles). |
| **Text** | An asset that draws a line of text (`text`, `font`, `fontSize`, `color`, `align`) — titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. | | **Text** | An asset that draws a line of text (`text`, `font`, `fontSize`, `color`, `align`) — titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. |
+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 # 844 editor checks, screenshots in /tmp/gm-e2e-*.png node tools/seed-e2e.mjs && node tools/e2e.mjs # 862 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
@@ -165,4 +165,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 844 checks as here. WebGL 2.0, and the same 862 checks as here.
+1 -1
@@ -15,7 +15,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
| [Colliders](Colliders) | Collider assets, collision events, blocking movement | | [Colliders](Colliders) | Collider assets, collision events, blocking movement |
| [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 |
| [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, `State branch` and `Set state` | | [State machines](State-machines) | The state machine asset, named transitions, `State branch`, `Set state` and `State transition` |
| [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 |
| [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 |
| [Keyboard](Keyboard) | Every shortcut | | [Keyboard](Keyboard) | Every shortcut |
+40 -7
@@ -1,14 +1,40 @@
A **state machine asset** is a named set of states and nothing more, yet: its graph holds one A **state machine asset** is a named set of states and the moves allowed between them: its graph
`State` node per state (a title and a *Name* setting, no ports) and no edges. Transitions, holds one `State` node per state (a title and a *Name* setting) and one edge per transition. Events
events like "on state entered" and a highlight of the current state while playing are later 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
steps. The states are edited in the **State machine** tab, which is the graph canvas the Behavior canvas the Behavior tab uses — so placing, dragging, the header's **Snap** checkbox, renaming in
tab uses — so placing, dragging, the header's **Snap** checkbox, renaming in the inspector, the inspector, connecting, `Delete` and undo are the graph's.
`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
only selects. Right-click on its canvas offers a `State` and 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. nothing else, named by its number (`State 3` for the third); no behavior palette offers one.
**A behavior reads it through two nodes**, both with a `machine` input that is a picker while ## 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. Two arrows the *same* way round are refused
(`Already connected`), because every transition hangs on the same two ports.
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 — that list is also the only way to delete a transition, since the arrow itself cannot
be clicked. 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 three nodes**, each with a `machine` input that is a picker while
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
@@ -19,6 +45,13 @@ unconnected (or takes a `State machine` node, the literal every kind has):
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
a pulse entered through (`run(…, entry)`); this is the first node to read it. a pulse entered through (`run(…, entry)`); this is the first node to read it.
- **`State transition`** takes 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 its
`machine` input — 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 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 every edge. Deleting one is the awkward case, because the port lives in a *behavior* while the