Describe On fixed update, sound preloading and timed FX changes

- Behaviors: On fixed update in the node list and the event order,
  and a section Fixed update.
- Physics: the order of On fixed update against the engine steps.
- Sounds: sounds are loaded ahead, and Set sound FX parameters has a
  time that ramps on the audio clock, with the sweep as an example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adminandClaude Opus 5.5 committed 2026-10-05 21:47:38 +02:00
1 parent f4de42b406
commit e02851255c
3 files changed
+36 -12

No files matched your search

+13 -3
@@ -212,7 +212,7 @@ the inspector).
- **Keys ignore case**: a letter is the same key with or without Shift or Caps Lock, so a key node
set to `a` also hears `A`, and a key picked with Shift held is stored lower-case. A key is let go
under the name it went down as, even when Shift changes in between (`1` down, `!` up).
- **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 fixed update` (see *Fixed update*), `On disable`, `On destroy` (see *Lifecycle*), `On key down/up/held`,
`On timer` (fires on the first frame past each due time; `late` says by how much, so `time - late` is exactly a multiple of the interval), `On collision enter/stay/exit` (stay every frame, or with an interval; while playing enter and stay tell normal, impulse and point of the contact; on a trigger tile of a tilemap all three tell `tile` and `cell x/y`, once per area or with the input `per cell` once per cell, see [Tilemaps](Tilemaps#trigger-tiles)), `On click` (the mouse pressed on the owner — anywhere when it has none), `On mouse wheel` (the wheel turned over the owner, or anywhere without one; `delta` in notches, positive downwards, a trackpad gives fractions), `On animation finished`, `On state entered`, `On state exited` (one output per state, see *State machines*), `On behavior` (another behavior heard as an event, see *A behavior as an event* below), `Start` · flow `Branch`, `Sequence`, `Repeat`, `For each`, `Find` … `Match`, `Run behavior`, `End`, and the four that move or follow a state machine, `State branch`, `Set state`, `Set state by name`, `State transition` (see *State machines*) · data `Self`, `Owner`, `Parent`, `Object by name`, `Child by name` (the nearest object of that name below the given one: its children, then their children, and so on; within a level the first in hierarchy order, nothing when none is called so), `Name of`,
`Is asset` (is this object an instance of asset X — how you tell what you collided with),
and the plural lookups, each handing on a `list` of objects taken at that moment (destroyed
@@ -643,8 +643,8 @@ the inspector).
Off and on again within one frame is no edge: neither event fires.
- **Event order**: a frame goes event kind by event kind, each across every behavior: `On disable`,
`On enable`, `On key down`, `On key up`, `On key held`, `On timer`, `On collision enter`,
`On collision stay`, `On collision exit`, `On animation finished`, `On click`, `On mouse wheel`
and last `On update`. So a key event of any behavior runs before the first `On update`. The state
`On collision stay`, `On collision exit`, `On animation finished`, `On click`, `On mouse wheel`,
`On fixed update` (as many steps as are due) and last `On update`. So a key event of any behavior runs before the first `On update`. The state
events a kind caused follow right after it. New instances get all their `On start` and then all
their `On enable`, a finished world all `On disable` and then all `On destroy`. Within one kind
every event node has the setting *Order*, a whole number, 0 by default: a lower order runs first,
@@ -652,6 +652,16 @@ the inspector).
hierarchy order, top to bottom as the hierarchy shows it right now (a spawned object stands where
it was put), and within one graph in the order the nodes were made. The node shows `Order: 5`
only when the order is not 0.
- **Fixed update**: `On fixed update` runs 60 times per second of game time, each time with `dt`
exactly 1/60, however long the frames are. A frame at 120 Hz has a step only every other time,
a frame at 30 Hz has two, one after the other, before `On update`. A node inside a step that
reads `dt` (`Time`, `Move`, `Apply force`) gets 1/60 too. So something that counts up or down
in fixed amounts per step (a pitch sweep, a countdown) comes out the same on every run and every
machine, where adding up a frame's `dt` wobbles with the frame rate. It counts its steps by the
same rule as the physics, so it runs exactly as often as the physics steps. The time scale slows
it down like everything else, and while the game is paused it stands still, also for a behavior
that runs while paused. After a hitch it catches up 8 steps at most and drops the rest, as the
physics does. Code: `fixedSteps` in `src/core/time.ts`.
- **Runtime**: `BehaviorSystem` creates one `GraphInterpreter` per behavior entity; graphs only
touch world entities. A runaway pulse throws (see *Loops* for the two limits) and stops play
with a notice.
+4
@@ -71,6 +71,10 @@ fast for a while after. What the game view shows of a dynamic body lies between
so it moves evenly on any display — which also means it is drawn up to one step (1/60 s) behind the
engine.
`On fixed update` (see [Behaviors](Behaviors)) runs exactly as often as the engine steps. It does
not run in between the steps, though: the engine takes all of a frame's steps first, the behaviors
come after. A force set in `On fixed update` therefore acts from the next frame's steps on.
## Collision events
`On collision enter/stay/exit` and `Is touching` come from the engine's contact pairs.
+19 -9
@@ -38,7 +38,7 @@ so what the inspector plays is what the graph will play.
| --- | --- |
| **Play sound** | Starts the sound named by its `sound` input. `volume` **multiplies** the asset's own volume (1 leaves it alone), `loop` is a choice of three (*From the asset*, *On*, *Off*), `detached` says the voice belongs to nobody, `fx` names a [sound FX](#sound-fx) to play it through, and `fx params` takes values for that FX's [parameters](#parameters). Its output `voice` is the number of this one play, for changing it [while it plays](#changing-a-playing-sound); 0 where nothing was played. |
| **Stop sound** | Silences every voice of the sound it names, however many plays started it — a loop included. Naming no sound silences everything the run is playing. |
| **Set sound FX parameters** | Hands new values to the [parameters](#changing-a-playing-sound) of one voice that is already playing, by the `voice` number its `Play sound` gave out. |
| **Set sound FX parameters** | Hands new values to the [parameters](#changing-a-playing-sound) of one voice that is already playing, by the `voice` number its `Play sound` gave out, at once or over `time` seconds. |
| **Sound** | A constant sound, for either of the two to work on through an edge. |
The division is deliberate: the **asset says what a sound is**, the node only colours it. A
@@ -61,10 +61,13 @@ overlapping — two coins picked up half a second apart sound like two coins. Th
sounds as well: playing a loop twice gives two loops, and it is `Stop sound` that ends all of
them at once.
**It arrives late.** The bytes are fetched and decoded on first use, so the very first play of a
sound starts a moment after the node ran. `Play sound` never waits on the network, and a
`Stop sound` that happens while the bytes are still in flight wins: the sound does not start
afterwards on its own.
**It is loaded ahead.** Every sound of the project is fetched and decoded as soon as the project is
open, the way images and fonts are, so the first play starts in the frame its node ran. Decoding
ahead opens no audio device: that only happens on the first play. A sound that is still loading
when it is played — a run started the moment the project opened — starts a moment late instead.
`Play sound` never waits on the network, and a `Stop sound` that happens while the bytes are still
in flight wins: the sound does not start afterwards on its own. Values a graph sends to such a
voice in the meantime are what it starts with.
**A sound belongs to the object making it.** A voice is owned by the behavior that started it, and
ends when that behavior is destroyed — a sound is something a thing in the world is *doing*, not a
@@ -168,15 +171,22 @@ speed, or a loop that gets quieter.
- The number is there at once, before the sound has loaded. A number is never given out twice in
a run, so a kept number of a voice that has ended reaches nothing, never a newer sound.
- A parameter not in the bundle keeps the value it has now, not its default.
- Every value **glides** there in about 0.1 s (`setTargetAtTime`, 30 ms time constant), so a jump
does not click. A value that did not change is not sent again, so running it every frame costs
next to nothing.
- With `time` at 0, every value **glides** there in about 0.1 s (`setTargetAtTime`, 30 ms time
constant), so a jump does not click. A value that did not change is not sent again, so running it
every frame costs next to nothing.
- **`time`** is how many seconds the values take to get there, evenly: a pitch in semitones, a
Gain in dB, a band and a Mix in what they hold. It runs on the clock of the sound itself, not on
the game's, so a sweep sounds the same every time, also the first. For a sweep, set it once
right after `Play sound`: `On enable` → `Play sound` → `Set sound FX parameters` (`voice` from
`Play sound`, `pitch` -64, `time` 1.28) falls by 50 semitones a second. Counting a value down
in `On update` or `On fixed update` instead runs on game time, and the sound drifts away from it.
- A new change starts from where the voice is right now; whatever was still under way is dropped.
- **A pitch changes by speed here.** A pitched copy cannot be worked out again in the middle of a
sound, so the voice plays faster or slower to reach the new pitch: it gets that much shorter or
longer. On a loop that is not heard; the next play gets its own length again.
- A sound that has ended and the `voice` 0 of a play that played nothing are not reached. A
detached voice is reached like any other. A voice still waiting for its bytes starts with the
new values.
new values, and a change with a `time` starts with the voice, so the whole of it is heard.
**In the game** the `fx` input of `Play sound` names the asset. An FX that does not compile, or
one that was deleted, plays the sound **dry** rather than not at all: a broken effect should not