Texture: Kollisions-Polygon pro Frame, genutzt von der Tilemap (Kollision v3) #141

Closed
opened 2026-09-18 09:48:25 +02:00 by meleki · 2 comments
Owner

Folge-Issue zu #132 (Stufe 3 der Tilemap-Kollision). Stufe 2, die Kollisionsgröße pro Tile, steht in #140.

Befund

  • Kollision kennt nur zwei Formen: box und circle (ColliderShape, src/core/properties/collider.ts:15). Überlappung (shapesOverlap) und Anhalten an der Wand (exitDistance) sind genau für diese Paare geschrieben: Box/Box über getrennte Achsen, Kreis/Kreis, Box/Kreis.
  • Im Texture-Tab legt man Frames als Rechtecke an, dazu gibt es pro Frame die Nine-Slice-Ränder (border, fill an TextureFrame, src/core/model/Asset.ts:80). Eine Kollisionsform am Frame gibt es nicht.
  • Eine Tilemap-Zelle blockiert seit #132 immer als Rechteck. Schräge Kanten, runde Felsen oder ein Baumstamm im Tile sind so nicht abzubilden.

Zu bauen

  • Neue Ebene im Texture-Tab: „Collision" neben den Frames. Ist sie aktiv, zeichnet man für den ausgewählten Frame ein Polygon: Punkte setzen, verschieben, einfügen (Klick auf eine Kante), löschen. Jeder Schritt ist ein Undo-Schritt. Die Punkte liegen in Pixeln relativ zum Frame, so wandern sie mit, wenn der Frame verschoben wird.
  • Das Polygon wird mit dem Frame gespeichert und beim Laden wie die übrigen Frame-Felder nachsichtig gelesen (kaputte Punkte fallen weg, zu wenige Punkte = kein Polygon).
  • Neuer Formtyp polygon im CollisionSystem, mit Überlappung und exitDistance gegen Box, Kreis und Polygon. Konkave Polygone werden intern in konvexe Teile zerlegt.
  • Tilemap: Hat das Bild eines Solid-Tiles ein Polygon, blockiert die Zelle mit diesem Polygon statt mit der ganzen Zelle. Zellen ohne Polygon werden weiter zu Rechtecken zusammengefasst wie bisher.
  • Scene-View: Die Polygone werden wie die übrigen Kollisionsformen umrandet.
  • Tests: Unit-Tests für die neue Form (Überlappung, Anhalten, Gleiten an einer Schräge, konkav zerlegt), für Speichern und Laden, für die Polygon-Kommandos mit Undo. e2e: Polygon im Texture-Tab zeichnen, eine Figur hält an der Schräge.

Offen (mit Vorschlag)

  • Welche Tiles? Ein Floor- oder Wall-Tile setzt sich aus Viertelkacheln eines ganzen Blocks zusammen, dort gehört ein Polygon zu keinem einzelnen Frame. Vorschlag: Polygone gelten zuerst nur für None (1 × 1)-Tiles, deren Bild ein Frame ist. Autotiles blockieren weiter als Rechteck.
  • Animierte Tiles: Jedes Bild ist ein Frame und könnte ein eigenes Polygon haben. Vorschlag: es zählt das Polygon des ersten Bildes. Eine Kollision, die mit der Animation zuckt, ist selten gewollt.
  • Verhältnis zu #140: Vorschlag: Die Kollisionsgröße skaliert auch das Polygon, um die Zellmitte. Polygone werden nicht mit Nachbarn verschmolzen, jede Zelle bringt ihr eigenes mit.
  • Auch für platzierte Sprites? Ein Objekt mit diesem Frame könnte dasselbe Polygon als Collider nutzen (Collider-Form „Frame polygon"). Vorschlag: nicht in diesem Issue, eigenes Folge-Issue, wenn gewünscht.
  • Konvex erzwingen statt zerlegen? Einfacher, aber für Felsen und Bäume zu eng. Vorschlag: zerlegen, selbstüberschneidende Polygone werden abgelehnt und im Tab rot markiert.
Folge-Issue zu #132 (Stufe 3 der Tilemap-Kollision). Stufe 2, die Kollisionsgröße pro Tile, steht in #140. ## Befund - Kollision kennt nur zwei Formen: `box` und `circle` (`ColliderShape`, `src/core/properties/collider.ts:15`). Überlappung (`shapesOverlap`) und Anhalten an der Wand (`exitDistance`) sind genau für diese Paare geschrieben: Box/Box über getrennte Achsen, Kreis/Kreis, Box/Kreis. - Im Texture-Tab legt man Frames als Rechtecke an, dazu gibt es pro Frame die Nine-Slice-Ränder (`border`, `fill` an `TextureFrame`, `src/core/model/Asset.ts:80`). Eine Kollisionsform am Frame gibt es nicht. - Eine Tilemap-Zelle blockiert seit #132 immer als Rechteck. Schräge Kanten, runde Felsen oder ein Baumstamm im Tile sind so nicht abzubilden. ## Zu bauen - **Neue Ebene im Texture-Tab: „Collision"** neben den Frames. Ist sie aktiv, zeichnet man für den ausgewählten Frame ein **Polygon**: Punkte setzen, verschieben, einfügen (Klick auf eine Kante), löschen. Jeder Schritt ist ein Undo-Schritt. Die Punkte liegen in Pixeln relativ zum Frame, so wandern sie mit, wenn der Frame verschoben wird. - Das Polygon wird mit dem Frame gespeichert und beim Laden wie die übrigen Frame-Felder nachsichtig gelesen (kaputte Punkte fallen weg, zu wenige Punkte = kein Polygon). - **Neuer Formtyp `polygon` im `CollisionSystem`**, mit Überlappung und `exitDistance` gegen Box, Kreis und Polygon. Konkave Polygone werden intern in konvexe Teile zerlegt. - **Tilemap:** Hat das Bild eines Solid-Tiles ein Polygon, blockiert die Zelle mit diesem Polygon statt mit der ganzen Zelle. Zellen ohne Polygon werden weiter zu Rechtecken zusammengefasst wie bisher. - Scene-View: Die Polygone werden wie die übrigen Kollisionsformen umrandet. - Tests: Unit-Tests für die neue Form (Überlappung, Anhalten, Gleiten an einer Schräge, konkav zerlegt), für Speichern und Laden, für die Polygon-Kommandos mit Undo. e2e: Polygon im Texture-Tab zeichnen, eine Figur hält an der Schräge. ## Offen (mit Vorschlag) - **Welche Tiles?** Ein Floor- oder Wall-Tile setzt sich aus Viertelkacheln eines ganzen Blocks zusammen, dort gehört ein Polygon zu keinem einzelnen Frame. Vorschlag: Polygone gelten zuerst nur für **None (1 × 1)**-Tiles, deren Bild ein Frame ist. Autotiles blockieren weiter als Rechteck. - **Animierte Tiles:** Jedes Bild ist ein Frame und könnte ein eigenes Polygon haben. Vorschlag: es zählt das Polygon des **ersten** Bildes. Eine Kollision, die mit der Animation zuckt, ist selten gewollt. - **Verhältnis zu #140:** Vorschlag: Die Kollisionsgröße skaliert auch das Polygon, um die Zellmitte. Polygone werden nicht mit Nachbarn verschmolzen, jede Zelle bringt ihr eigenes mit. - **Auch für platzierte Sprites?** Ein Objekt mit diesem Frame könnte dasselbe Polygon als Collider nutzen (Collider-Form „Frame polygon"). Vorschlag: nicht in diesem Issue, eigenes Folge-Issue, wenn gewünscht. - **Konvex erzwingen statt zerlegen?** Einfacher, aber für Felsen und Bäume zu eng. Vorschlag: **zerlegen**, selbstüberschneidende Polygone werden abgelehnt und im Tab rot markiert.
admin added this to the Development project 2026-09-18 09:49:51 +02:00
admin added the low-priority label 2026-09-18 09:50:53 +02:00
admin added held-back and removed low-priority labels 2026-09-18 12:20:26 +02:00
admin moved this to Held back in Development on 2026-09-18 12:20:34 +02:00
admin moved this to Next in Development on 2026-09-18 16:30:43 +02:00
admin moved this to Held back in Development on 2026-09-18 16:31:07 +02:00
admin moved this to Next in Development on 2026-09-18 22:19:20 +02:00
Author
Owner

Umgesetzt in zwei Commits:

  • 32057a9 – erste Fassung: ein Kollisions-Umriss pro Frame, bearbeitet im Texture-Tab mit dem Umschalter Collision. Eine einfache Tile blockiert mit dem Umriss ihres ersten Bilds statt mit ihrem Rechteck, die Collision Scale skaliert ihn um die Zellmitte. Autotiles blockierten dort noch als Rechteck.
  • 947a6b1 – Überarbeitung für Autotiles, weil ein Umriss pro Frame bei ihnen nicht passen kann (eine Autotile-Zelle wird aus vier Vierteln des Blocks zusammengesetzt, nie aus einem Frame).

Wie es jetzt funktioniert

Mehrere Umrisse pro Frame (Texture-Tab, Collision an):

  • Klick setzt eine Ecke an den aktiven Umriss.
  • Ctrl/Cmd-Klick beginnt einen neuen Umriss.
  • Klick auf Ecke, Kante oder Fläche eines anderen Umrisses macht diesen aktiv (dicker gezeichnet, helle Griffe).
  • Delete entfernt den aktiven Umriss, im Collision-Modus nie den Frame.
  • Ecken ziehen, auf Kante einfügen, per Doppelklick löschen wie bei den Polygon-Collidern aus #151.

Autotiles: Die Umrisse werden über den ganzen Block gezeichnet. Sie werden an den Vierteln zerschnitten (Sutherland-Hodgman), und jede Zelle bekommt die Stücke genau der Viertel, aus denen ihr Bild besteht. Kanten, Außenecken und Kerben ergeben sich damit automatisch.

  • Tile des Blocks ohne Zeichnung → ihre Viertel blockieren voll. Das Kreuz für die Kerben muss also nicht gezeichnet werden, Innenecken bleiben zu. Sobald in einer Tile etwas gezeichnet ist, zählt dort nur das, so lässt sich ein Viertel absichtlich offen lassen.
  • Zellen aus lauter vollen Vierteln werden wieder zu Rechtecken zusammengefasst.
  • Die Collision Scale gilt nicht für eine Autotile mit Umriss, dort gibt der Umriss die Form vor. Ohne Umriss bleibt es beim Rechteck mit Scale.
  • Das Format der ersten Fassung (ein Umriss als flache Liste) wird beim Laden weiter gelesen.

Tests

  • npm run check: 106 Files, 1508 Tests (vorher 1501), grün.
  • Neue Unit-Tests u. a.: See aus 3 × 3 Zellen (auf einem Messraster geprüft: blockiert genau 2,25–4,75, rundum eine Viertel-Zelle eingerückt), Ring um ein leeres Feld (Kerbe ohne Zeichnung blockiert, mit Zeichnung in derselben Tile bleibt sie offen), eine Kiste gleitet am Ufer von 12 See-Zellen 80 Schritte lang exakt gleichmäßig und bündig ohne Haken – in beiden Systemen; dazu mehrere Umrisse pro Frame, altes Format, Autotile ohne Umriss.
  • Gegenprobe: 6 eingebaute Fehler, alle gefangen.
  • e2e 42c prüft neues Format, Ctrl-Klick und Delete im Browser (+1 Check, Summe 1068). Lokal GM_STEPS=39,40,41,42 → ALL PASSED – 94 checks.
  • Vom User selbst im Editor getestet.
  • CI zu 947a6b1 lief beim Schreiben dieses Kommentars noch; die Summe 1068 ist gerechnet, das Ergebnis trage ich nach.

Bekannt

Im Scene-View werden Ufer-Zellen als einzelne Viertel-Stücke umrandet, an den Viertel-Grenzen sieht man kleine Querstriche. Auf die Kollision hat das keinen Einfluss.

Doku: Wiki-Seiten Textures, Tilemaps, Tiles, Architecture, Development (93a5641).

Umgesetzt in zwei Commits: - **`32057a9`** – erste Fassung: ein Kollisions-Umriss pro Frame, bearbeitet im Texture-Tab mit dem Umschalter **Collision**. Eine einfache Tile blockiert mit dem Umriss ihres ersten Bilds statt mit ihrem Rechteck, die Collision Scale skaliert ihn um die Zellmitte. Autotiles blockierten dort noch als Rechteck. - **`947a6b1`** – Überarbeitung für Autotiles, weil ein Umriss pro Frame bei ihnen nicht passen kann (eine Autotile-Zelle wird aus vier Vierteln des Blocks zusammengesetzt, nie aus einem Frame). ## Wie es jetzt funktioniert **Mehrere Umrisse pro Frame** (Texture-Tab, Collision an): - Klick setzt eine Ecke an den aktiven Umriss. - Ctrl/Cmd-Klick beginnt einen neuen Umriss. - Klick auf Ecke, Kante oder Fläche eines anderen Umrisses macht diesen aktiv (dicker gezeichnet, helle Griffe). - `Delete` entfernt den aktiven Umriss, im Collision-Modus nie den Frame. - Ecken ziehen, auf Kante einfügen, per Doppelklick löschen wie bei den Polygon-Collidern aus #151. **Autotiles:** Die Umrisse werden über den ganzen Block gezeichnet. Sie werden an den Vierteln zerschnitten (Sutherland-Hodgman), und jede Zelle bekommt die Stücke genau der Viertel, aus denen ihr Bild besteht. Kanten, Außenecken und Kerben ergeben sich damit automatisch. - **Tile des Blocks ohne Zeichnung** → ihre Viertel blockieren voll. Das Kreuz für die Kerben muss also nicht gezeichnet werden, Innenecken bleiben zu. Sobald in einer Tile etwas gezeichnet ist, zählt dort nur das, so lässt sich ein Viertel absichtlich offen lassen. - Zellen aus lauter vollen Vierteln werden wieder zu Rechtecken zusammengefasst. - Die Collision Scale gilt nicht für eine Autotile mit Umriss, dort gibt der Umriss die Form vor. Ohne Umriss bleibt es beim Rechteck mit Scale. - Das Format der ersten Fassung (ein Umriss als flache Liste) wird beim Laden weiter gelesen. ## Tests - `npm run check`: 106 Files, 1508 Tests (vorher 1501), grün. - Neue Unit-Tests u. a.: See aus 3 × 3 Zellen (auf einem Messraster geprüft: blockiert genau 2,25–4,75, rundum eine Viertel-Zelle eingerückt), Ring um ein leeres Feld (Kerbe ohne Zeichnung blockiert, mit Zeichnung in derselben Tile bleibt sie offen), eine Kiste gleitet am Ufer von 12 See-Zellen 80 Schritte lang exakt gleichmäßig und bündig ohne Haken – in beiden Systemen; dazu mehrere Umrisse pro Frame, altes Format, Autotile ohne Umriss. - Gegenprobe: 6 eingebaute Fehler, alle gefangen. - e2e 42c prüft neues Format, Ctrl-Klick und `Delete` im Browser (+1 Check, Summe 1068). Lokal `GM_STEPS=39,40,41,42` → ALL PASSED – 94 checks. - Vom User selbst im Editor getestet. - CI zu `947a6b1` lief beim Schreiben dieses Kommentars noch; die Summe 1068 ist gerechnet, das Ergebnis trage ich nach. ## Bekannt Im Scene-View werden Ufer-Zellen als einzelne Viertel-Stücke umrandet, an den Viertel-Grenzen sieht man kleine Querstriche. Auf die Kollision hat das keinen Einfluss. Doku: Wiki-Seiten Textures, Tilemaps, Tiles, Architecture, Development (`93a5641`).
Author
Owner

Nachtrag CI zu 947a6b1 (aus den Job-Logs gelesen):

  • Gate (Task 142): 106 Testdateien grün, CHECK EXIT 0
  • Suite (Task 143): ALL PASSED – 1068 checks — die Summe 1068 ist damit gemessen
  • Smokes 24 / 19 / 21, alle grün
**Nachtrag CI zu `947a6b1`** (aus den Job-Logs gelesen): - Gate (Task 142): 106 Testdateien grün, `CHECK EXIT 0` - Suite (Task 143): `ALL PASSED – 1068 checks` — die Summe 1068 ist damit gemessen - Smokes 24 / 19 / 21, alle grün
admin moved this to Persistent in Development on 2026-09-19 11:07:11 +02:00
admin moved this to Done in Development on 2026-09-19 11:07:15 +02:00
admin removed the held-back label 2026-09-19 11:08:23 +02:00
meleki referenced this issue from a commit 2026-09-20 19:26:03 +02:00
Moving against a tilemap whose cells block with an outline no longer costs more than a whole frame: an outlined cell is its own polygon, split into convex parts, so a map that is a handful of merged rectangles when built from plain solid tiles is hundreds of solids under one entity - and Move brought every one of them into world coordinates and tested it exactly, including the ones on the far side of the map, which measured 148 ms per Move call on a 20x20 map with 16-corner outlines (34 ms at 8 corners, 0.4 ms with plain solid tiles), matching the report that standing still holds 60 fps while anything that moves stutters; the fix is a broad phase in two stages that decides nothing and only leaves out what cannot be reached - a move never ends further from its start than it is long, because sliding always shortens the rest, so every collider of the mover gets a rectangle grown by the move length and an obstacle whose rectangle does not meet it is never tested, a ray taking the rectangle around itself, and that rectangle is carried back into the map's own units with invert(m) and turned into cell coordinates (cellsWithin), a rotated map through its own axes, so a far cell is not brought into world form in the first place - solidShapesOf was O(cells) per call, which was the second cost block and the reason the 60x60 case still grew; CollisionSystem.overlaps takes the same range, so the editor run asks a map only for the cells within reach of the other side each frame, two maps narrowing nothing, and a Move obstacle now falls away at build time when its layer does not meet the mover's (#152) instead of inside the loop; the measurements after: 148 -> 0.26 ms, 34 -> 0.27 ms, 60x60 20 -> 3.5 ms, plain tiles unchanged; this is no regression of the recent commits - the same benchmark in worktrees against 947a6b1 (#141), 6a31f5e, 4f55f85 and ae3aa7d gives the same 85-148 ms, the cost has been there since outlined cells arrived with #141; new pure functions shapeBounds/grownBounds/boundsMeet in collider.ts and cellsWithin/cellsMeet in tilemapCollision.ts, 11 new unit tests in tests/core/broadphase.test.ts (the rectangles of all four shapes including a rotated box measuring its diagonal reach, touching counts as meeting, cellsWithin straight and turned by 90 degrees, a map handing out 400 cells without a range and under 10 with one, an 800 unit move from far away still stopping at the first cell, a rotated map blocking where its one painted cell was turned to, a ray across the whole map, and the editor run seeing a collider on a far cell), and four counter-checks hold: dropping the cell filter fails the counting test, padding without the move length fails 4 move tests, ignoring the rotation in cellsWithin fails 3, and a wrong range in the detection fails the touching test; one blunt test on the way - the first rotated map was painted on every cell, where even a wrongly computed cell range blocks and the counter-check stayed green, so exactly one cell is painted now and the test measures where it turned to; no e2e check and the check sum stays 1157, because a timing check in the browser would flutter under CI load while modules 40 and 42 cover the correctness anyway, the speed evidence being the node benchmark (#184)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#141