diff --git a/Architecture.md b/Architecture.md index ca94c6f..cd9dd6e 100644 --- a/Architecture.md +++ b/Architecture.md @@ -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) diff --git a/Concepts.md b/Concepts.md index 84ee3be..f9e1afb 100644 --- a/Concepts.md +++ b/Concepts.md @@ -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. | diff --git a/Development.md b/Development.md index 2b5354e..2fae18e 100644 --- a/Development.md +++ b/Development.md @@ -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 diff --git a/Scenes.md b/Scenes.md index 314cdf4..e35e3c6 100644 --- a/Scenes.md +++ b/Scenes.md @@ -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 | diff --git a/Sounds.md b/Sounds.md index 6052afc..929abea 100644 --- a/Sounds.md +++ b/Sounds.md @@ -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.