From e6849977eb93634547cf4254a8ebd43a6244d821 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Tue, 15 Sep 2026 13:24:35 +0200 Subject: [PATCH] A game that is more than one scene needs a page about scenes Scenes.md explains what a scene swap keeps (the shared state, so global variables and global machines) and what it throws away, why the run starts either from the start scene or from the open one, and why the sounds of the old scene stop: their behaviors are gone, which is also why Play sound learned detached. --- Architecture.md | 25 ++++++++++++++ Behaviors.md | 6 ++-- Concepts.md | 12 ++++--- Development.md | 8 ++++- Home.md | 1 + Scenes.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ Sounds.md | 14 +++++++- _Sidebar.md | 1 + 8 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 Scenes.md diff --git a/Architecture.md b/Architecture.md index d5c6c14..d400826 100644 --- a/Architecture.md +++ b/Architecture.md @@ -177,6 +177,31 @@ browser the way it is free of Pixi, lets a test run a world that plays into a re a device, and is the reason a run started before anything wired a sink in plays silently rather than crashing. +**A voice has an owner, so nobody has to remember to stop it.** `AudioSink.play` takes the id of +the behavior that asked, and destroying that behavior silences its voices — the reaping pass in +`BehaviorSystem` does it right after `On destroy`, so a graph still gets its last word. That single +rule is also what ends the sounds of a scene a `Load scene` swaps away, without the swap knowing +anything about audio. `detached` gives the ownership up on purpose, for the death sound of an object +that destroys itself; an empty owner is deliberately *nobody*, never a group `stopOwner('')` could +silence by accident. Cancellation is counted rather than flagged (per file, per owner, and for +"everything"), because a play whose bytes are still in flight has to know whether a stop happened +*after it was asked for* — a flag cannot tell that from one that happened long before. + +**A scene swap is a new world inside the same run.** `Load scene` sets a flag on the world +(`loadSceneRequested`) the way `Stop game` and `Restart game` do, and the editor acts on it once +the tick is over: half a frame in each of two worlds is undebuggable. What survives is one object — +the `SharedState` handed to the next `BehaviorSystem` through `carryOver` — which is exactly the +line between "belongs to the run" and "belongs to the scene". Carrying it over also suppresses the +entry event of the global machines, because standing where you already stand is not a move. + +**Where a run begins is a project field, which of the two questions Play asks is a browser +setting.** `project.startSceneId` is content — it travels with the project, and a dangling id reads +as none rather than being cleaned up, so undoing a deletion brings it back. The switch next to Play +lives in `editor/settings.ts` (`localStorage`, best effort: a browser that refuses it gets the +defaults and keeps working), because "I am working on level 4 this afternoon" is not something the +project should carry into anyone else's checkout. The new field needed no format bump: a document +without it reads as "none named", which is precisely what older projects meant. + **A predicate named after what a thing looks like will be asked what a thing *is*.** `isSymbolType` reads as "draws an icon", but four callers use it for "is editor decoration": the running world, the Game tab, the draw order and the upright placement all key off it. Giving a particle source an icon diff --git a/Behaviors.md b/Behaviors.md index 8bdc257..0fac3c5 100644 --- a/Behaviors.md +++ b/Behaviors.md @@ -99,7 +99,7 @@ shown on the node and editable in the inspector. - **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`, `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), - `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`/`State machine`/`Sound`, `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`/`State machine`/`Sound`/`Scene`, `Add/Subtract/Multiply/Divide`, `Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`, `Substring`, `Find text`, `Equals`, lists `List` (a constant), `Fill` (one value repeated — how an empty board is made), `Count`, @@ -112,7 +112,9 @@ shown on the node and editable in the inspector. (asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Emit` (starts a particle source over, see *Particles*), `Play sound` / `Stop sound` (see [Sounds](Sounds): the asset says how loud and whether it loops, the node's `volume` multiplies that and its `loop` either leaves it alone or overrules it in either - direction; every play is a voice of its own, so two in a row overlap), `Log`, `Stop game`, `Restart game`. A node type is one file exporting a `NodeDef` + direction; every play is a voice of its own, so two in a row overlap, and the voice ends with the + behavior that started it unless `detached` says otherwise), + `Load scene` (swaps the running scene once the frame is over — see [Scenes](Scenes)), `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 inspector fields are generated from it. A port may declare a `default` for its inline value (e.g. `Move.scale = 1`), shown on the node and in the inspector. diff --git a/Concepts.md b/Concepts.md index 6de6dc2..3d5efa3 100644 --- a/Concepts.md +++ b/Concepts.md @@ -20,8 +20,10 @@ | **Default** | Every property has a schema default. Asset values that differ from it are marked gold in the inspector and can be reset (↺) the same way an override can. The asset's name in the **Assets** panel carries the same gold while any of them does (`assetDeviates`, `src/core/properties/deviation.ts` — the inspector marks the rows of that same schema, so the list cannot claim a deviation the inspector then fails to show). A behavior's variables are not in it: their default is declared on the variable itself, so the asset value cannot differ from it. | | **Instance** | An object resolved from a placed container's template. Its **values** can be edited — position, name, property overrides — and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is the **template as the containers in between leave it**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. | | **Unpack** | Writes a placed container's content into the scene as ordinary objects (hierarchy context menu, or the inspector's *Content* section). The ids survive it, so the selection does; from then on the structure is editable and the objects no longer follow the template. | +| **Start scene** | The scene a run *begins* with, named on the project rather than on any asset (right-click a scene in the Assets panel → *Set as start scene*, `▶` marks it in the list; *Clear start scene* takes it back). It is a different question from which scene is **active**, which is the one you have open in the editor — the switch next to Play says which of the two Play asks. Without a start scene named, both are the same thing. See [Scenes](Scenes). | +| **Load scene** | The node that swaps the running scene for another one, inside the same run. Everything of the old scene is destroyed — its objects, their behaviors, and the sounds those behaviors were playing — while what belongs to the **run** carries on: the values of the Variables assets, the states of the global machines, and the clock. See [Scenes](Scenes). | | **Edit mode** | The Scene tab draws the project model; panels, grid, gizmos and drag-to-move are active. | -| **Play mode** | The editor switches to a **runtime copy** of the project (same ids). A `World` is instantiated from its active scene (one entity per scene object, resolved properties as mutable state) and shown in the **Game** tab. Systems tick it and mirror their changes into the copy; every panel stays open and edits the copy, which the world picks up immediately. **Stop discards world and copy** — the saved model is never touched. | +| **Play mode** | The editor switches to a **runtime copy** of the project (same ids). A `World` is instantiated from the scene the switch next to Play names — the start scene, else the active one (one entity per scene object, resolved properties as mutable state) and shown in the **Game** tab. Systems tick it and mirror their changes into the copy; every panel stays open and edits the copy, which the world picks up immediately. **Stop discards world and copy** — the saved model is never touched. | **The camera defines what is rendered.** A camera asset has a view size (`width` x `height`, default 640x360) and a `zoom`; the frame it covers in world units is `size / zoom`. The Scene @@ -35,7 +37,9 @@ no movement): everything that happens is defined by behavior graphs. **Toolbar and panels.** The toolbar holds the **Project** menu (new project, switch project, *Delete this project…*), the project name (type to rename), the **Scene** picker for the active -scene, Undo/Redo, Play/Pause, and on the right the notice line, the save status and the frame +scene — usable while a run is on, because it only moves the editor's view and never the running +world — Undo/Redo, the **start-scene switch** (*Start scene* / *Active scene*, see **Start +scene**), Play/Pause, and on the right the notice line, the save status and the frame rate. The **Assets** panel's `+` creates an asset and opens its tab; its menu lists the types in two alphabetical groups, first what can be placed into a scene, then what only lives in the library (#43) — the line between them answers the question that follows right after "which type?". @@ -44,8 +48,8 @@ selects (the inspector shows it, no tab moves); a **double-click** opens the tab in — Scene, Behavior, State machine, Shader, Particles, Texture, Animation or Container — and on a scene also makes it the active one. `Enter` does the same once the list has keyboard focus (`Tab`). A sprite, camera, text or collider has no tab of its own, so a double-click on one does nothing. -Right-clicking a row offers *Set as active scene*, *Create sprite from texture*, *Create animation -from texture* and *Delete…*. In +Right-clicking a row offers *Set as active scene*, *Set as start scene* / *Clear start scene*, +*Create sprite from texture*, *Create animation from texture* and *Delete…*. In **Scene objects** it offers *Select asset*, *Add collider*, *Create container*, *Unpack container*, *Unparent*, *Expand all* / *Collapse all* and *Remove*. diff --git a/Development.md b/Development.md index db8a0b1..5d62544 100644 --- a/Development.md +++ b/Development.md @@ -64,7 +64,7 @@ The e2e run is a list of steps in `tools/e2e/` (hierarchy, inspector, play, asse graph, parenting, colliders, containers, text/enabled, menus, textures, frames, overflow, aspect, container tab, run-in-editor, pause, nested behaviors, key repeat, list tab, struct variable, variable nodes, property nodes, animations, state machines, console, shaders, transforms, fonts, -particles, shared scope, render on change, sounds) that build on each other's +particles, shared scope, render on change, sounds, load scene) that build on each other's scene state and share the CDP driver in `tools/lib/harness.mjs` — `press`, `undo`, `play`, `object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it exercises. @@ -107,6 +107,12 @@ and loop flag) is the witness, for the reason `renderStats` is one. The seed's t audio file the host and the CI image are guaranteed to share, and the bytes are a pure function of the arguments, so storage stays content-addressed and a check can name a file by its exact size. +**The one step that must clean up after itself twice.** `35-load-scene` makes a second scene and +plays *from* it, so it leaves behind both assets and a project field. Its own scene is where all of +its objects live, which keeps the shared fixture scene untouched; the start scene it names is +cleared at the end, because a step that leaves one behind would silently redirect every Play of +every step after it. + **A check that demands an exact number must be handed the number, not something it can be derived from.** The particle preview used to hold second 0.5 by computing the clock reading that *would* mean 0.5 since the field started; the round trip through floating point misses by the last bit about one diff --git a/Home.md b/Home.md index 359ff39..a600c7d 100644 --- a/Home.md +++ b/Home.md @@ -10,6 +10,7 @@ Live: · Widget playground: