Describe objects in structs and lists (#238)

Behaviors gets a paragraph on objects in struct fields and list items. Colliders no longer says a raycast list turns into names. Development counts 1407 e2e checks.
admin committed 2026-09-26 21:12:54 +02:00
1 parent e77a6ac00f
commit 96bc416bc3
3 files changed
+24 -21

No files matched your search

+20 -18
@@ -39,25 +39,26 @@ playing use the same two words.
type as a setting row (*Type: Sprite*, plus *Behavior: Game* under that type). Textures and
scenes are no type here: they are not objects in a running game. A variable is a
**number, a text, a yes/no, a colour, a texture, a state machine, an asset, an object, a list or a struct**; a list declares **nothing** about its items —
each one is a number, a text, a yes/no, a colour, a texture, a state machine or a struct and says so itself, so one list may
each one is a number, a text, a yes/no, a colour, a texture, a state machine, an asset, an object or a struct and says so itself, so one list may
hold several kinds at once. A list is
compared by its items, never by identity — otherwise every reload would look like an override.
There is no **stored** list of objects: a stored list lives in the project file and an object is a
live handle that no reload survives. A list **travelling on an edge** may hold objects, though, and
`Raycast all` (#157) is what hands one out; `Item at` gives such an item back as the object it is.
The moment that list is written into a variable or a property, every object in it becomes its name
— that is the one door into a saved value, and nothing alive goes through it. A set of objects
that has to *survive* is therefore still a list of names, walked with `Repeat` and `Object by name`.
One object on its own **can** be held, by an **object variable**: write the `object` output of
`Spawn` (or of `Object by name`, `Child by name`, a collision) into it, and every later frame
reads the same object back. It lives for the run only. The file stores the declaration and an
empty value, the inspector shows `(null)` and offers nothing to pick, and neither a scene
override, a snapshot nor a save of a persistent Variables asset ever holds the object. The handle
sits beside the variable rather than in it (`core/graph/objectSlots.ts`), since everything in the
variable bag is written back into the scene as an override. A **destroyed** object reads as
`(null)`, as does one from a scene that was restarted or left. "Is it still there?" is `Equals`
with the variable on `a` and `b` left empty: true once the object is gone. Kept variables of a called behavior (#229) keep their object across calls too,
and `Properties` of type *Behavior* reads and writes another behavior's object variable.
**Objects** can be held wherever a value can (#238): an **object variable**, an **object field** of a
struct (`+ Field → Object`) and an **object item** of a list. Write the `object` output of
`Spawn` (or of `Object by name`, `Child by name`, a collision) into one, and every later frame
reads the same object back: `Expand` gives the field back as the object, `Item at` the item.
`Raycast all` (#157) hands out a list of objects, and written into a list variable it stays one.
An object lives for the run only. At rest (in the file, a snapshot, the inspector) its place
always holds the empty object, stored as `{"$object": ""}` wherever it sits, and the inspector and
the List tab show `(null)` and offer nothing to pick. During a run the object sits right in the
value, and every way out of the run empties it again (`core/graph/liveObjects.ts`,
`withoutObjects`): the values the editor run writes back into the scene as overrides, the save of
a persistent Variables asset, the live values the inspector shows and the test hooks. A struct
whose only change is an object therefore writes no override. A list that grew by an object does:
the scene learns the new item, as the empty object. A **destroyed** object reads as `(null)`,
alone as well as out of a list or a struct, as does one from a scene that was restarted or left.
"Is it still there?" is `Equals` with the object on `a` and `b` left empty: true once it is gone.
Kept variables of a called behavior (#229) keep their objects across calls too, and `Properties`
of type *Behavior* reads and writes another behavior's object variable.
A list property is shown as a button saying how many items it holds;
clicking it opens the **List** tab, which gives each item a row with the same widget the
inspector would use for that kind — a struct item is its fields as rows under each other. The inspector stays visible beside the tab, so what the list
@@ -281,7 +282,8 @@ playing use the same two words.
at its declared value, and a value whose kind changed is converted like any other value.
An **object variable** may sit in a persistent asset too, but is never saved: the object is an
instance of that run, and the next run has new ones. It starts every run at `(null)` while the
other values go on, and the inspector says so under the switch.
other values go on, and the inspector says so under the switch. The same goes for an object in a
saved list or struct: the save keeps its place, as the empty object.
- **Cross-object state** the other way: a behavior's own variables *are* the properties of its
object, so another graph reads and writes them with `Object by name` → `Properties` of type
*Behavior*. That reaches one named object, where a Variables asset belongs to no object at all.
+3 -2
@@ -196,8 +196,9 @@ the same settings as Raycast — **Ignore owner**, **Hit triggers**, **Layers**
rather than a run of the flow per hit: that is what `Count`, `Item at` and `Sub list` already work
with.
- `objects` and `colliders` hold **live objects**, not names — `Item at` gives one back as an object,
ready for `Destroy`, `Apply force` or `Name of`. A list only carries them while it travels: written
into a variable or a property, each object becomes its name, because nothing saved is alive.
ready for `Destroy`, `Apply force` or `Name of`. Written into a list variable they stay objects for
the rest of the run; only what leaves the run (a save, the scene) holds the empty object instead,
see [Behaviors](Behaviors).
- **A collider counts once**, however many convex pieces its shape falls into, and the hit is the
first face the ray enters. A **tilemap** counts once per cell the ray goes through, since a map is
many solids under one entity — the `collider` of each of those hits is the tilemap.
+1 -1
@@ -15,7 +15,7 @@ npm run build # writes dist/ and regenerates index.html
php tools/make-user.php <name> <password> [admin|user] # an account; every tool signs in (see Accounts)
php tests/php/accounts.test.php # the API's account rules, no server needed (CI's gate job)
php tests/php/snapshots.test.php # the snapshot store (#231), the same way
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1406 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1407 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