Document Keep on scene load (#165)

- Scenes: new section on the switch, what travels, the duplicate rules
  and what does not travel. Load scene builds a scene as it is stored.
- Architecture: how a kept entity moves into the next world, and that
  a run keeps what each scene stored before it built it.
- Concepts and Sounds: one sentence each on kept objects.
- Development: the e2e sum is 1625.
admin committed 2026-10-01 17:30:22 +02:00
1 parent 412b94a064
commit b2d413bd60
5 files changed
+86 -11

No files matched your search

+25
@@ -458,6 +458,31 @@ the `SharedState` handed to the next `BehaviorSystem` through `carryOver` — wh
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.
**A kept object is the same entity in the next world (#165).** `GameRun.loadScene` does it in four
steps, and the rules are pure functions in `runtime/keptObjects.ts`. `keptEntities` picks the
top-level entities with the switch on and everything below them. `objectsToBring` copies their
stored objects, parented as the run has them — a released game never writes the run back, so the
scene's own `parentId` may be stale. `bringObjects` puts the copies into the next scene and takes
out what they stand in for (same id, or a kept top-level object of the same asset). Then
`World.instantiate` is handed the entities: when the scene's object for one comes up, the world
takes the entity as it is instead of making one, and only notes what the scene says of it — so the
next `syncToModel` writes the run's values into the scene rather than the scene's replacing them.
The ids the scene left out answer through `findByObjectId` as the entity standing in.
What is not on the entity is handed over next to it, as one `KeptState`:
`BehaviorSystem.release` takes the running instances out before the old world is destroyed — which
is all it takes for them to hear no `On destroy` and keep their sounds — together with the graph
index their interpreters read and, from the physics, how their dynamic bodies move. The next
system `adopt`s them before it starts, so they are no new instances and hear no `On start`; their
trace is cleared and timed by the new world, which starts at second zero. `PhysicsSystem.resume`
gives each body the velocity when it is made, and forgets the rest after the first sync.
**A run keeps what each scene stored before it built it.** `GameRun` copies a scene's objects and
groups the first time it builds it, and puts them back before it builds it again: the run writes
into the scene it is in (`syncToModel`, baking, `spawn`), and a level loaded twice must not start
from what was left of it. `GameRun.end` puts every such scene back, which the player calls on stop
because it plays the project itself; the editor's Play runs on a copy it throws away.
**Where a run begins and which of the two questions Play asks are both project fields.**
`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. `project.playFrom` (the switch)
+1 -1
@@ -25,7 +25,7 @@
| **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). |
| **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. A top-level object with **Keep on scene load** switched on travels into the next scene instead, with everything below it. 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 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. |
+1 -1
@@ -18,7 +18,7 @@ php tests/php/accounts.test.php # the API's accoun
# has and the web server's container has not (SKIP there)
php tests/php/snapshots.test.php # the snapshot store (#231), the same way
php tests/php/releases.test.php # the release store (#266), the same way
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1619 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1625 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
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
+56 -8
@@ -49,11 +49,11 @@ door, a game-over screen. Its `scene` input takes a scene the way `Play sound` t
On collision (Exit) → Load scene (Level 2)
```
**What ends.** Everything of the old scene: its objects, the behaviors on them, and the sounds
those behaviors were playing. That last one is not a rule of its own — a voice belongs to the
behavior that started it (see [Sounds](Sounds)), and those behaviors are destroyed like everything
else in the scene. Music that should carry across a swap is started again in the new scene, or
played **detached**.
**What ends.** Everything of the old scene that is not kept (see below): its objects, the
behaviors on them, and the sounds those behaviors were playing. That last one is not a rule of its
own — a voice belongs to the behavior that started it (see [Sounds](Sounds)), and those behaviors
are destroyed like everything else in the scene. Music that should carry across a swap is played by
a kept object, started again in the new scene, or played **detached**.
**What carries on.** What belongs to the *run* rather than to the scene:
@@ -62,8 +62,8 @@ played **detached**.
- the **clock**: `Time` keeps counting, only a restart puts it back to zero.
A **local** machine's state does not: it belongs to a behavior instance, and that instance is
gone. Nor does any object — there are no persistent objects. Something that has to travel between
scenes travels as a shared variable.
gone — unless its object is kept. An object travels only with **Keep on scene load** switched on;
everything else that has to get from one scene to the next travels as a shared variable.
**When it happens.** Once the frame is over, never mid-flow: the graph that asked runs to its end
first, in the world it is still standing in. Half a frame in the old world and half in the new one
@@ -74,16 +74,64 @@ happens.
picture as a restart, except that the shared variables are kept, which is what makes a life
counter work.
**A scene is always built as it is stored.** Whatever the run did in it on an earlier visit —
moved a box, destroyed a coin, spawned an enemy — is gone when the scene is loaded again, whether
that is the scene it is in or one it comes back to. Until #165 a run in the editor built it from
what it had left behind instead. The same holds for what you edit in a scene *while playing*: such
an edit was never saved, and the next load of that scene does not have it either.
**An id naming no scene** does nothing and says so in the console, rather than tearing down the
world it is standing in half way through.
**In the editor run it is ignored** (a warning in the console). The editor run *is* the scene you
are looking at, brought to life; swapping it would mean editing a scene you are not looking at.
## Keep on scene load
A top-level object with **Keep on scene load** switched on is not destroyed by a `Load scene`. It
moves into the next scene as it is, with everything below it:
- **the same objects**, where the run has them: position, every property and variable. An object
held in a variable somewhere still names it,
- **its behaviors, running on**: no `On destroy` in the old scene and no second `On start` in the
new one, timers, variables and local machines stand where they stood, and a sound such a behavior
plays is not cut off,
- **its animations**, playing on from where they are,
- **how its body moves**: a dynamic rigidbody arrives with the velocity and spin it had.
The switch sits right under *Enabled*, on the asset and on a placed object, so a `Player` asset can
say it once for all its placements. Like every property it can be set while the game runs, with a
`Properties` node whose port is on *Set*.
**Only a top-level object of a scene can be kept.** A child would arrive without the parent it is
placed in, so the inspector shows the switch on top-level objects only, and a value an asset brings
to an object further down counts for nothing. What hangs below a kept object comes along whatever
its own switch says.
**Duplicates: the one that was brought along wins.** A kept object lands in the next scene after
that scene's own objects. Two kinds of the scene's own objects are then not built, each with
everything below it:
- an object with **the same id**. That is the scene the object came from, loaded again: coming back
to level 1 gives one player, not the travelled one plus the stored one,
- a top-level object of **the same asset that is kept itself**. That is what lets every level place
its own `Player` — so each level can be played on its own from the editor — and still hold exactly
one after a swap. An object of that asset that is *not* kept is built as usual: five placed crates
stay five, whatever crate was brought along.
Whatever in the scene named its own object by a reference — an *Object* variable picked in the
inspector, a row of a scene animation — finds the object that came instead. That covers the object
itself, not what hung below the one left out.
**What does not travel.** Collisions are found anew: a kept object that touches something in the
new scene gets an `Enter` for it, also for what it brought along itself. `Ignore one-way` ends with
the scene. The picture of a moving body stands still for the one frame the new scene starts in.
An object the run kept is back where it was stored once the run stops, like everything a run does.
## Restart, stop, load — what each one keeps
| | Objects | Shared variables | Global machines | Clock | Time scale | Pause game | Sounds |
| --- | --- | --- | --- | --- | --- | --- | --- |
| **Load scene** | rebuilt | kept | kept | keeps running | kept | lifted | end with their behavior |
| **Load scene** | rebuilt as stored, kept ones travel | kept | kept | keeps running | kept | lifted | end with their behavior |
| **Restart game** | rebuilt | back to declared | back to their start state | back to 0 | back to 1 | lifted | all end |
| **Stop** (`Esc`) | gone | discarded | discarded | back to 0 | back to 1 | lifted | all end |
+3 -1
@@ -69,7 +69,9 @@ afterwards on its own.
ends when that behavior is destroyed — a sound is something a thing in the world is *doing*, not a
thing of its own. Destroy an enemy and the engine it was running stops with it; no `Stop sound` in
an `On destroy` needed, and none forgotten. The same rule is what ends the sounds of a scene that
[Load scene](Scenes) swaps away: every behavior in it is destroyed.
[Load scene](Scenes) swaps away: every behavior in it is destroyed. A behavior on an object with
*Keep on scene load* is not, so its sound plays on through the swap — the plain way to have music
that does not restart with every level.
**`detached` is the way out, and it exists for one shape.** An object that plays its own death
sound and destroys itself in the same flow would cut that sound off in the instant it started.