Physik: „Raycast all“ – alle Treffer als Liste (eigener Node) #157

Closed
opened 2026-09-19 11:37:23 +02:00 by meleki · 2 comments
Owner

Aus #153 (offener Punkt 2, dort auf „später“ gesetzt; angefordert im Kommentar in #153).

Befund

  • Der Node Raycast (#153) liefert nur den ersten Treffer. Für Durchschuss, Laser durch mehrere Gegner oder „was liegt alles auf der Linie“ braucht es alle Treffer.
  • Die Geometrie ist schon da: rayCast in src/core/properties/ray.ts rechnet pro Form in f64, Solids.raycast geht über dieselben Colliderlisten in Editor-Run und Play und nimmt heute nur den nächsten.
  • Der Graph kennt den Port-Typ list samt Nodes Count, Item at, Sub list, Index of. Einen Datensatz-Typ (ein Treffer mit mehreren Feldern) gibt es nicht.

Zu bauen

  • Eigener Node Raycast all (Action mit Flow), gleiche Eingänge wie Raycast (Start, Richtung, Länge, „Ignore owner“, „Hit triggers“).
  • Ausgänge als parallele Listen, nach Distanz sortiert: objects, colliders, points x, points y, normals x, normals y, distances, dazu count.
  • Pro Collider zählt ein Treffer (der erste Eintritt), eine Tilemap liefert pro getroffener Zelle einen. One-Way-Collider nur von ihrer blockierenden Seite, wie bei Raycast.
  • Tests gegen beide Systeme: Reihenfolge nach Distanz, Collider aus mehreren Polygon-Teilen zählt einmal, Tilemap-Zellen, Start innen.

Offen (mit Vorschlag)

  1. Parallele Listen oder ein Treffer pro Flow-Durchlauf (For-each-artig)? Vorschlag: parallele Listen, das passt zu den vorhandenen Listen-Nodes und braucht keinen neuen Typ.
  2. Obergrenze an Treffern? Vorschlag: Eingang max hits, Vorgabe 0 = alle.
  3. Kollisionsebenen: Filter-Eingang erst mit #152, siehe #158.
Aus #153 (offener Punkt 2, dort auf „später“ gesetzt; angefordert im Kommentar in #153). ## Befund - Der Node **Raycast** (#153) liefert nur den **ersten** Treffer. Für Durchschuss, Laser durch mehrere Gegner oder „was liegt alles auf der Linie“ braucht es alle Treffer. - Die Geometrie ist schon da: `rayCast` in `src/core/properties/ray.ts` rechnet pro Form in f64, `Solids.raycast` geht über dieselben Colliderlisten in Editor-Run und Play und nimmt heute nur den nächsten. - Der Graph kennt den Port-Typ `list` samt Nodes `Count`, `Item at`, `Sub list`, `Index of`. Einen Datensatz-Typ (ein Treffer mit mehreren Feldern) gibt es nicht. ## Zu bauen - Eigener Node **Raycast all** (Action mit Flow), gleiche Eingänge wie Raycast (Start, Richtung, Länge, „Ignore owner“, „Hit triggers“). - Ausgänge als **parallele Listen, nach Distanz sortiert**: objects, colliders, points x, points y, normals x, normals y, distances, dazu `count`. - Pro Collider zählt ein Treffer (der erste Eintritt), eine Tilemap liefert pro getroffener Zelle einen. One-Way-Collider nur von ihrer blockierenden Seite, wie bei Raycast. - Tests gegen beide Systeme: Reihenfolge nach Distanz, Collider aus mehreren Polygon-Teilen zählt einmal, Tilemap-Zellen, Start innen. ## Offen (mit Vorschlag) 1. **Parallele Listen oder ein Treffer pro Flow-Durchlauf (For-each-artig)?** Vorschlag: parallele Listen, das passt zu den vorhandenen Listen-Nodes und braucht keinen neuen Typ. 2. **Obergrenze an Treffern?** Vorschlag: Eingang `max hits`, Vorgabe 0 = alle. 3. **Kollisionsebenen:** Filter-Eingang erst mit #152, siehe #158.
meleki added the new label 2026-09-19 11:37:23 +02:00
admin added this to the Development project 2026-09-19 12:42:48 +02:00
admin removed the new label 2026-09-19 12:42:56 +02:00

vorschläge passen, zu 3. auch klar, zuerst muss es kollisionsebenen überhaupt mal geben bevor wir das umsetzen können

vorschläge passen, zu 3. auch klar, zuerst muss es kollisionsebenen überhaupt mal geben bevor wir das umsetzen können
admin moved this to Next in Development on 2026-09-19 12:49:44 +02:00
meleki referenced this issue from a commit 2026-09-20 18:54:21 +02:00
The new node "Raycast all" answers with every collider along the ray instead of only the nearest one: it carries the same inputs and settings as Raycast (Ignore owner, Hit triggers, Layers from #158) plus "max hits" where 0 means all, and it hands back parallel lists sorted by distance - objects, colliders, points x/y, normals x/y, distances - next to count, so index i of every list speaks about the same hit; a collider counts once no matter how many convex parts its shape is built from, while a tilemap counts per cell it is hit in, and the filter sits in Solids.raycastAll like the one of #158, the one path the editor run and Play both take, so all 12 new unit tests run through describe.each against both systems; the node needed the value system to learn something it could not do - a graph list carried numbers, text, booleans and bundles but no object handles, asList turned an object into its name, so objects/colliders would have been lists of names, which loses the object when two carry the same name, and collider children usually do; Value now knows a travelling list that may hold handles and "Item at" gives such an item out as an object, ready for Destroy, Apply force or Name of, while the single door into a stored file - writeTyped/asTypeOf - runs the list through the new storedList() so every object becomes its name again and nothing living reaches a project file, which is its own unit test over JSON.stringify; three list nodes follow the handles: "Index of" compares objects by identity because a name is not the object of that name, "Set item" and "Fill" write an object as an object, and "Join text" prints the names instead of the innards; this decision was mine to make because the question ran into the MCP timeout - the alternative was a list of names plus "Object by name", which is the smaller change but lossy; 4 new e2e checks in the new block 43e (shield and floor in one go reading 2 and "145, 280", the second object answering to its name, which only a real object in a list can do, "max hits" 1 stopping at the nearest, and the node showing the row) and 18 new unit tests, 12 in raycast.test.ts, 4 on the value system and 2 in the node catalog; two counter-checks hold: not counting tilemap cells one by one drops 2 unit tests, and answering with the nearest hit only drops module 43 with 2 FAILs; one finding on the way - the node read "maxHits" while every other input carries a spoken label, because int() took the key as its label, so it has a third parameter now, and the e2e check caught that, no unit test did (#157)
Author
Owner

Umgesetzt in ffdf0a8, Wiki in 77b6aa1.

Der Node

Raycast all trägt dieselben Eingänge und Einstellungen wie Raycast — Ignore owner, Hit triggers, Layers (aus #158) — dazu max hits, wobei 0 „alle" heißt. Heraus kommen nach Distanz sortierte parallele Listen: objects, colliders, points x, points y, normals x, normals y, distances, dazu count. Index i jeder Liste spricht über denselben Treffer.

  • Ein Collider zählt einmal, egal aus wie vielen konvexen Teilen seine Form gebaut ist.
  • Eine Tilemap zählt pro getroffener Zelle.
  • Der Filter sitzt in Solids.raycastAll — wie der von #158 im einen Weg, den Editor-Run und Play beide gehen.

Eine Entscheidung, die ich selbst getroffen habe

Die Rückfrage ist ins MCP-Timeout gelaufen (300 s), deshalb steht hier, was entschieden wurde und warum.

Eine Graph-Liste konnte Zahlen, Text, Wahrheitswerte und Bündel tragen — keine Objekt-Handles; asList machte aus einem Objekt seinen Namen. objects/colliders wären also Namenslisten geworden, und das ist verlustbehaftet: gleichnamige Objekte sind nicht zu unterscheiden, und Collider-Kinder heißen meistens gleich.

Gebaut ist deshalb: Listen dürfen Objekte tragen.

  • Value kennt eine reisende Liste mit Handles; Item at gibt ein solches Item als Objekt heraus — fertig für Destroy, Apply force, Name of.
  • Die einzige Tür in eine gespeicherte Datei ist writeTyped/asTypeOf. Dort läuft die Liste durch das neue storedList(), aus jedem Objekt wird wieder sein Name. Nichts Lebendiges landet in einer Projektdatei — eigener Unit-Test über JSON.stringify.
  • Handle-sicher nachgezogen: Index of vergleicht Objekte nach Identität (ein Name ist nicht das Objekt dieses Namens), Set item und Fill schreiben ein Objekt als Objekt, Join text druckt die Namen statt des Innenlebens.

Die Alternative wäre eine Namensliste plus Object by name gewesen — kleinerer Eingriff, aber verlustbehaftet.

Tests

./check.sh                       → 121 Files, 1788 Tests, CHECK EXIT 0   (vorher 1770)
npm run build                    → BUILD EXIT 0
node tools/e2e.mjs (alle Module) → ALL PASSED – 1157 checks
GM_STEPS=43 (frisch geseedet)    → ALL PASSED – 17 checks   (vorher 13)

Die volle Suite lief lokal, nicht nur in CI, weil die Änderung am Wertesystem in jedem Graphen steckt.

  • 4 neue e2e-Checks (Block 43e): Schild und Boden in einem Zug (2, 145, 280); das zweite getroffene Objekt antwortet auf seinen Namen — was nur ein echtes Objekt in der Liste kann; max hits 1 hält beim Nächsten an; der Node zeigt die Zeile „max hits" samt derselben Layers-Einstellung.
  • 18 neue Unit-Tests: 12 in raycast.test.ts (je 6 gegen beide Systeme: Reihenfolge und Deckung mit dem einfachen Cast, mehrteiliger Collider zählt einmal, die drei Filter, Tilemap-Zellen, max hits, der Node im Graphen), 4 zum Wertesystem, 2 im Node-Katalog.
  • Zwei Gegenproben, beide greifen: Tilemap-Zellen nicht einzeln zählen → 2 Unit-Tests fallen; nur den nächsten Treffer liefern → Modul 43 fällt mit 2 FAILs (count: 1, kein zweites Objekt). Originale per cmp zurückgeprüft und neu gebaut.
  • Screenshot angesehen (43-ray-all): Node mit Layers und „max hits", Konsole mit 2 / 145, 280 / Query ground / 145.

Drei Funde

  • Am Node stand „maxHits", während jeder andere Eingang eine gesprochene Beschriftung trägt: int() nahm den Schlüssel als Beschriftung und hat jetzt einen dritten Parameter. Gefangen hat das der e2e-Check, kein Unit-Test.
  • Modul 43 ist nicht idempotent. Zweimal hintereinander ohne frisches node tools/seed-e2e.mjs fällt Block 43a (Dropper/Jumper). Liegt nicht an dieser Änderung, steht jetzt in den Projektnotizen.
  • Beim ersten Blick auf den verkleinerten Screenshot habe ich „1" statt „2" gelesen. Nachgemessen (vergrößerter Ausschnitt plus Diagnose-Check im Lauf): der Node meldet durchgehend 2 Treffer, es gab nichts zu beheben.

Doku

Wiki: Colliders (neuer Abschnitt Raycast all; die Zeile „nur der erste Treffer, #157 kommt später" ist raus), Behaviors (der Node in der Liste; der Absatz zu Listen sagt jetzt, was eine reisende Liste darf und was beim Speichern passiert), Physics (offene Punkte: bleibt #154), Architecture (storedList), Development. Check-Summe 1157 in README und Wiki, gemessen.

CI zu ffdf0a8 läuft; darauf wurde auftragsgemäß nicht gewartet.

Umgesetzt in [`ffdf0a8`](http://localhost:3000/meleki/game-maker/commit/ffdf0a8), Wiki in [`77b6aa1`](http://localhost:3000/meleki/game-maker.wiki/commit/77b6aa1). ## Der Node `Raycast all` trägt dieselben Eingänge und Einstellungen wie `Raycast` — Ignore owner, Hit triggers, Layers (aus #158) — dazu **`max hits`**, wobei 0 „alle" heißt. Heraus kommen nach Distanz sortierte **parallele Listen**: `objects`, `colliders`, `points x`, `points y`, `normals x`, `normals y`, `distances`, dazu `count`. Index *i* jeder Liste spricht über denselben Treffer. - Ein Collider zählt **einmal**, egal aus wie vielen konvexen Teilen seine Form gebaut ist. - Eine **Tilemap** zählt **pro getroffener Zelle**. - Der Filter sitzt in `Solids.raycastAll` — wie der von #158 im einen Weg, den Editor-Run und Play beide gehen. ## Eine Entscheidung, die ich selbst getroffen habe Die Rückfrage ist ins MCP-Timeout gelaufen (300 s), deshalb steht hier, was entschieden wurde und warum. Eine Graph-Liste konnte Zahlen, Text, Wahrheitswerte und Bündel tragen — **keine Objekt-Handles**; `asList` machte aus einem Objekt seinen Namen. `objects`/`colliders` wären also Namenslisten geworden, und das ist verlustbehaftet: gleichnamige Objekte sind nicht zu unterscheiden, und Collider-Kinder heißen meistens gleich. Gebaut ist deshalb: **Listen dürfen Objekte tragen.** - `Value` kennt eine **reisende** Liste mit Handles; `Item at` gibt ein solches Item als Objekt heraus — fertig für `Destroy`, `Apply force`, `Name of`. - Die **einzige Tür in eine gespeicherte Datei** ist `writeTyped`/`asTypeOf`. Dort läuft die Liste durch das neue `storedList()`, aus jedem Objekt wird wieder sein Name. Nichts Lebendiges landet in einer Projektdatei — eigener Unit-Test über `JSON.stringify`. - Handle-sicher nachgezogen: `Index of` vergleicht Objekte nach **Identität** (ein Name ist nicht das Objekt dieses Namens), `Set item` und `Fill` schreiben ein Objekt als Objekt, `Join text` druckt die Namen statt des Innenlebens. Die Alternative wäre eine Namensliste plus `Object by name` gewesen — kleinerer Eingriff, aber verlustbehaftet. ## Tests ``` ./check.sh → 121 Files, 1788 Tests, CHECK EXIT 0 (vorher 1770) npm run build → BUILD EXIT 0 node tools/e2e.mjs (alle Module) → ALL PASSED – 1157 checks GM_STEPS=43 (frisch geseedet) → ALL PASSED – 17 checks (vorher 13) ``` Die volle Suite lief **lokal**, nicht nur in CI, weil die Änderung am Wertesystem in jedem Graphen steckt. - **4 neue e2e-Checks** (Block 43e): Schild und Boden in einem Zug (`2`, `145, 280`); das zweite getroffene Objekt antwortet auf seinen Namen — was nur ein echtes Objekt in der Liste kann; `max hits` 1 hält beim Nächsten an; der Node zeigt die Zeile „max hits" samt derselben Layers-Einstellung. - **18 neue Unit-Tests:** 12 in `raycast.test.ts` (je 6 gegen beide Systeme: Reihenfolge und Deckung mit dem einfachen Cast, mehrteiliger Collider zählt einmal, die drei Filter, Tilemap-Zellen, `max hits`, der Node im Graphen), 4 zum Wertesystem, 2 im Node-Katalog. - **Zwei Gegenproben, beide greifen:** Tilemap-Zellen nicht einzeln zählen → 2 Unit-Tests fallen; nur den nächsten Treffer liefern → Modul 43 fällt mit 2 FAILs (`count: 1`, kein zweites Objekt). Originale per `cmp` zurückgeprüft und neu gebaut. - **Screenshot angesehen** (`43-ray-all`): Node mit Layers und „max hits", Konsole mit `2` / `145, 280` / `Query ground` / `145`. ## Drei Funde - **Am Node stand „maxHits"**, während jeder andere Eingang eine gesprochene Beschriftung trägt: `int()` nahm den Schlüssel als Beschriftung und hat jetzt einen dritten Parameter. Gefangen hat das der e2e-Check, kein Unit-Test. - **Modul 43 ist nicht idempotent.** Zweimal hintereinander ohne frisches `node tools/seed-e2e.mjs` fällt Block 43a (Dropper/Jumper). Liegt nicht an dieser Änderung, steht jetzt in den Projektnotizen. - Beim ersten Blick auf den verkleinerten Screenshot habe ich „1" statt „2" gelesen. Nachgemessen (vergrößerter Ausschnitt plus Diagnose-Check im Lauf): der Node meldet durchgehend 2 Treffer, es gab nichts zu beheben. ## Doku Wiki: **Colliders** (neuer Abschnitt *Raycast all*; die Zeile „nur der erste Treffer, #157 kommt später" ist raus), **Behaviors** (der Node in der Liste; der Absatz zu Listen sagt jetzt, was eine reisende Liste darf und was beim Speichern passiert), **Physics** (offene Punkte: bleibt #154), **Architecture** (`storedList`), **Development**. Check-Summe **1157** in README und Wiki, **gemessen**. CI zu `ffdf0a8` läuft; darauf wurde auftragsgemäß nicht gewartet.
admin moved this to Done in Development on 2026-09-20 18:55:36 +02:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#157