Ich hätte gerne für Behavior Variablen und State Machine Zustand die möglichkeit einen Scope festzulegen.
Bei variablen scope: global (= projektweit), scene (= fast global, aber nur innerhalb der scene), object (oder andere passender name = behaviors vom selben objekt haben zugriff drauf), lokal (oder anderer passender name = nur das eigene behavior - ich glaube das ist der stand jetzt)
bei statemachine scope: ähnlich, lass uns das durchplanen
soweit so gut, gibt es noch andere stellen die wir beachten müssen? grundsätzlich erstmal durchplanen vor umsetzung - genau rückfragen stellen. danach wenn alles klar ist => umsetzen.
Ich hätte gerne für Behavior Variablen und State Machine Zustand die möglichkeit einen Scope festzulegen.
Bei variablen scope: global (= projektweit), scene (= fast global, aber nur innerhalb der scene), object (oder andere passender name = behaviors vom selben objekt haben zugriff drauf), lokal (oder anderer passender name = nur das eigene behavior - ich glaube das ist der stand jetzt)
bei statemachine scope: ähnlich, lass uns das durchplanen
soweit so gut, gibt es noch andere stellen die wir beachten müssen? grundsätzlich erstmal durchplanen vor umsetzung - genau rückfragen stellen. danach wenn alles klar ist => umsetzen.
Bevor wir entscheiden — was heute tatsaechlich da ist:
Ebene
Heute
Deklaration
BehaviorAsset.variables: PropertyDef[] (core/model/Asset.ts:31) — das Property-Schema des Behaviors. Default an der Deklaration und in asset.props.
Laufzeit
entity.props — es gibt keinen eigenen Variablenspeicher. BehaviorSystem.ts:335: variables: entity.props. Key = Variablenname, flach, ein Bag pro Behavior-Entity.
Zugriff eigen
Variables-Node, read: (ctx) => ctx.variables. Keine Namensaufloesung zur Laufzeit — der Port-Key ist der Bag-Key.
Zugriff fremd
Properties-Node Typ Behavior: asEntity(input('object'))?.props (nodes/data/Properties.ts:30).
Instance.states: Record<machineId, stateId>, pro Behavior-Instanz (BehaviorSystem.ts:43).
Projekt-/Szenenebene
Nichts.NodeContext kennt kein globals; eine Szene hat genau eine Property (backgroundColor).
Der heutige Stand ist also lokal — genauer: pro Behavior-Instanz = pro Szenenobjekt. Und dieser
Satz Variablen ist zugleich das Property-Schema des Objekts. Genau daran haengt alles Weitere.
Andere Stellen, die mitbetroffen sind
Zu deiner Frage, ob es noch welche gibt — ja, sechs:
Instanz-Override. Variablen erscheinen am platzierten Objekt im Inspector und sind pro Instanz
ueberschreibbar (objectSection.ts, bei Container-Inhalten zusaetzlich instanceEdits). Bei einer
geteilten Variable ist das bedeutungslos — die Zeile darf dort nicht aussehen wie heute.
World.syncToModel (runtime/World.ts:184) schreibt zur Laufzeit abgewichene Werte als Override
zurueck. Im Play harmlos (Wegwerf-Kopie), im Editor-Run (runInEditor) ist es eine echte
Projektaenderung mit commit(). Eine globale Variable hat kein Objekt, an dem der Wert haengt.
Properties-Node. Der heutige Weg zu fremdem Zustand liest entity.props. Wandert eine Variable
in einen geteilten Bag, ist sie dort nicht mehr zu sehen — das Beispielspiel baut seinen Muenzzaehler
genau so (Object by name -> Properties).
Run behavior. Nested Calls bekommen frische Variablen. Geteilte muessten sie sehen — eine
Semantikaenderung, die in #59 schon als Schmerzpunkt steht.
Persistenz.normalizeVariables (core/store/serialize.ts:238) baut die Deklaration beim Laden
neu auf. Ein neues Feld scope braucht dort eine eigene Zeile, sonst ueberlebt es das Speichern,
aber nicht das Laden (dieselbe Falle wie zuletzt bei GraphEdge.label in #52).
Szenen-Scope ist heute nicht messbar. Ein Projekt hat mehrere Szenen, aber genau eine activeSceneId, und ein Lauf kennt genau eine Szene. Ohne Load scene (#39) ist scene zur
Laufzeit nicht von global unterscheidbar — die Wahl waere baubar, aber nicht pruefbar.
Offene Entscheidungen
A — Deklarationsort und Namenskollision (die Hauptweiche)
Heisst global, dass der Name der Schluessel ist, teilen sich zwei Behaviors mit je einer Variablen score automatisch eine. Folge: ein Tippfehler ist eine stille zweite Variable, und bei
widersprechendem Typ oder Default muss jemand entscheiden, welcher gilt.
A1 Deklaration bleibt am Behavior, jede Variable bekommt ein Feld scope; der Name ist der
geteilte Schluessel. Wenig UI. Der Editor muesste Konflikte melden.
A2 Eigener Deklarationsort je Scope: globale Variablen am Projekt, Szenen-Variablen an der Szene,
Objekt-Variablen am Objekt; das Behavior benutzt sie nur. Eindeutig, keine Kollision — kostet je
eine Inspector-Liste und eine zweite Quelle fuer die Ports des Variables-Node.
B — Wohin legt sich der Objekt-Scope?
Nicht nach owner.props — dort liegen x, y, texture, und syncToModel schriebe sie zurueck.
Es braeuchte einen eigenen Bag an der Owner-Entity, nach dem Vorbild von Entity.animation / Entity.emits ("kept out of props, which the world writes back").
Und: ein Behavior kann gar keinen Owner haben (parent: Entity | null; das "click anywhere"-Behavior
ist genau so gebaut). Worauf faellt object dann zurueck — auf lokal, oder ist es ein Fehler?
C — Lebensdauer und Reset
Gilt global fuer die Dauer eines Laufs (beim Play-Start zurueck auf Default), oder soll es einen
Lauf ueberdauern und gespeichert werden? Ersteres ist der kleine Schritt; Letzteres waere der Anfang
eines Savegames.
D — State-Machine-Scope: wo steht er?
Vorschlag: am Maschinen-Asset, nicht pro Node. Eine Maschine ist ein Asset mit einer Id, und der
Zustand haengt schon heute an dieser Id (states[machineId]). Ein Feld scope am StateMachineAsset
ist ein Wort im Modell und macht #59 ueberfluessig. Die Alternative — Scope pro Set state-Node —
liesse dieselbe Maschine an zwei Stellen verschieden wohnen.
E — Reihenfolge
Der State-Machine-Scope ist der kleinere, sofort messbare Schritt (ein Feld am Asset, ein Bag mehr im BehaviorSystem, NodeContext.states bekommt eine zweite Quelle). Der Variablen-Scope zieht Inspector,
Overrides, Properties-Node und Persistenz mit. Sollen wir sie trennen — erst D, dann A?
Kein Umbau, bevor A bis E entschieden sind.
## Ist-Stand, gemessen am Code
Bevor wir entscheiden — was heute tatsaechlich da ist:
| Ebene | Heute |
|---|---|
| Deklaration | `BehaviorAsset.variables: PropertyDef[]` (`core/model/Asset.ts:31`) — das Property-Schema des Behaviors. Default an der Deklaration *und* in `asset.props`. |
| Laufzeit | **`entity.props`** — es gibt keinen eigenen Variablenspeicher. `BehaviorSystem.ts:335`: `variables: entity.props`. Key = Variablenname, flach, ein Bag pro Behavior-Entity. |
| Zugriff eigen | `Variables`-Node, `read: (ctx) => ctx.variables`. **Keine Namensaufloesung zur Laufzeit** — der Port-Key *ist* der Bag-Key. |
| Zugriff fremd | `Properties`-Node Typ *Behavior*: `asEntity(input('object'))?.props` (`nodes/data/Properties.ts:30`). |
| `Run behavior` | `{ ...ctx, variables: { ...program.defaults }, states: {} }` (`GraphInterpreter.ts:139`) — frisch, teilt nichts, behaelt nichts. |
| State | `Instance.states: Record<machineId, stateId>`, pro Behavior-Instanz (`BehaviorSystem.ts:43`). |
| Projekt-/Szenenebene | **Nichts.** `NodeContext` kennt kein `globals`; eine Szene hat genau eine Property (`backgroundColor`). |
Der heutige Stand ist also `lokal` — genauer: **pro Behavior-Instanz = pro Szenenobjekt**. Und dieser
Satz Variablen *ist* zugleich das Property-Schema des Objekts. Genau daran haengt alles Weitere.
## Andere Stellen, die mitbetroffen sind
Zu deiner Frage, ob es noch welche gibt — ja, sechs:
1. **Instanz-Override.** Variablen erscheinen am platzierten Objekt im Inspector und sind pro Instanz
ueberschreibbar (`objectSection.ts`, bei Container-Inhalten zusaetzlich `instanceEdits`). Bei einer
geteilten Variable ist das bedeutungslos — die Zeile darf dort nicht aussehen wie heute.
2. **`World.syncToModel`** (`runtime/World.ts:184`) schreibt zur Laufzeit abgewichene Werte als Override
zurueck. Im Play harmlos (Wegwerf-Kopie), im **Editor-Run (`runInEditor`) ist es eine echte
Projektaenderung mit `commit()`**. Eine globale Variable hat kein Objekt, an dem der Wert haengt.
3. **`Properties`-Node.** Der heutige Weg zu fremdem Zustand liest `entity.props`. Wandert eine Variable
in einen geteilten Bag, ist sie dort nicht mehr zu sehen — das Beispielspiel baut seinen Muenzzaehler
genau so (`Object by name` -> `Properties`).
4. **`Run behavior`.** Nested Calls bekommen frische Variablen. Geteilte muessten sie sehen — eine
Semantikaenderung, die in #59 schon als Schmerzpunkt steht.
5. **Persistenz.** `normalizeVariables` (`core/store/serialize.ts:238`) baut die Deklaration beim Laden
neu auf. Ein neues Feld `scope` braucht dort eine eigene Zeile, sonst ueberlebt es das Speichern,
aber nicht das Laden (dieselbe Falle wie zuletzt bei `GraphEdge.label` in #52).
6. **Szenen-Scope ist heute nicht messbar.** Ein Projekt hat mehrere Szenen, aber genau eine
`activeSceneId`, und ein Lauf kennt genau eine Szene. Ohne `Load scene` (#39) ist `scene` zur
Laufzeit **nicht von `global` unterscheidbar** — die Wahl waere baubar, aber nicht pruefbar.
## Offene Entscheidungen
### A — Deklarationsort und Namenskollision (die Hauptweiche)
Heisst `global`, dass der **Name** der Schluessel ist, teilen sich zwei Behaviors mit je einer Variablen
`score` automatisch eine. Folge: ein Tippfehler ist eine stille zweite Variable, und bei
widersprechendem Typ oder Default muss jemand entscheiden, welcher gilt.
- **A1** Deklaration bleibt am Behavior, jede Variable bekommt ein Feld `scope`; der Name ist der
geteilte Schluessel. Wenig UI. Der Editor muesste Konflikte melden.
- **A2** Eigener Deklarationsort je Scope: globale Variablen am Projekt, Szenen-Variablen an der Szene,
Objekt-Variablen am Objekt; das Behavior *benutzt* sie nur. Eindeutig, keine Kollision — kostet je
eine Inspector-Liste und eine zweite Quelle fuer die Ports des `Variables`-Node.
### B — Wohin legt sich der Objekt-Scope?
**Nicht** nach `owner.props` — dort liegen `x`, `y`, `texture`, und `syncToModel` schriebe sie zurueck.
Es braeuchte einen eigenen Bag an der Owner-Entity, nach dem Vorbild von `Entity.animation` /
`Entity.emits` (*"kept out of `props`, which the world writes back"*).
Und: **ein Behavior kann gar keinen Owner haben** (`parent: Entity | null`; das "click anywhere"-Behavior
ist genau so gebaut). Worauf faellt `object` dann zurueck — auf lokal, oder ist es ein Fehler?
### C — Lebensdauer und Reset
Gilt `global` fuer die Dauer **eines Laufs** (beim Play-Start zurueck auf Default), oder soll es einen
Lauf ueberdauern und gespeichert werden? Ersteres ist der kleine Schritt; Letzteres waere der Anfang
eines Savegames.
### D — State-Machine-Scope: wo steht er?
Vorschlag: **am Maschinen-Asset**, nicht pro Node. Eine Maschine ist ein Asset mit einer Id, und der
Zustand haengt schon heute an dieser Id (`states[machineId]`). Ein Feld `scope` am `StateMachineAsset`
ist ein Wort im Modell und macht **#59** ueberfluessig. Die Alternative — Scope pro `Set state`-Node —
liesse dieselbe Maschine an zwei Stellen verschieden wohnen.
### E — Reihenfolge
Der State-Machine-Scope ist der kleinere, sofort messbare Schritt (ein Feld am Asset, ein Bag mehr im
`BehaviorSystem`, `NodeContext.states` bekommt eine zweite Quelle). Der Variablen-Scope zieht Inspector,
Overrides, `Properties`-Node und Persistenz mit. Sollen wir sie trennen — erst D, dann A?
---
Kein Umbau, bevor A bis E entschieden sind.
Die Planungsfragen aus dem Kommentar oben sind durchgesprochen. Das ist der Stand, gegen den gebaut wird:
Frage
Entscheidung
A
Deklarationsort und Schlüssel
Deklaration bleibt am Behavior, neues Feld scope an der Variablen-Deklaration. Der Name ist der geteilte Schlüssel — zwei Behaviors mit je einer globalen score teilen sich eine.
A2
Typ-/Default-Konflikt
Typkonflikt = Fehler (blockiert den Lauf, rot). Default-Konflikt = Warnung (gelb), die erste Deklaration gewinnt.
B
Scope object
Wird nicht gebaut. Der Objektfall bleibt über Object by name → Properties lösbar; kommt als eigenes Issue zurück, wenn er wehtut.
G
Scope scene
Wird nicht gebaut. Ohne Load scene (#39) ist er zur Laufzeit nicht von global unterscheidbar — also erst mit #39, dann ist er messbar.
C
Lebensdauer
Pro Lauf. Bei jedem Play-Start frisch aus den Defaults, beim Stop weg. Nichts wird zurückgeschrieben — auch im Editor-Run nicht.
E
Objekt-Inspector
Eine globale Variable verschwindet aus dem Inspector des platzierten Objekts. Vorhandene Instanz-Overrides fallen beim Umstellen weg.
F
Zugriff im Graphen
Der bestehende Variables-Node bleibt transparent: er zeigt weiterhin alle Variablen des Behaviors und leitet intern in den richtigen Bag um. Bestehende Graphen brechen beim Umstellen nicht.
D
State-Machine-Scope
Feld scope am StateMachineAsset (nicht pro Set state-Node). Der Zustand hängt schon heute an der Asset-Id. Schließt #59 mit.
Was daraus folgt
Das Feld hat genau zwei Werte, local (heute) und global. Es entstehen genau zwei neue Speicher — ein laufweiter Variablen-Bag und ein laufweiter States-Bag — beide beim Play-Start frisch.
Drei Punkte leite ich daraus ab, ohne dass sie eigens entschieden wurden:
Der globale States-Bag folgt derselben Regel wie C: Startzustand bei jedem Play-Start.
Run behavior sieht die globalen Bags; nested Calls bekommen weiterhin frische lokale Variablen.
normalizeVariables braucht eine Zeile für scope, sonst überlebt das Feld das Speichern, aber nicht das Laden — dieselbe Falle, die bei GraphEdge.label in #52 zugeschnappt wäre.
Nicht Teil dieser Runde
object- und scene-Scope (siehe B und G) sowie jede Form von Persistenz über den Lauf hinaus (C) — Letzteres wäre der Anfang eines Savegames und gehört in ein eigenes Issue.
## Entschieden — Frage/Antwort-Runde vom 2026-09-14
Die Planungsfragen aus dem Kommentar oben sind durchgesprochen. Das ist der Stand, gegen den gebaut wird:
| | Frage | Entscheidung |
|---|---|---|
| **A** | Deklarationsort und Schlüssel | Deklaration bleibt am Behavior, neues Feld `scope` an der Variablen-Deklaration. **Der Name ist der geteilte Schlüssel** — zwei Behaviors mit je einer globalen `score` teilen sich eine. |
| **A2** | Typ-/Default-Konflikt | **Typkonflikt = Fehler** (blockiert den Lauf, rot). **Default-Konflikt = Warnung** (gelb), die erste Deklaration gewinnt. |
| **B** | Scope `object` | **Wird nicht gebaut.** Der Objektfall bleibt über `Object by name` → `Properties` lösbar; kommt als eigenes Issue zurück, wenn er wehtut. |
| **G** | Scope `scene` | **Wird nicht gebaut.** Ohne `Load scene` (#39) ist er zur Laufzeit nicht von `global` unterscheidbar — also erst mit #39, dann ist er messbar. |
| **C** | Lebensdauer | **Pro Lauf.** Bei jedem Play-Start frisch aus den Defaults, beim Stop weg. **Nichts wird zurückgeschrieben — auch im Editor-Run nicht.** |
| **E** | Objekt-Inspector | Eine globale Variable **verschwindet** aus dem Inspector des platzierten Objekts. Vorhandene Instanz-Overrides fallen beim Umstellen weg. |
| **F** | Zugriff im Graphen | Der bestehende `Variables`-Node bleibt **transparent**: er zeigt weiterhin alle Variablen des Behaviors und leitet intern in den richtigen Bag um. Bestehende Graphen brechen beim Umstellen nicht. |
| **D** | State-Machine-Scope | Feld `scope` am **`StateMachineAsset`** (nicht pro `Set state`-Node). Der Zustand hängt schon heute an der Asset-Id. **Schließt #59 mit.** |
### Was daraus folgt
Das Feld hat genau zwei Werte, `local` (heute) und `global`. Es entstehen genau **zwei** neue Speicher — ein laufweiter Variablen-Bag und ein laufweiter States-Bag — beide beim Play-Start frisch.
Drei Punkte leite ich daraus ab, ohne dass sie eigens entschieden wurden:
- Der globale States-Bag folgt derselben Regel wie **C**: Startzustand bei jedem Play-Start.
- `Run behavior` sieht die globalen Bags; nested Calls bekommen weiterhin frische **lokale** Variablen.
- `normalizeVariables` braucht eine Zeile für `scope`, sonst überlebt das Feld das Speichern, aber nicht das Laden — dieselbe Falle, die bei `GraphEdge.label` in #52 zugeschnappt wäre.
### Nicht Teil dieser Runde
`object`- und `scene`-Scope (siehe B und G) sowie jede Form von Persistenz über den Lauf hinaus (**C**) — Letzteres wäre der Anfang eines Savegames und gehört in ein eigenes Issue.
Entscheidung A ist verworfen. Der Scope sitzt nicht mehr auf der Deklaration; es gibt PropertyDef.scope nicht mehr. Ausloeser war die Frage, ob eine global gesetzte Variable in
einem anderen Behavior am Variables-Node vorgeschlagen wird. Gemessen: nein — die
Vorschlagsliste kam aus asset.variables des eigenen Behaviors, ohne Freitext-Ausweg.
Teilnehmen hiess damit: den Namen in jedem Behavior noch einmal deklarieren, mit Tippfehler
als stiller zweiter Variable und ohne jede Uebersicht, welche geteilten Werte ein Projekt hat.
Was stattdessen gebaut ist
Ein eigener Asset-Typ Variables: Deklarationen und sonst nichts — kein Graph, keine
Position, nie platziert, kein eigener Tab. Der Variables-Node waehlt eines ueber die neue
Einstellung variablesAsset (leer = eigene Variablen wie bisher) und liest/schreibt dann einen Wert pro Schluessel fuer den ganzen Lauf. Das Muster ist geborgt, nicht erfunden: Set state waehlt seine Maschine genauso.
Damit faellt weg: globalConflicts samt Lauf-Stopp bei Kind-Konflikt (A2 wird
gegenstandslos), SetVariableScopeCommand, der Scope-Picker, beide Objekt-Inspector-Filter.
Eine Behavior-Variable ist ausnahmslos wieder eine Objekt-Property. Unterm Strich 72 Zeilen
weniger Code. Zwei Variables-Assets duerfen denselben Namen fuehren, ohne sich zu treffen —
das Asset ist der Schluessel, nicht der Name.
Entscheidung C bleibt: Der Bag entsteht beim Play-Start aus den Deklarationen und wird nie
zurueckgeschrieben — auch der Editor-Run veraendert das Projekt nicht. B und G (Scope object/scene) sind weiterhin bewusst nicht gebaut.
Nachgezogen: volle Symmetrie zum Inspector
Waehrend eines Laufs zeigte der Inspector den Startwert aus dem Dokument, und eine Aenderung
dort wirkte erst beim naechsten Play. Gecacht war dabei nie etwas (GraphInterpreter legt nur
Knoten ohneread in den Pulse-Cache, der Variablen-Knoten hat eines) — es fehlte die
Leitung. Jetzt zwei benannte: Inspector.liveVariables zeigt die laufenden Werte und faellt
beim Ende des Laufs von selbst auf die Deklaration zurueck; adoptDeclarations() uebernimmt
einen Schluessel nur dort, wo das Dokument ihn seit dem letzten Abgleich geaendert hat —
ohne diese Bedingung reisst jede beliebige Projektaenderung den laufenden Zaehler auf den
Startwert zurueck.
StateMachineAsset.scope steht wie gebaut, Scope als Typ traegt jetzt nur noch die
Maschinen. Nachgebessert wurde nur die Darstellung: die Scope-Zeile war handgebaut (eigenes HStack, Ghost-Dropdown) und begann ~34 px weiter links als jeder Wert darueber — sie geht
jetzt durch createFieldRow wie jede andere enum-Zeile, die Erklaerungen neben Local/Global sind aus dem Menue raus.
Zwei Funde nebenbei
"Run in editor" ueberlebte kein Speichern — deserializeProject coercte Behaviors gegen [...COMMON_PROPS, ...variables], wo runInEditor nicht drinsteht. Eigener Commit 9027a86, weil es nichts mit diesem Issue zu tun hat.
Die Vorschlagsliste wurde doppelt berechnet (nodeSection und portSection) — die
Gegenprobe dazu biss zuerst nicht, die eigentliche Anforderung hing allein am Browser-Test.
Jetzt eine Funktion, variableChoices, mit eigenem Unit-Test.
Belege
./check.sh
CHECK EXIT 0, 999 Tests in 78 Dateien
e2e Modul 32
ALL PASSED, dazu Module 4/19/20/21/31 gegen den Umbau
Gegenproben
6, jede beisst isoliert an ihrer eigenen Wache
Commits
d1fcfe8 (erster Entwurf, Scope an der Deklaration) → 8143130 (Umbau auf das Asset)
Wiki: Behaviors (Shared state neu geschrieben), Concepts (Glossar-Eintrag), Architecture, State machines (#59).
## Umgesetzt — aber mit gekippter Entscheidung A
**Entscheidung A ist verworfen.** Der Scope sitzt nicht mehr auf der Deklaration; es gibt
`PropertyDef.scope` nicht mehr. Ausloeser war die Frage, ob eine global gesetzte Variable in
einem *anderen* Behavior am `Variables`-Node vorgeschlagen wird. Gemessen: nein — die
Vorschlagsliste kam aus `asset.variables` des eigenen Behaviors, ohne Freitext-Ausweg.
Teilnehmen hiess damit: den Namen in jedem Behavior noch einmal deklarieren, mit Tippfehler
als stiller zweiter Variable und ohne jede Uebersicht, welche geteilten Werte ein Projekt hat.
### Was stattdessen gebaut ist
Ein eigener Asset-Typ **Variables**: Deklarationen und sonst nichts — kein Graph, keine
Position, nie platziert, kein eigener Tab. Der `Variables`-Node waehlt eines ueber die neue
Einstellung `variablesAsset` (leer = eigene Variablen wie bisher) und liest/schreibt dann
**einen Wert pro Schluessel fuer den ganzen Lauf**. Das Muster ist geborgt, nicht erfunden:
`Set state` waehlt seine Maschine genauso.
Damit faellt weg: `globalConflicts` samt Lauf-Stopp bei Kind-Konflikt (**A2** wird
gegenstandslos), `SetVariableScopeCommand`, der Scope-Picker, beide Objekt-Inspector-Filter.
Eine Behavior-Variable ist ausnahmslos wieder eine Objekt-Property. Unterm Strich **72 Zeilen
weniger Code**. Zwei Variables-Assets duerfen denselben Namen fuehren, ohne sich zu treffen —
das Asset ist der Schluessel, nicht der Name.
**Entscheidung C bleibt:** Der Bag entsteht beim Play-Start aus den Deklarationen und wird nie
zurueckgeschrieben — auch der Editor-Run veraendert das Projekt nicht. **B und G** (Scope
`object`/`scene`) sind weiterhin bewusst nicht gebaut.
### Nachgezogen: volle Symmetrie zum Inspector
Waehrend eines Laufs zeigte der Inspector den Startwert aus dem Dokument, und eine Aenderung
dort wirkte erst beim naechsten Play. Gecacht war dabei nie etwas (`GraphInterpreter` legt nur
Knoten **ohne** `read` in den Pulse-Cache, der Variablen-Knoten hat eines) — es fehlte die
Leitung. Jetzt zwei benannte: `Inspector.liveVariables` zeigt die laufenden Werte und faellt
beim Ende des Laufs von selbst auf die Deklaration zurueck; `adoptDeclarations()` uebernimmt
einen Schluessel **nur dort, wo das Dokument ihn seit dem letzten Abgleich geaendert hat** —
ohne diese Bedingung reisst jede beliebige Projektaenderung den laufenden Zaehler auf den
Startwert zurueck.
### D / #59 — unveraendert
`StateMachineAsset.scope` steht wie gebaut, `Scope` als Typ traegt jetzt nur noch die
Maschinen. Nachgebessert wurde nur die Darstellung: die Scope-Zeile war handgebaut (eigenes
`HStack`, Ghost-Dropdown) und begann ~34 px weiter links als jeder Wert darueber — sie geht
jetzt durch `createFieldRow` wie jede andere `enum`-Zeile, die Erklaerungen neben
`Local`/`Global` sind aus dem Menue raus.
### Zwei Funde nebenbei
1. **"Run in editor" ueberlebte kein Speichern** — `deserializeProject` coercte Behaviors gegen
`[...COMMON_PROPS, ...variables]`, wo `runInEditor` nicht drinsteht. Eigener Commit
`9027a86`, weil es nichts mit diesem Issue zu tun hat.
2. **Die Vorschlagsliste wurde doppelt berechnet** (`nodeSection` und `portSection`) — die
Gegenprobe dazu biss zuerst nicht, die eigentliche Anforderung hing allein am Browser-Test.
Jetzt eine Funktion, `variableChoices`, mit eigenem Unit-Test.
### Belege
| | |
|---|---|
| `./check.sh` | `CHECK EXIT 0`, **999 Tests** in 78 Dateien |
| e2e Modul 32 | `ALL PASSED`, dazu Module 4/19/20/21/31 gegen den Umbau |
| Gegenproben | 6, jede beisst isoliert an ihrer eigenen Wache |
| Commits | `d1fcfe8` (erster Entwurf, Scope an der Deklaration) → `8143130` (Umbau auf das Asset) |
Wiki: *Behaviors* (Shared state neu geschrieben), *Concepts* (Glossar-Eintrag), *Architecture*,
*State machines* (#59).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Ich hätte gerne für Behavior Variablen und State Machine Zustand die möglichkeit einen Scope festzulegen.
Bei variablen scope: global (= projektweit), scene (= fast global, aber nur innerhalb der scene), object (oder andere passender name = behaviors vom selben objekt haben zugriff drauf), lokal (oder anderer passender name = nur das eigene behavior - ich glaube das ist der stand jetzt)
bei statemachine scope: ähnlich, lass uns das durchplanen
soweit so gut, gibt es noch andere stellen die wir beachten müssen? grundsätzlich erstmal durchplanen vor umsetzung - genau rückfragen stellen. danach wenn alles klar ist => umsetzen.
Ist-Stand, gemessen am Code
Bevor wir entscheiden — was heute tatsaechlich da ist:
BehaviorAsset.variables: PropertyDef[](core/model/Asset.ts:31) — das Property-Schema des Behaviors. Default an der Deklaration und inasset.props.entity.props— es gibt keinen eigenen Variablenspeicher.BehaviorSystem.ts:335:variables: entity.props. Key = Variablenname, flach, ein Bag pro Behavior-Entity.Variables-Node,read: (ctx) => ctx.variables. Keine Namensaufloesung zur Laufzeit — der Port-Key ist der Bag-Key.Properties-Node Typ Behavior:asEntity(input('object'))?.props(nodes/data/Properties.ts:30).Run behavior{ ...ctx, variables: { ...program.defaults }, states: {} }(GraphInterpreter.ts:139) — frisch, teilt nichts, behaelt nichts.Instance.states: Record<machineId, stateId>, pro Behavior-Instanz (BehaviorSystem.ts:43).NodeContextkennt keinglobals; eine Szene hat genau eine Property (backgroundColor).Der heutige Stand ist also
lokal— genauer: pro Behavior-Instanz = pro Szenenobjekt. Und dieserSatz Variablen ist zugleich das Property-Schema des Objekts. Genau daran haengt alles Weitere.
Andere Stellen, die mitbetroffen sind
Zu deiner Frage, ob es noch welche gibt — ja, sechs:
ueberschreibbar (
objectSection.ts, bei Container-Inhalten zusaetzlichinstanceEdits). Bei einergeteilten Variable ist das bedeutungslos — die Zeile darf dort nicht aussehen wie heute.
World.syncToModel(runtime/World.ts:184) schreibt zur Laufzeit abgewichene Werte als Overridezurueck. Im Play harmlos (Wegwerf-Kopie), im Editor-Run (
runInEditor) ist es eine echteProjektaenderung mit
commit(). Eine globale Variable hat kein Objekt, an dem der Wert haengt.Properties-Node. Der heutige Weg zu fremdem Zustand liestentity.props. Wandert eine Variablein einen geteilten Bag, ist sie dort nicht mehr zu sehen — das Beispielspiel baut seinen Muenzzaehler
genau so (
Object by name->Properties).Run behavior. Nested Calls bekommen frische Variablen. Geteilte muessten sie sehen — eineSemantikaenderung, die in #59 schon als Schmerzpunkt steht.
normalizeVariables(core/store/serialize.ts:238) baut die Deklaration beim Ladenneu auf. Ein neues Feld
scopebraucht dort eine eigene Zeile, sonst ueberlebt es das Speichern,aber nicht das Laden (dieselbe Falle wie zuletzt bei
GraphEdge.labelin #52).activeSceneId, und ein Lauf kennt genau eine Szene. OhneLoad scene(#39) istscenezurLaufzeit nicht von
globalunterscheidbar — die Wahl waere baubar, aber nicht pruefbar.Offene Entscheidungen
A — Deklarationsort und Namenskollision (die Hauptweiche)
Heisst
global, dass der Name der Schluessel ist, teilen sich zwei Behaviors mit je einer Variablenscoreautomatisch eine. Folge: ein Tippfehler ist eine stille zweite Variable, und beiwidersprechendem Typ oder Default muss jemand entscheiden, welcher gilt.
scope; der Name ist dergeteilte Schluessel. Wenig UI. Der Editor muesste Konflikte melden.
Objekt-Variablen am Objekt; das Behavior benutzt sie nur. Eindeutig, keine Kollision — kostet je
eine Inspector-Liste und eine zweite Quelle fuer die Ports des
Variables-Node.B — Wohin legt sich der Objekt-Scope?
Nicht nach
owner.props— dort liegenx,y,texture, undsyncToModelschriebe sie zurueck.Es braeuchte einen eigenen Bag an der Owner-Entity, nach dem Vorbild von
Entity.animation/Entity.emits("kept out ofprops, which the world writes back").Und: ein Behavior kann gar keinen Owner haben (
parent: Entity | null; das "click anywhere"-Behaviorist genau so gebaut). Worauf faellt
objectdann zurueck — auf lokal, oder ist es ein Fehler?C — Lebensdauer und Reset
Gilt
globalfuer die Dauer eines Laufs (beim Play-Start zurueck auf Default), oder soll es einenLauf ueberdauern und gespeichert werden? Ersteres ist der kleine Schritt; Letzteres waere der Anfang
eines Savegames.
D — State-Machine-Scope: wo steht er?
Vorschlag: am Maschinen-Asset, nicht pro Node. Eine Maschine ist ein Asset mit einer Id, und der
Zustand haengt schon heute an dieser Id (
states[machineId]). Ein FeldscopeamStateMachineAssetist ein Wort im Modell und macht #59 ueberfluessig. Die Alternative — Scope pro
Set state-Node —liesse dieselbe Maschine an zwei Stellen verschieden wohnen.
E — Reihenfolge
Der State-Machine-Scope ist der kleinere, sofort messbare Schritt (ein Feld am Asset, ein Bag mehr im
BehaviorSystem,NodeContext.statesbekommt eine zweite Quelle). Der Variablen-Scope zieht Inspector,Overrides,
Properties-Node und Persistenz mit. Sollen wir sie trennen — erst D, dann A?Kein Umbau, bevor A bis E entschieden sind.
Entschieden — Frage/Antwort-Runde vom 2026-09-14
Die Planungsfragen aus dem Kommentar oben sind durchgesprochen. Das ist der Stand, gegen den gebaut wird:
scopean der Variablen-Deklaration. Der Name ist der geteilte Schlüssel — zwei Behaviors mit je einer globalenscoreteilen sich eine.objectObject by name→Propertieslösbar; kommt als eigenes Issue zurück, wenn er wehtut.sceneLoad scene(#39) ist er zur Laufzeit nicht vonglobalunterscheidbar — also erst mit #39, dann ist er messbar.Variables-Node bleibt transparent: er zeigt weiterhin alle Variablen des Behaviors und leitet intern in den richtigen Bag um. Bestehende Graphen brechen beim Umstellen nicht.scopeamStateMachineAsset(nicht proSet state-Node). Der Zustand hängt schon heute an der Asset-Id. Schließt #59 mit.Was daraus folgt
Das Feld hat genau zwei Werte,
local(heute) undglobal. Es entstehen genau zwei neue Speicher — ein laufweiter Variablen-Bag und ein laufweiter States-Bag — beide beim Play-Start frisch.Drei Punkte leite ich daraus ab, ohne dass sie eigens entschieden wurden:
Run behaviorsieht die globalen Bags; nested Calls bekommen weiterhin frische lokale Variablen.normalizeVariablesbraucht eine Zeile fürscope, sonst überlebt das Feld das Speichern, aber nicht das Laden — dieselbe Falle, die beiGraphEdge.labelin #52 zugeschnappt wäre.Nicht Teil dieser Runde
object- undscene-Scope (siehe B und G) sowie jede Form von Persistenz über den Lauf hinaus (C) — Letzteres wäre der Anfang eines Savegames und gehört in ein eigenes Issue.Umgesetzt — aber mit gekippter Entscheidung A
Entscheidung A ist verworfen. Der Scope sitzt nicht mehr auf der Deklaration; es gibt
PropertyDef.scopenicht mehr. Ausloeser war die Frage, ob eine global gesetzte Variable ineinem anderen Behavior am
Variables-Node vorgeschlagen wird. Gemessen: nein — dieVorschlagsliste kam aus
asset.variablesdes eigenen Behaviors, ohne Freitext-Ausweg.Teilnehmen hiess damit: den Namen in jedem Behavior noch einmal deklarieren, mit Tippfehler
als stiller zweiter Variable und ohne jede Uebersicht, welche geteilten Werte ein Projekt hat.
Was stattdessen gebaut ist
Ein eigener Asset-Typ Variables: Deklarationen und sonst nichts — kein Graph, keine
Position, nie platziert, kein eigener Tab. Der
Variables-Node waehlt eines ueber die neueEinstellung
variablesAsset(leer = eigene Variablen wie bisher) und liest/schreibt danneinen Wert pro Schluessel fuer den ganzen Lauf. Das Muster ist geborgt, nicht erfunden:
Set statewaehlt seine Maschine genauso.Damit faellt weg:
globalConflictssamt Lauf-Stopp bei Kind-Konflikt (A2 wirdgegenstandslos),
SetVariableScopeCommand, der Scope-Picker, beide Objekt-Inspector-Filter.Eine Behavior-Variable ist ausnahmslos wieder eine Objekt-Property. Unterm Strich 72 Zeilen
weniger Code. Zwei Variables-Assets duerfen denselben Namen fuehren, ohne sich zu treffen —
das Asset ist der Schluessel, nicht der Name.
Entscheidung C bleibt: Der Bag entsteht beim Play-Start aus den Deklarationen und wird nie
zurueckgeschrieben — auch der Editor-Run veraendert das Projekt nicht. B und G (Scope
object/scene) sind weiterhin bewusst nicht gebaut.Nachgezogen: volle Symmetrie zum Inspector
Waehrend eines Laufs zeigte der Inspector den Startwert aus dem Dokument, und eine Aenderung
dort wirkte erst beim naechsten Play. Gecacht war dabei nie etwas (
GraphInterpreterlegt nurKnoten ohne
readin den Pulse-Cache, der Variablen-Knoten hat eines) — es fehlte dieLeitung. Jetzt zwei benannte:
Inspector.liveVariableszeigt die laufenden Werte und faelltbeim Ende des Laufs von selbst auf die Deklaration zurueck;
adoptDeclarations()uebernimmteinen Schluessel nur dort, wo das Dokument ihn seit dem letzten Abgleich geaendert hat —
ohne diese Bedingung reisst jede beliebige Projektaenderung den laufenden Zaehler auf den
Startwert zurueck.
D / #59 — unveraendert
StateMachineAsset.scopesteht wie gebaut,Scopeals Typ traegt jetzt nur noch dieMaschinen. Nachgebessert wurde nur die Darstellung: die Scope-Zeile war handgebaut (eigenes
HStack, Ghost-Dropdown) und begann ~34 px weiter links als jeder Wert darueber — sie gehtjetzt durch
createFieldRowwie jede andereenum-Zeile, die Erklaerungen nebenLocal/Globalsind aus dem Menue raus.Zwei Funde nebenbei
deserializeProjectcoercte Behaviors gegen[...COMMON_PROPS, ...variables], worunInEditornicht drinsteht. Eigener Commit9027a86, weil es nichts mit diesem Issue zu tun hat.nodeSectionundportSection) — dieGegenprobe dazu biss zuerst nicht, die eigentliche Anforderung hing allein am Browser-Test.
Jetzt eine Funktion,
variableChoices, mit eigenem Unit-Test.Belege
./check.shCHECK EXIT 0, 999 Tests in 78 DateienALL PASSED, dazu Module 4/19/20/21/31 gegen den Umbaud1fcfe8(erster Entwurf, Scope an der Deklaration) →8143130(Umbau auf das Asset)Wiki: Behaviors (Shared state neu geschrieben), Concepts (Glossar-Eintrag), Architecture,
State machines (#59).