Physik: Rigidbodies mit Schwerkraft, Reibung, Bounciness, Masse und mehr Formen (Physik-Engine statt eigener Kollision) #144

Closed
opened 2026-09-18 16:37:54 +02:00 by meleki · 2 comments
Owner

Umbrella-Issue: Kollision soll reagieren statt nur anzuhalten. Die einzelnen Stufen werden nach den Entscheidungen unten eigene Issues. #141 (Kollisions-Polygon pro Frame) hängt davon ab, siehe unten.

Befund

  • Heute gibt es keine Physik, nur Erkennen und Anhalten. CollisionSystem sortiert Überlappungen in enter/stay/exit (src/runtime/systems/CollisionSystem.ts:54). Bewegt wird ausschließlich über den Node Move mit „Collide with solids“ (src/core/graph/nodes/action/Move.ts:22) → moveAndSlide (CollisionSystem.ts:121): Achse für Achse, an der Wand anhalten, entlanggleiten. Es gibt keine Geschwindigkeit, keine Masse, keinen Impuls, keine Reibung und kein Abprallen. Zwei bewegte Objekte schieben sich nicht, eins bleibt einfach stehen. Daher das „starre“ Gefühl.
  • Schwerkraft muss man sich im Graph selbst bauen (eigene Variable für die Geschwindigkeit, jeden Tick Move).
  • Formen: nur box und circle (src/core/properties/PropertySchema.ts:338). Jedes Formpaar ist in src/core/properties/collider.ts von Hand geschrieben (SAT, Kreis/Kreis, Box/Kreis per Bisektion). Jede neue Form braucht Überlappung und exitDistance gegen alle anderen, beim Polygon (#141) auch konkav.
  • Die Paarsuche ist O(n²) pro Tick (CollisionSystem.ts:59), ohne Broadphase.
  • Pixi kann das nicht: Pixi v8 ist reiner Renderer. In node_modules/pixi.js/lib gibt es keinen Treffer für rigid body, gravity oder restitution. Physik muss also dazukommen, selbst geschrieben oder als Library.

Library statt selbst schreiben (gemessen am 2026-09-18, npm)

Zum Vergleich: Unser Haupt-Bundle dist/assets/index-*.js hat heute 263 KB gzip.

Library Version, Stand Lizenz Größe gzip Anmerkung
Rapier 2D (@dimforge/rapier2d-compat) 0.20.0, Aug 2026 Apache-2.0 ~795 KB (rapier.mjs, WASM eingebettet) Rust/WASM. Alles an Bord: Static/Dynamic/Kinematic, Reibung, Restitution, Dichte/Masse, Gravity Scale, CCD, Sensoren, Kollisionsgruppen, Joints, KinematicCharacterController (= unser Move-and-Slide), Formen Capsule/Segment/ConvexHull/Polyline/Heightfield/Compound und convexDecomposition für konkave Polygone. Deterministische Variante als eigenes Paket. Init ist asynchron.
Box2D v3 (box2d3-wasm) 5.2.0, Feb 2026 MIT ~180 KB (WASM 153 + JS 26) Sehr gute Stapelstabilität, CCD, Chains, CastMover/SolvePlanes für Charaktere. Community-Port eines einzelnen Maintainers, Embind-API mit manuellem Speicher. Polygone nur konvex und klein (Box2D-Konstante 8 Ecken, im Port nicht nachgemessen). Zerlegung konkaver Polygone müssten wir selbst machen.
Planck.js 1.5.0, Apr 2026 MIT ~55 KB Box2D 2.x in reinem TypeScript, synchron, läuft ohne Aufwand in vitest. Konvexe Polygone bis 12 Ecken (maxPolygonVertices = 12). Kein Character-Controller, keine Zerlegung. Ältere Solver-Generation.
Matter.js 0.20.0, Jun 2024 MIT ~26 KB Seit 2 Jahren ohne Release, bekannt wacklig beim Stapeln. Scheidet aus.

Vorschlag: Rapier. Die Library deckt alles aus diesem Issue ab, auch Polygone (#141) und den Charakter-Controller, und wird aktiv gepflegt. Der Preis ist die Größe. Sie wird deshalb erst beim Start des Spiels per dynamic import() geladen, und der Editor selbst bleibt so groß wie heute. Ist die Größe ein Ausschlusskriterium, wäre Box2D v3 die Alternative.

Zu bauen (Stufen, werden eigene Issues)

Stufe 0 – Spike (kurz, eigenes Issue): Rapier lazy im Play-Modus laden. Gemessen werden Ladezeit, Größe im Build, Init in vitest und im e2e-Container. Eine Kiste fällt auf eine Box. Danach ist entschieden, ob die Library bleibt.

Stufe 1 – Physik v1:

  • Body-Typ pro Objekt: Static / Dynamic / Kinematic, dazu Gravity scale, Linear/Angular damping, Fixed rotation, Continuous (CCD, gegen Tunneln schneller Objekte).
  • Am Collider: Friction, Bounciness (Restitution), Density (daraus die Masse). Optional eine feste Masse statt Dichte. Trigger bleibt.
  • Scene: Gravity (x, y) in px/s².
  • Laufzeit: fester Zeitschritt 60 Hz mit Akkumulator, Darstellung interpoliert. Dynamic Bodies schreiben Position und Rotation zurück.
  • Tilemap: ein Static Body aus den zusammengefassten Rechtecken (solidRects, inkl. #140).
  • Die Events OnCollisionEnter/Stay/Exit und IsTouching kommen aus der Engine. Dazu die Kontaktdaten (Normale, Stärke) als Ausgänge.
  • Neue Nodes: Apply Force, Apply Impulse, Set/Get Velocity, Set Angular Velocity, Set Gravity.
  • Move mit „Collide with solids“ läuft für Kinematic über den Character-Controller: Gleiten an Wänden, Stufen, Schrägen.

Stufe 2 – Formen: Capsule, Polygon mit Punkt-Editing im Scene-View (konkav → intern zerlegt). Danach setzt #141 (Polygon pro Frame, Tilemap) darauf auf, statt eigene Polygon-Mathematik zu schreiben.

Später, jeweils eigenes Issue: Kollisionsebenen/Masken, Raycast-Node, Joints (Scharnier, Feder), One-Way-Plattformen.

Tests: Unit-Tests gegen die Engine (fallen, abprallen, rutschen auf Schräge mit/ohne Reibung, Masse beim Stoß, Tilemap als Boden). Dazu e2e mit echter Szene: eine Kiste fällt und bleibt auf dem Boden liegen, ein Ball springt ab.

Offen (mit Vorschlag)

  1. Library: Vorschlag Rapier, lazy geladen (siehe Tabelle). Alternativen: Box2D v3 (klein, aber Community-Port) oder Planck (winzig, reines TS, weniger Funktionen).
  2. Wo die Body-Einstellungen wohnen: Vorschlag: ein neues Kind-Objekt „Rigidbody“ neben den Collidern, so wie Collider heute Kinder sind. Ein Objekt hat höchstens einen. Alternative: Felder direkt an Sprite/Text/Container.
  3. Objekte ohne Rigidbody (alle heutigen Projekte): Vorschlag: Sie verhalten sich wie heute: Collider ohne Body = Static, bewegt über Move = Kinematic. Es braucht keine Migration. Die Zahlen können sich im Nachkommabereich verschieben, weil ab dann die Engine rechnet und nicht mehr moveAndSlide. Einige Tests (e2e 08, 40l, Unit-Tests zu Move-and-Slide) werden auf Toleranzen umgestellt.
  4. Eine Wahrheit zur Laufzeit: Vorschlag: Im Spiel rechnet nur die Engine, auch Trigger und Events. collider.ts bleibt für den Editor (Umrandung, Hit-Test), damit der Editor die Library nicht laden muss.
  5. Einheiten: Vorschlag: Nach außen bleibt alles in Pixeln (px, px/s, px/s²). Intern gilt 100 px = 1 m, weil die Engines auf Meter abgestimmt sind. Der User sieht davon nichts.
  6. Vorgabe der Schwerkraft: Vorschlag: (0, 980) px/s² pro Scene. Das betrifft nur Dynamic Bodies, also ändert sich für bestehende Projekte (Top-Down) nichts.
  7. Dynamic Body unter einem Parent: Vorschlag: Er wird in Weltkoordinaten simuliert und lokal zurückgeschrieben. Bewegt sich der Parent selbst, zieht er den Body nicht mit (wie in Unity). Der Inspector weist darauf hin.
  8. Skalieren zur Laufzeit (Animation, Graph): Vorschlag: Die Form wird bei einer Änderung neu gebaut. Das ist nicht billig, aber korrekt.
  9. Determinismus: Vorschlag: vorerst die normale Variante. Die deterministische (rapier2d-deterministic-compat) nehmen wir erst, wenn Replays oder Netzwerk kommen.
Umbrella-Issue: Kollision soll **reagieren** statt nur anzuhalten. Die einzelnen Stufen werden nach den Entscheidungen unten eigene Issues. #141 (Kollisions-Polygon pro Frame) hängt davon ab, siehe unten. ## Befund - **Heute gibt es keine Physik, nur Erkennen und Anhalten.** `CollisionSystem` sortiert Überlappungen in enter/stay/exit (`src/runtime/systems/CollisionSystem.ts:54`). Bewegt wird ausschließlich über den Node `Move` mit „Collide with solids“ (`src/core/graph/nodes/action/Move.ts:22`) → `moveAndSlide` (`CollisionSystem.ts:121`): Achse für Achse, an der Wand anhalten, entlanggleiten. Es gibt keine Geschwindigkeit, keine Masse, keinen Impuls, keine Reibung und kein Abprallen. Zwei bewegte Objekte schieben sich nicht, eins bleibt einfach stehen. Daher das „starre“ Gefühl. - **Schwerkraft muss man sich im Graph selbst bauen** (eigene Variable für die Geschwindigkeit, jeden Tick `Move`). - **Formen:** nur `box` und `circle` (`src/core/properties/PropertySchema.ts:338`). Jedes Formpaar ist in `src/core/properties/collider.ts` von Hand geschrieben (SAT, Kreis/Kreis, Box/Kreis per Bisektion). Jede neue Form braucht Überlappung und `exitDistance` gegen alle anderen, beim Polygon (#141) auch konkav. - **Die Paarsuche ist O(n²) pro Tick** (`CollisionSystem.ts:59`), ohne Broadphase. - **Pixi kann das nicht:** Pixi v8 ist reiner Renderer. In `node_modules/pixi.js/lib` gibt es keinen Treffer für rigid body, gravity oder restitution. Physik muss also dazukommen, selbst geschrieben oder als Library. ## Library statt selbst schreiben (gemessen am 2026-09-18, npm) Zum Vergleich: Unser Haupt-Bundle `dist/assets/index-*.js` hat heute **263 KB gzip**. | Library | Version, Stand | Lizenz | Größe gzip | Anmerkung | |---|---|---|---|---| | **Rapier 2D** (`@dimforge/rapier2d-compat`) | 0.20.0, Aug 2026 | Apache-2.0 | **~795 KB** (`rapier.mjs`, WASM eingebettet) | Rust/WASM. Alles an Bord: Static/Dynamic/Kinematic, Reibung, Restitution, Dichte/Masse, Gravity Scale, CCD, Sensoren, Kollisionsgruppen, Joints, **KinematicCharacterController** (= unser Move-and-Slide), Formen Capsule/Segment/ConvexHull/Polyline/Heightfield/Compound und **`convexDecomposition`** für konkave Polygone. Deterministische Variante als eigenes Paket. Init ist asynchron. | | **Box2D v3** (`box2d3-wasm`) | 5.2.0, Feb 2026 | MIT | **~180 KB** (WASM 153 + JS 26) | Sehr gute Stapelstabilität, CCD, Chains, `CastMover`/`SolvePlanes` für Charaktere. Community-Port eines einzelnen Maintainers, Embind-API mit manuellem Speicher. Polygone nur konvex und klein (Box2D-Konstante 8 Ecken, im Port nicht nachgemessen). Zerlegung konkaver Polygone müssten wir selbst machen. | | **Planck.js** | 1.5.0, Apr 2026 | MIT | **~55 KB** | Box2D 2.x in reinem TypeScript, synchron, läuft ohne Aufwand in vitest. Konvexe Polygone bis 12 Ecken (`maxPolygonVertices = 12`). Kein Character-Controller, keine Zerlegung. Ältere Solver-Generation. | | Matter.js | 0.20.0, **Jun 2024** | MIT | ~26 KB | Seit 2 Jahren ohne Release, bekannt wacklig beim Stapeln. Scheidet aus. | **Vorschlag: Rapier.** Die Library deckt alles aus diesem Issue ab, auch Polygone (#141) und den Charakter-Controller, und wird aktiv gepflegt. Der Preis ist die Größe. Sie wird deshalb **erst beim Start des Spiels** per dynamic `import()` geladen, und der Editor selbst bleibt so groß wie heute. Ist die Größe ein Ausschlusskriterium, wäre Box2D v3 die Alternative. ## Zu bauen (Stufen, werden eigene Issues) **Stufe 0 – Spike (kurz, eigenes Issue):** Rapier lazy im Play-Modus laden. Gemessen werden Ladezeit, Größe im Build, Init in vitest und im e2e-Container. Eine Kiste fällt auf eine Box. Danach ist entschieden, ob die Library bleibt. **Stufe 1 – Physik v1:** - **Body-Typ pro Objekt:** Static / Dynamic / Kinematic, dazu Gravity scale, Linear/Angular damping, Fixed rotation, Continuous (CCD, gegen Tunneln schneller Objekte). - **Am Collider:** Friction, Bounciness (Restitution), Density (daraus die Masse). Optional eine feste Masse statt Dichte. Trigger bleibt. - **Scene:** Gravity (x, y) in px/s². - **Laufzeit:** fester Zeitschritt 60 Hz mit Akkumulator, Darstellung interpoliert. Dynamic Bodies schreiben Position **und Rotation** zurück. - **Tilemap:** ein Static Body aus den zusammengefassten Rechtecken (`solidRects`, inkl. #140). - **Die Events** `OnCollisionEnter/Stay/Exit` und `IsTouching` kommen aus der Engine. Dazu die Kontaktdaten (Normale, Stärke) als Ausgänge. - **Neue Nodes:** Apply Force, Apply Impulse, Set/Get Velocity, Set Angular Velocity, Set Gravity. - **`Move` mit „Collide with solids“** läuft für Kinematic über den Character-Controller: Gleiten an Wänden, Stufen, Schrägen. **Stufe 2 – Formen:** Capsule, Polygon mit Punkt-Editing im Scene-View (konkav → intern zerlegt). Danach setzt #141 (Polygon pro Frame, Tilemap) darauf auf, statt eigene Polygon-Mathematik zu schreiben. **Später, jeweils eigenes Issue:** Kollisionsebenen/Masken, Raycast-Node, Joints (Scharnier, Feder), One-Way-Plattformen. **Tests:** Unit-Tests gegen die Engine (fallen, abprallen, rutschen auf Schräge mit/ohne Reibung, Masse beim Stoß, Tilemap als Boden). Dazu e2e mit echter Szene: eine Kiste fällt und bleibt auf dem Boden liegen, ein Ball springt ab. ## Offen (mit Vorschlag) 1. **Library:** Vorschlag Rapier, lazy geladen (siehe Tabelle). Alternativen: Box2D v3 (klein, aber Community-Port) oder Planck (winzig, reines TS, weniger Funktionen). 2. **Wo die Body-Einstellungen wohnen:** Vorschlag: ein neues Kind-Objekt **„Rigidbody“** neben den Collidern, so wie Collider heute Kinder sind. Ein Objekt hat höchstens einen. Alternative: Felder direkt an Sprite/Text/Container. 3. **Objekte ohne Rigidbody (alle heutigen Projekte):** Vorschlag: Sie verhalten sich wie heute: Collider ohne Body = Static, bewegt über `Move` = Kinematic. Es braucht keine Migration. Die Zahlen können sich im Nachkommabereich verschieben, weil ab dann die Engine rechnet und nicht mehr `moveAndSlide`. Einige Tests (e2e 08, 40l, Unit-Tests zu Move-and-Slide) werden auf Toleranzen umgestellt. 4. **Eine Wahrheit zur Laufzeit:** Vorschlag: Im Spiel rechnet **nur** die Engine, auch Trigger und Events. `collider.ts` bleibt für den Editor (Umrandung, Hit-Test), damit der Editor die Library nicht laden muss. 5. **Einheiten:** Vorschlag: Nach außen bleibt alles in Pixeln (px, px/s, px/s²). Intern gilt 100 px = 1 m, weil die Engines auf Meter abgestimmt sind. Der User sieht davon nichts. 6. **Vorgabe der Schwerkraft:** Vorschlag: (0, 980) px/s² pro Scene. Das betrifft nur Dynamic Bodies, also ändert sich für bestehende Projekte (Top-Down) nichts. 7. **Dynamic Body unter einem Parent:** Vorschlag: Er wird in Weltkoordinaten simuliert und lokal zurückgeschrieben. Bewegt sich der Parent selbst, zieht er den Body nicht mit (wie in Unity). Der Inspector weist darauf hin. 8. **Skalieren zur Laufzeit** (Animation, Graph): Vorschlag: Die Form wird bei einer Änderung neu gebaut. Das ist nicht billig, aber korrekt. 9. **Determinismus:** Vorschlag: vorerst die normale Variante. Die deterministische (`rapier2d-deterministic-compat`) nehmen wir erst, wenn Replays oder Netzwerk kommen.
  1. Rapier passt
  2. eigenes kind object "Rigidbody" passt
  3. vorschlag passt
  4. bin mir nicht sicher was du meinst, aber der editor sollte die library auch laden, weil vielleicht will ich außerhalb vom play mode später auch simulieren
  5. passt 100px = 1m
  6. vorschlag passt
  7. vorschlag passt
  8. vorschlag passt (korrekt besser als billig)
  9. passt, lassen wir erstmal weg
1. Rapier passt 2. eigenes kind object "Rigidbody" passt 3. vorschlag passt 4. bin mir nicht sicher was du meinst, aber der editor sollte die library auch laden, weil vielleicht will ich außerhalb vom play mode später auch simulieren 5. passt 100px = 1m 6. vorschlag passt 7. vorschlag passt 8. vorschlag passt (korrekt besser als billig) 9. passt, lassen wir erstmal weg
admin added this to the Development project 2026-09-18 16:58:44 +02:00
admin moved this to Next in Development on 2026-09-18 16:58:53 +02:00
Author
Owner

Alle Stufen sind erledigt:

„Später“ steht jetzt in eigenen Issues: #152 (Ebenen/Masken), #153 (Raycast), #154 (Joints), #155 (One-Way-Plattformen). Weiter offen aus der Physik-Reihe: #148 (Simulation im Editor-Run) und #149 (On collision stay mit Kontaktdaten).

Damit schließe ich das Sammel-Issue.

Alle Stufen sind erledigt: - Stufe 0: #145 - Stufe 1a: #146 - Stufe 1b: #147, dazu #150 (Move-and-Slide entlang der Kontaktnormale mit Friction) - Stufe 2: #151 (Capsule, Polygon mit Punkt-Editing, Zerlegung konkaver Polygone) - Aufbauend: #141 (Kollisions-Umrisse pro Frame, auch für Autotiles) „Später“ steht jetzt in eigenen Issues: #152 (Ebenen/Masken), #153 (Raycast), #154 (Joints), #155 (One-Way-Plattformen). Weiter offen aus der Physik-Reihe: #148 (Simulation im Editor-Run) und #149 (`On collision stay` mit Kontaktdaten). Damit schließe ich das Sammel-Issue.
admin moved this to Done in Development on 2026-09-19 11:07:09 +02:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#144