Laggy mit bestimmten Collisions #184

Closed
opened 2026-09-20 18:28:13 +02:00 by admin · 1 comment
image.png

wenn ich diese collision drinnen hab, dann fängt es an zu ruckeln (20 fps oder sogar weniger) - das ist aber erst vor kurzem so, das hatte ich vor ein paar commits davor noch nicht das problem. Es ruckelt aber auch nur dann wenn sich ein objekt per move tatsächlich bewegt. wenn man still stehen bleibt wieder volle 60fps

<img width="648" alt="image.png" src="attachments/981fe645-0bfb-4396-84ae-000031fcf146"> wenn ich diese collision drinnen hab, dann fängt es an zu ruckeln (20 fps oder sogar weniger) - das ist aber erst vor kurzem so, das hatte ich vor ein paar commits davor noch nicht das problem. Es ruckelt aber auch nur dann wenn sich ein objekt per move tatsächlich bewegt. wenn man still stehen bleibt wieder volle 60fps
744 KiB
admin added the high-priority label 2026-09-20 18:28:13 +02:00
admin added this to the Development project 2026-09-20 18:28:13 +02:00
admin moved this to Next in Development on 2026-09-20 18:28:21 +02:00
admin moved this to Done in Development on 2026-09-20 19:25:27 +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)
Owner

Umgesetzt in eac50e4, Wiki in 64a527a.

Was die Ursache war

Das Bild im Issue zeigt Kollisions-Umrisse im Texture-Tab — also Kacheln, die mit einem Polygon blockieren. Ein Umriss ist pro gemalter Zelle ein eigenes Polygon, jedes in konvexe Teile zerlegt. Wo dieselbe Karte aus schlichten soliden Tiles ein paar gemergte Rechtecke ist, sind es hier hunderte Solids unter einem Entity.

Dazu kam: Move hat jedes dieser Solids in Weltkoordinaten umgerechnet und exakt geprüft — auch das auf der anderen Seite der Karte. Gemessen an einer Karte mit 20 × 20 Umriss-Zellen (Node-Benchmark, kein Browser):

Karte vorher nachher
20 × 20, Umriss mit 16 Ecken 148 ms je Move 0,26 ms
20 × 20, Umriss mit 8 Ecken 34 ms 0,27 ms
60 × 60, Umriss mit 8 Ecken 20 ms 3,5 ms
20 × 20, schlichte solide Tiles 0,4 ms 0,4 ms

Das passt auf das Symptom: im Stillstand ruft niemand Move auf, also 60 fps; sobald sich etwas bewegt, kostet ein einziger Aufruf mehr als ein ganzer Frame.

Keine Regression der letzten Commits. Derselbe Benchmark in Worktrees gegen 947a6b1 (#141), 6a31f5e, 4f55f85 (#152) und ae3aa7d gibt überall dieselben 85–148 ms. Die Kosten stecken seit den Umriss-Kacheln (#141) drin und fallen auf, sobald man Umrisse zeichnet und etwas bewegt.

Was gebaut ist

Eine Broad Phase in zwei Stufen, die nichts entscheidet — sie lässt nur weg, was gar nicht erreichbar ist:

  • Reichweite eines Moves: Ein Move kommt nie weiter von seinem Startpunkt weg, als er lang ist (Gleiten kürzt den Rest immer). Also: Rechteck um jeden Collider des Movers, gewachsen um die Move-Länge — was das nicht berührt, wird nicht geprüft. Ein Strahl nimmt das Rechteck um sich selbst.
  • Zellen statt Shapes: Das Rechteck wird per invert(m) in die eigenen Einheiten der Karte zurückgerechnet und in Zellkoordinaten umgerechnet (cellsWithin), eine gedrehte Karte durch ihre eigenen Achsen. Eine ferne Zelle wird damit gar nicht erst in Weltform gebracht — solidShapesOf war O(Zellen) pro Aufruf, das war der zweite Kostenblock und der Grund, warum der 60 × 60-Fall noch wuchs.
  • Auch die Erkennung: CollisionSystem.overlaps (Editor-Run, jeder Frame, jedes Paar) fragt eine Karte nur nach den Zellen in Reichweite der Gegenseite. Bei zwei Karten engt nichts ein.
  • Nebenbei fällt im Move ein Hindernis schon beim Aufbau weg, wenn seine Ebene die des Movers nicht trifft (#152) — vorher erst im Inneren der Schleife.

Neue reine Funktionen: shapeBounds / grownBounds / boundsMeet in collider.ts, cellsWithin / cellsMeet in tilemapCollision.ts.

Tests

./check.sh                      → 122 Files, 1799 Tests, CHECK EXIT 0   (vorher 121 / 1788)
npm run build                   → BUILD EXIT 0
GM_STEPS=1-12,17,19,39-43       → ALL PASSED – 546 checks
  • 11 neue Unit-Tests in tests/core/broadphase.test.ts: die Rechtecke der vier Formen (gedrehte Box misst ihren Schrägzug), Berühren zählt als Treffen, cellsWithin gerade und um 90° gedreht, eine Karte gibt nur die erwarteten Zellen heraus (400 ohne Bereich, < 10 mit), ein 800 Einheiten langer Move aus der Ferne stoppt weiterhin an der ersten Zelle, eine gedrehte Karte blockt dort, wohin ihre eine bemalte Zelle gedreht ist, ein Strahl über die ganze Karte trifft, und der Editor-Run erkennt einen Collider auf einer fernen Zelle.
  • Vier Gegenproben, alle greifen: Zellfilter aus → der Zähltest fällt; Padding ohne Move-Länge → 4 Move-Tests fallen; Drehung in cellsWithin ignoriert → 3 Tests fallen; falscher Bereich in der Erkennung → der Touching-Test fällt. Originale per cmp zurückgeprüft.
  • Ein stumpfer Test unterwegs: Die erste gedrehte Karte war vollflächig bemalt — da blockt auch ein falsch berechneter Zellbereich, die Gegenprobe blieb grün. Jetzt ist genau eine Zelle bemalt, und der Test misst, wohin sie sich gedreht hat.
  • Kein e2e-Check, Check-Summe bleibt 1157. Ein Zeitcheck im Browser wäre unter CI-Last wackelig; die Korrektheit decken die Module 40 und 42 ohnehin ab. Der Geschwindigkeitsbeleg ist der Node-Benchmark oben.

Zwei Dinge, die bleiben

  • Der Geschwindigkeitsbeleg ist gemessen, aber nicht am echten Projekt. Bitte im eigenen Projekt nachprüfen — das ist die einzige Messung am realen Fall.
  • Was man berührt, kostet weiter. Ein Objekt, das in vielen Umriss-Zellen steht, wird gegen jede davon getestet, und ein Umriss mit 30 Ecken pro Zelle ist teurer als einer mit acht. Für einen Seeuferverlauf ist das in Ordnung, für jede Zelle eines vollen Sees wäre es Verschwendung; der Hinweis steht jetzt im Wiki unter Tilemaps.

Wiki: Colliders (Reichweite von Move und Strahl), Tilemaps (neuer Abschnitt The cost of outlined cells), Architecture (solids, cellsWithin, shapeBounds).

Umgesetzt in [`eac50e4`](http://localhost:3000/meleki/game-maker/commit/eac50e4), Wiki in [`64a527a`](http://localhost:3000/meleki/game-maker.wiki/commit/64a527a). ## Was die Ursache war Das Bild im Issue zeigt **Kollisions-Umrisse** im Texture-Tab — also Kacheln, die mit einem Polygon blockieren. Ein Umriss ist **pro gemalter Zelle ein eigenes Polygon**, jedes in konvexe Teile zerlegt. Wo dieselbe Karte aus schlichten soliden Tiles ein paar gemergte Rechtecke ist, sind es hier hunderte Solids unter einem Entity. Dazu kam: `Move` hat **jedes** dieser Solids in Weltkoordinaten umgerechnet und exakt geprüft — auch das auf der anderen Seite der Karte. Gemessen an einer Karte mit 20 × 20 Umriss-Zellen (Node-Benchmark, kein Browser): | Karte | vorher | nachher | |---|---|---| | 20 × 20, Umriss mit 16 Ecken | **148 ms** je `Move` | **0,26 ms** | | 20 × 20, Umriss mit 8 Ecken | 34 ms | 0,27 ms | | 60 × 60, Umriss mit 8 Ecken | 20 ms | 3,5 ms | | 20 × 20, schlichte solide Tiles | 0,4 ms | 0,4 ms | Das passt auf das Symptom: im Stillstand ruft niemand `Move` auf, also 60 fps; sobald sich etwas bewegt, kostet ein einziger Aufruf mehr als ein ganzer Frame. **Keine Regression der letzten Commits.** Derselbe Benchmark in Worktrees gegen `947a6b1` (#141), `6a31f5e`, `4f55f85` (#152) und `ae3aa7d` gibt überall dieselben 85–148 ms. Die Kosten stecken seit den Umriss-Kacheln (#141) drin und fallen auf, sobald man Umrisse zeichnet und etwas bewegt. ## Was gebaut ist Eine **Broad Phase** in zwei Stufen, die nichts entscheidet — sie lässt nur weg, was gar nicht erreichbar ist: - **Reichweite eines Moves:** Ein Move kommt nie weiter von seinem Startpunkt weg, als er lang ist (Gleiten kürzt den Rest immer). Also: Rechteck um jeden Collider des Movers, gewachsen um die Move-Länge — was das nicht berührt, wird nicht geprüft. Ein Strahl nimmt das Rechteck um sich selbst. - **Zellen statt Shapes:** Das Rechteck wird per `invert(m)` in die eigenen Einheiten der Karte zurückgerechnet und in **Zellkoordinaten** umgerechnet (`cellsWithin`), eine gedrehte Karte durch ihre eigenen Achsen. Eine ferne Zelle wird damit gar nicht erst in Weltform gebracht — `solidShapesOf` war O(Zellen) pro Aufruf, das war der zweite Kostenblock und der Grund, warum der 60 × 60-Fall noch wuchs. - **Auch die Erkennung:** `CollisionSystem.overlaps` (Editor-Run, jeder Frame, jedes Paar) fragt eine Karte nur nach den Zellen in Reichweite der Gegenseite. Bei zwei Karten engt nichts ein. - Nebenbei fällt im Move ein Hindernis schon beim Aufbau weg, wenn seine **Ebene** die des Movers nicht trifft (#152) — vorher erst im Inneren der Schleife. Neue reine Funktionen: `shapeBounds` / `grownBounds` / `boundsMeet` in `collider.ts`, `cellsWithin` / `cellsMeet` in `tilemapCollision.ts`. ## Tests ``` ./check.sh → 122 Files, 1799 Tests, CHECK EXIT 0 (vorher 121 / 1788) npm run build → BUILD EXIT 0 GM_STEPS=1-12,17,19,39-43 → ALL PASSED – 546 checks ``` - **11 neue Unit-Tests** in `tests/core/broadphase.test.ts`: die Rechtecke der vier Formen (gedrehte Box misst ihren Schrägzug), Berühren zählt als Treffen, `cellsWithin` gerade und um 90° gedreht, eine Karte gibt nur die erwarteten Zellen heraus (400 ohne Bereich, < 10 mit), ein 800 Einheiten langer Move aus der Ferne stoppt weiterhin an der ersten Zelle, eine gedrehte Karte blockt dort, wohin ihre eine bemalte Zelle gedreht ist, ein Strahl über die ganze Karte trifft, und der Editor-Run erkennt einen Collider auf einer fernen Zelle. - **Vier Gegenproben, alle greifen:** Zellfilter aus → der Zähltest fällt; Padding ohne Move-Länge → 4 Move-Tests fallen; Drehung in `cellsWithin` ignoriert → 3 Tests fallen; falscher Bereich in der Erkennung → der Touching-Test fällt. Originale per `cmp` zurückgeprüft. - **Ein stumpfer Test unterwegs:** Die erste gedrehte Karte war vollflächig bemalt — da blockt auch ein falsch berechneter Zellbereich, die Gegenprobe blieb grün. Jetzt ist genau **eine** Zelle bemalt, und der Test misst, wohin sie sich gedreht hat. - **Kein e2e-Check, Check-Summe bleibt 1157.** Ein Zeitcheck im Browser wäre unter CI-Last wackelig; die Korrektheit decken die Module 40 und 42 ohnehin ab. Der Geschwindigkeitsbeleg ist der Node-Benchmark oben. ## Zwei Dinge, die bleiben - **Der Geschwindigkeitsbeleg ist gemessen, aber nicht am echten Projekt.** Bitte im eigenen Projekt nachprüfen — das ist die einzige Messung am realen Fall. - **Was man berührt, kostet weiter.** Ein Objekt, das *in* vielen Umriss-Zellen steht, wird gegen jede davon getestet, und ein Umriss mit 30 Ecken pro Zelle ist teurer als einer mit acht. Für einen Seeuferverlauf ist das in Ordnung, für jede Zelle eines vollen Sees wäre es Verschwendung; der Hinweis steht jetzt im Wiki unter Tilemaps. Wiki: Colliders (Reichweite von Move und Strahl), Tilemaps (neuer Abschnitt *The cost of outlined cells*), Architecture (`solids`, `cellsWithin`, `shapeBounds`).
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#184