Files
feuerwerk/todo-notes/done/nested-behavior.md
T
root 6702ff8abe Let a behavior be a node in another graph
A behavior that has a Start and an End node can now be dropped into any
other graph as a node of its own. What is declared on the start node
becomes the inputs of that node, what hangs on the end node its outputs;
the port list is derived from the sub graph, not copied, so declaring a
port in one graph grows the node in the other without touching it. The
node carries the behavior's name and the palette lists behaviors by
name, so a call is one grab instead of a node plus a dropdown.

An embedded behavior has no object and therefore no place for
variables: every call starts on the asset defaults and communicates
only through its ports. That is the conservative direction - state can
still be added later, and shared state already has a home in a behavior
object in the scene. Node ids are unique per graph only, so each call
gets its own pulse cache; without that, two graphs would collide.

Renaming a port used to silently drop the value typed on the calling
node. It is carried along now, project wide, like the edges.
2026-09-09 20:01:49 +02:00

105 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Nested Behavior / Behavior als Node in Graph
Was damit gemeint ist?
=> Ein Behavior Asset als Node innerhalb eines Graph
Grundsätzlich soll so ein Behavior Node im Graf mit einem Execution Flow ausgestattet sein, sprich wir brauchen einen Start/End Node:
- Start Node ist ähnlich wie ein Event Node mit exec output port und dort soll man auch ouput "variablen" festlegen können (quasi wie variablen)
- End Node ähnlich, hat als Input ein exec port + inputs die als output gelten, wenn man es als node in einen anderen graphen eines behavior assets einbindet.
wenn alles verstanden, direkt loslegen, ansonsten warte ich auf rückfragen :)
---
## Umgesetzt
Verstanden war alles — gebaut ist genau die beschriebene Form. Drei neue Node-Typen:
| | |
|---|---|
| **`Start`** | exec-Ausgang plus die Eingaenge, die das Behavior von aussen bekommt. Was hier deklariert wird, ist ein **Eingang des aufrufenden Nodes** |
| **`End`** | exec-Eingang plus die Werte, die zurueckgehen. Was hier deklariert wird, ist ein **Ausgang des aufrufenden Nodes** |
| **`Run behavior`** | der Aufruf selbst. Seine Ports gibt es nicht in seiner Typdefinition — sie werden aus Start- und End-Node des Ziels **abgeleitet**, bei jedem Zeichnen neu |
Deklariert wird im Inspector des Start-/End-Nodes: `+ Input` bzw. `+ Output`, Art waehlbar
(Number, Boolean, Text, Object), Name direkt editierbar, `×` zum Entfernen — dieselbe Bauform
wie die Variablen eines Behaviors.
**Der Node traegt den Namen des Behaviors**, nicht "Run behavior", und die Palette listet
Behaviors ebenfalls namentlich. Ein Aufruf ist damit ein Griff statt Node einfuegen + Dropdown.
## Die eine Entscheidung, die du mir ueberlassen hast
Ein eingebettetes Behavior hat kein Objekt in der Szene — also auch keinen Ort fuer seine
Variablen. Ich habe **(A) reine Funktion** gewaehlt: jeder Aufruf startet die Variablen auf den
Asset-Defaults, rein geht es ueber den Start-Node, raus ueber den End-Node.
Der Grund ist dein eigener naechster Schritt: du willst den Tetris-Graphen zerlegen. Dabei
sollen die Teile *rechnen*, waehrend der Zustand (`grid`, `pieceX`, …) im Haupt-Behavior
bleibt — genau das, was du mit "kann man ueber input/output loesen" gemeint hast. (A) ist
ausserdem die konservative Wahl: daraus laesst sich spaeter Zustand machen, umgekehrt nicht,
ohne bestehende Graphen zu brechen.
**Die Tuer ist nicht zu:** Zustand gibt es weiterhin ueber ein Behavior-Objekt in der Szene
(`Object by name` → `Get/Set property`) — der Weg, den dein Jump and Run fuer den Punktestand
schon geht. Lokale Variablen im eingebetteten Graphen funktionieren als Zwischenspeicher
innerhalb eines Aufrufs unveraendert; sie werden nur am Ende verworfen.
## Was ich selbst entschieden habe
| | |
|---|---|
| **Ein Start, ein End** | ein zweiter machte "welche Ports hat dieses Behavior" mehrdeutig. Der Command lehnt ihn ab, die Palette bietet ihn nicht mehr an |
| **Selbstaufruf = Fehler** | mit Namen im Log (`Behavior "X" runs itself`), statt still in die Tiefenbegrenzung zu laufen. Die Palette bietet das eigene Behavior gar nicht erst an |
| **`self`/`owner` bleiben** | der eingebettete Graph sieht dasselbe Objekt wie der Aufrufer — sonst waeren `Self`/`Owner` darin wertlos |
| **Toter Verweis ist still** | ein Node ohne gewaehltes Behavior (oder eines ohne Start-Node) tut nichts und **laesst den Fluss weiterlaufen**, statt den Lauf abzubrechen |
## Zwei Dinge, die der Bau erzwungen hat
**Eigener Pulse-Cache pro Aufruf.** Node-Ids sind nur *pro Graph* eindeutig. Liefe der
eingebettete Graph im Cache des Aufrufers, koennte er dessen Werte ueberschreiben. Der
Interpreter arbeitet deshalb auf **Frames** — der Aufruf schiebt einen neuen auf und nimmt ihn
danach wieder weg. Dafuer gibt es einen eigenen Test, der eine Id-Kollision erzwingt; ohne
Frames liefert er 99 statt 7.
**Ports aufraeumen ueber Graphgrenzen.** Einen Port umzubenennen oder zu entfernen betrifft
nicht nur den eigenen Graphen, sondern **jeden anderen, der das Behavior aufruft**. Beides
raeumt dort mit auf — und ein Rename nimmt den eingetippten Inline-Wert mit, sonst faellt er
still auf den Default zurueck. Das ist mir erst beim Gegenpruefen aufgefallen: der Test stand
auf 0 statt auf 6.
**Live-Highlight:** der Trace eines laufenden Behaviors deckt jetzt auch die Graphen ab, die es
als Node aufruft — sonst bliebe ein eingebettetes Behavior beim Zusehen dunkel.
## Belegt
**Zehn Unit-Tests** (`tests/core/nested-behavior.test.ts`) und **elf E2E-Checks** in Modul 19.
Der staerkste ist der erste: das aufrufende Behavior rechnet **nichts** selbst — die
Geschwindigkeit, mit der die Kiste laeuft, *ist* der zurueckgegebene Wert.
```
PASS a behavior used as a node runs, and what it returns is what drives the caller
– moved 200.0 px in ~1 s – 2 × 100 means the returned value arrived, 0 would mean nothing came back
PASS the calling node grows the ports the other behavior declares
PASS and it is drawn under that behavior's name, not as "Run behavior" – title="Double"
PASS adding an input declares it on the start node – [{"key":"in1",…},{"key":"in2",…}]
PASS and the node in the other graph grew that input without being touched
PASS undo removes it in both graphs – [{"key":"in1","kind":"number"}]
PASS picking a behavior in the palette drops in a node already set to it
– {"type":"RunBehavior","params":{"behavior":"nb-double"}}
```
`200.0` statt `100` beweist, dass der **verdoppelte** Wert ankam, nicht bloss irgendeiner;
`0` waere "nichts kam zurueck". Die drei Port-Checks in Folge sind der Nachweis, dass die
Portliste wirklich *abgeleitet* und nicht kopiert ist: deklariert wird im einen Graphen,
gewachsen ist der Node im anderen, ohne dass er angefasst wurde.
Gegenprobe gefahren: ohne die frische Variablenkopie und ohne die Selbstaufruf-Sperre fallen
zwei Unit-Tests; ohne getrennte Frames faellt der Kollisionstest (`expected 99 to be 7`) — der
war beim ersten Versuch faelschlich gruen und musste geschaerft werden, bevor er etwas taugte.
| | |
|---|---|
| volle E2E-Suite, remote | **ALL PASSED – 379 Checks**, 0 FAIL |
| `npm run check` lokal | gruen, **524 Tests** |