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.
105 lines
6.1 KiB
Markdown
105 lines
6.1 KiB
Markdown
## 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** |
|