Physik: Move-and-Slide entlang der Kontaktnormale mit Friction (Ausgänge hit/normal, ohne Stufen, ohne Character-Controller) #150

Closed
opened 2026-09-18 19:00:08 +02:00 by meleki · 1 comment
Owner

Folge aus #147. Umgeschrieben am 2026-09-18 nach der Diskussion zu #147: Es geht nicht mehr um Schrägen und Stufen, sondern darum, dass Move mit „Collide with solids" nicht mehr starr korrigiert, sondern über die Kontaktnormale reagiert, mit Friction.

Befund

moveAndSlide in src/runtime/systems/solids.ts bewegt erst X, dann Y, jede Achse für sich. Trifft eine Achse etwas, wird sie nur entlang derselben Achse zurückgesetzt (stepAxis). Eine Kontaktnormale gibt es nicht. Folgen:

  • Ein Circle, der gegen eine Kante fährt, bleibt an der Ecke hängen, statt abzurutschen.
  • Schräg gegen eine gerade Wand gleitet ein Objekt immer mit voller Geschwindigkeit weiter. Friction am Collider (seit #146) spielt für Move keine Rolle.
  • An einer schrägen Fläche (gedrehter Box-Collider) bleibt ein Objekt stehen, statt entlangzugleiten. Das ist aus dem Code abgeleitet, gemessen ist es noch nicht.

Entscheidungen (aus der Diskussion zu #147)

  • Kein Character-Controller von Rapier. Er kennt weder Friction noch Bounciness (in character_controller.d.ts kommt keins von beiden vor), hakt an Nahtstellen (Messung in #147) und sieht nur den Stand des letzten Physik-Steps. Sein größter Vorteil, die Stufen, wird nicht gebraucht.
  • Keine Stufen, keine Grenzwinkel. Wird bei Bedarf später selbst über Nodes gebaut.
  • Keine Bounciness bei Move. Move gibt nur eine Verschiebung pro Frame vor und hat kein Gedächtnis für eine Geschwindigkeit, ein Abprall wäre nicht sichtbar. Abprallen gibt es mit einem Dynamic Body (Set velocity, Apply force), dort rechnet Rapier Bounciness schon heute.

Zu bauen

  1. Geometrie über Rapier: Shape.castShape (Rapier 0.20, geometry/shape.d.ts) liefert für zwei Formen Zeitpunkt des Treffers und Normale, ohne Welt und ohne Broadphase. Move castet gegen die blockierenden Collider, die solids.ts heute schon mit ihren aktuellen Positionen sammelt. Damit gibt es kein Problem mit einem veralteten Stand, und die Formpaare in collider.ts müssen nicht von Hand erweitert werden. Polygone aus Stufe 2 (#144) gehen später ohne weiteren Aufwand mit.
  2. Reaktion, in bis zu 3–4 Durchgängen pro Frame:
    1. Bis zur Kontaktfläche fahren (bündig, mit dem kleinstmöglichen Abstand).
    2. Den Rest der Bewegung in einen Anteil in Richtung der Normale und einen entlang der Fläche zerlegen.
    3. Der Anteil in Richtung der Normale fällt weg.
    4. Der Anteil entlang der Fläche wird um Friction × Anteil in Richtung der Normale gekürzt (Coulomb), höchstens bis 0.
  3. Neue Ausgänge am Move-Node: hit (bool), normal x, normal y. Damit lässt sich ein Abprall oder eine andere Reaktion im Graph selbst bauen.
  4. Schieben von Dynamic Bodies (#147) bleibt, der Stoß geht entlang der Normale statt entlang der Achse.
  5. Editor-Run und Play gleich: Beide gehen über denselben Weg. Rapier ist im Editor ohnehin geladen (#145). Heute läuft der Editor-Run über das alte CollisionSystem.

Messlatte / Tests

  • Circle fährt gegen eine Kante und rutscht ab.
  • Schräg gegen eine Wand: bei Friction 0 ungebremstes Gleiten, bei hoher Friction Stillstand, dazwischen gemessen gegen die Formel.
  • Gedrehte Box als Schräge: gleitet entlang, hängt nicht.
  • Nahtstellen: Boden aus 12 Kisten (100 Schritte mit „Schwerkraft" per Move) und Wand aus 3 gestapelten Rechtecken (60 Schritte), wie in #147. Mit Normalen kann an Nähten dasselbe Haken auftreten wie beim Controller. Vorgabe: 0× hängen, bündig.
  • Tilemap mit Zellen nebeneinander, gleiche Messung.
  • Die bestehenden Kollisionstests, die gerade gegen Wände fahren, sollen ihre exakten Werte behalten. Wo schräg getroffen wird, ändern sich Werte, jede Änderung wird begründet.
  • e2e in Modul 41: Circle an der Kante im echten Play, Ausgänge hit/normal im Graph.

Offen (mit Vorschlag)

  1. Bestehende Spiele ändern sich: Die Vorgabe für Friction am Collider ist 0,5. Eine Figur, die per Move „Schwerkraft" nach unten drückt und dabei läuft, wird dadurch gebremst. Vorschlag: physikalisch richtig lassen und an den Smokes messen, wie stark es sich auswirkt. Wenn es stört, gilt Friction bei Move erst ab einem Schalter am Node.
  2. Friction von zwei Collidern kombinieren: Vorschlag: so wie Rapier es im PhysicsSystem für diese Paarung tut, damit Move und Dynamic Bodies gleich rutschen.
  3. Gegenmittel, falls Nähte haken: Vorschlag: Tilemap als zusammenhängender Umriss statt einer Box pro Zelle. Bei einzelnen Objekten nebeneinander bleibt ein Rest, der dokumentiert wird. Erst bauen, wenn die Messung es verlangt.
  4. Move ohne eigenen Collider bewegt heute gar nichts (Nebenbefund aus #147). Vorschlag: bei der Gelegenheit bewegt es sich dann frei.
Folge aus #147. Umgeschrieben am 2026-09-18 nach der Diskussion zu #147: Es geht nicht mehr um Schrägen und Stufen, sondern darum, dass `Move` mit „Collide with solids" nicht mehr starr korrigiert, sondern über die Kontaktnormale reagiert, mit Friction. ## Befund `moveAndSlide` in `src/runtime/systems/solids.ts` bewegt erst X, dann Y, jede Achse für sich. Trifft eine Achse etwas, wird sie nur entlang derselben Achse zurückgesetzt (`stepAxis`). Eine Kontaktnormale gibt es nicht. Folgen: - Ein Circle, der gegen eine Kante fährt, bleibt an der Ecke hängen, statt abzurutschen. - Schräg gegen eine gerade Wand gleitet ein Objekt immer mit voller Geschwindigkeit weiter. Friction am Collider (seit #146) spielt für `Move` keine Rolle. - An einer schrägen Fläche (gedrehter Box-Collider) bleibt ein Objekt stehen, statt entlangzugleiten. Das ist aus dem Code abgeleitet, gemessen ist es noch nicht. ## Entscheidungen (aus der Diskussion zu #147) - **Kein Character-Controller von Rapier.** Er kennt weder Friction noch Bounciness (in `character_controller.d.ts` kommt keins von beiden vor), hakt an Nahtstellen (Messung in #147) und sieht nur den Stand des letzten Physik-Steps. Sein größter Vorteil, die Stufen, wird nicht gebraucht. - **Keine Stufen, keine Grenzwinkel.** Wird bei Bedarf später selbst über Nodes gebaut. - **Keine Bounciness bei `Move`.** `Move` gibt nur eine Verschiebung pro Frame vor und hat kein Gedächtnis für eine Geschwindigkeit, ein Abprall wäre nicht sichtbar. Abprallen gibt es mit einem Dynamic Body (Set velocity, Apply force), dort rechnet Rapier Bounciness schon heute. ## Zu bauen 1. **Geometrie über Rapier:** `Shape.castShape` (Rapier 0.20, `geometry/shape.d.ts`) liefert für zwei Formen Zeitpunkt des Treffers und Normale, ohne Welt und ohne Broadphase. `Move` castet gegen die blockierenden Collider, die `solids.ts` heute schon mit ihren aktuellen Positionen sammelt. Damit gibt es kein Problem mit einem veralteten Stand, und die Formpaare in `collider.ts` müssen nicht von Hand erweitert werden. Polygone aus Stufe 2 (#144) gehen später ohne weiteren Aufwand mit. 2. **Reaktion, in bis zu 3–4 Durchgängen pro Frame:** 1. Bis zur Kontaktfläche fahren (bündig, mit dem kleinstmöglichen Abstand). 2. Den Rest der Bewegung in einen Anteil in Richtung der Normale und einen entlang der Fläche zerlegen. 3. Der Anteil in Richtung der Normale fällt weg. 4. Der Anteil entlang der Fläche wird um **Friction × Anteil in Richtung der Normale** gekürzt (Coulomb), höchstens bis 0. 3. **Neue Ausgänge am `Move`-Node:** `hit` (bool), `normal x`, `normal y`. Damit lässt sich ein Abprall oder eine andere Reaktion im Graph selbst bauen. 4. **Schieben von Dynamic Bodies (#147) bleibt**, der Stoß geht entlang der Normale statt entlang der Achse. 5. **Editor-Run und Play gleich:** Beide gehen über denselben Weg. Rapier ist im Editor ohnehin geladen (#145). Heute läuft der Editor-Run über das alte `CollisionSystem`. ## Messlatte / Tests - Circle fährt gegen eine Kante und rutscht ab. - Schräg gegen eine Wand: bei Friction 0 ungebremstes Gleiten, bei hoher Friction Stillstand, dazwischen gemessen gegen die Formel. - Gedrehte Box als Schräge: gleitet entlang, hängt nicht. - **Nahtstellen:** Boden aus 12 Kisten (100 Schritte mit „Schwerkraft" per `Move`) und Wand aus 3 gestapelten Rechtecken (60 Schritte), wie in #147. Mit Normalen kann an Nähten dasselbe Haken auftreten wie beim Controller. Vorgabe: 0× hängen, bündig. - Tilemap mit Zellen nebeneinander, gleiche Messung. - Die bestehenden Kollisionstests, die gerade gegen Wände fahren, sollen ihre exakten Werte behalten. Wo schräg getroffen wird, ändern sich Werte, jede Änderung wird begründet. - e2e in Modul 41: Circle an der Kante im echten Play, Ausgänge `hit`/`normal` im Graph. ## Offen (mit Vorschlag) 1. **Bestehende Spiele ändern sich:** Die Vorgabe für Friction am Collider ist 0,5. Eine Figur, die per `Move` „Schwerkraft" nach unten drückt und dabei läuft, wird dadurch gebremst. Vorschlag: physikalisch richtig lassen und an den Smokes messen, wie stark es sich auswirkt. Wenn es stört, gilt Friction bei `Move` erst ab einem Schalter am Node. 2. **Friction von zwei Collidern kombinieren:** Vorschlag: so wie Rapier es im `PhysicsSystem` für diese Paarung tut, damit `Move` und Dynamic Bodies gleich rutschen. 3. **Gegenmittel, falls Nähte haken:** Vorschlag: Tilemap als zusammenhängender Umriss statt einer Box pro Zelle. Bei einzelnen Objekten nebeneinander bleibt ein Rest, der dokumentiert wird. Erst bauen, wenn die Messung es verlangt. 4. **`Move` ohne eigenen Collider** bewegt heute gar nichts (Nebenbefund aus #147). Vorschlag: bei der Gelegenheit bewegt es sich dann frei.
admin added the high-priority label 2026-09-18 19:25:12 +02:00
admin added this to the Development project 2026-09-18 19:25:16 +02:00
admin moved this to Next in Development on 2026-09-18 19:25:21 +02:00
meleki changed title from Physik: Move mit Kollision über Schrägen und Stufen (eigenes Move-and-Slide statt Character-Controller) to Physik: Move-and-Slide entlang der Kontaktnormale mit Friction (Ausgänge hit/normal, ohne Stufen, ohne Character-Controller) 2026-09-18 19:47:02 +02:00
Author
Owner

Erledigt in 1b5e386 (Wiki e1e8bbd).

Was sich ändert

  • Move mit „Collide with solids“ reagiert über die Kontaktnormale: Das Objekt fährt bis zur Fläche, bleibt bündig davor stehen, und der Rest der Bewegung geht entlang der Fläche weiter. Pro Move höchstens 4 Kontakte nacheinander (Ecke Boden/Wand = 2).
  • Friction nach Coulomb: Der Anteil entlang der Fläche wird um Friction × Anteil in die Fläche gekürzt. Friction eines Paares = Mittelwert beider Collider, wie Rapier bei Dynamic Bodies (Antwort 2). Ein Tile zählt als 0,5.
  • Neue Ausgänge am Move-Node: hit, normal x, normal y (erster Kontakt, Weltkoordinaten, weg von der Fläche, wie On collision enter). blocked x/y bleiben und melden, gegen welche eigene Achse etwas stand: bergab auf einer Schräge ist x frei, bergauf nicht.
  • Schieben von Dynamic Bodies geht entlang der Normale: eine an der Ecke getroffene Kiste weicht schräg aus.
  • Move ohne eigenen Collider bewegt sich frei (Antwort 4).
  • Editor-Run und Play laufen beide über Solids, alle Kollisionstests laufen gegen beide Systeme.
  • Bewusst nicht drin: Stufen, Grenzwinkel, Bounciness, Character-Controller.

Abweichung vom Issue: keine Geometrie über castShape

Gemessen: Rapier rechnet in f32 und landet bei 200 px Weg 3·10⁻⁶ px hinter der Kontaktfläche, also schon in der Wand. Unsere Überlappungsprüfung in f64 würde das als Eindringen werten, bündige Werte gäbe es nicht mehr. Treffpunkt und Normale kommen deshalb aus der eigenen f64-Geometrie (contactBetween in collider.ts). Preis: Polygone aus Stufe 2 brauchen dort ein eigenes SAT.

Nahtstellen (Antwort 3)

Kein Haken gemessen: Boden aus 12 Blöcken (Box und Kreis, 100 Frames, jeden Frame exakt 2,25 weiter, bündig, nie blocked x), Wand aus 3 gestapelten Blöcken, Tilemap-Wand. Ein Umriss für die Tilemap war deshalb nicht nötig.

Tests

  • npm run check: 104 Files, 1469 Tests (vorher 1454).
  • Neue Unit-Tests gegen beide Systeme: Kreis rollt über die Kante; 30°-Schräge rutscht pro Frame genau g·sin − μ·g·cos (μ = 0 / 0,5 / 0,6, bei 0,6 bleibt er liegen); schräg gegen die Wand gleitet mit Friction 0 voll, mit 0,5 gar nicht; Nähte; Kiste an der Ecke geschoben (vy/vx = Normale); contactBetween; Node-Katalog; Objekt ohne Collider.
  • Geänderte alte Werte: Diagonale gegen die Wand endet bei y = 32 statt 40 (Friction), zwei Tilemap-Gleittests landen genau auf −200 + 60·(6,7 − 0,5·3,1). Gerade gegen eine Wand bleibt alles exakt.
  • Gegenprobe mit 8 eingebauten Fehlern, alle gefangen.
  • e2e 41d (2 neue Checks, echtes Play): der Ball rollt über die Kante auf die Oberseite, der Graph loggt normal = −0,8/−0,6.

CI zu 1b5e386

  • Gate (Task 138): 104 Testdateien grün, CHECK EXIT 0.
  • Suite (Task 139): ALL PASSED – 1057 checks, die Summe ist damit gemessen.
  • Smokes 24 / 19 / 21: Die drei Beispielspiele laufen auch mit Friction am Boden und an Wänden grün.

#144 bleibt offen bis Stufe 2.

Erledigt in `1b5e386` (Wiki `e1e8bbd`). ## Was sich ändert - **`Move` mit „Collide with solids“ reagiert über die Kontaktnormale:** Das Objekt fährt bis zur Fläche, bleibt bündig davor stehen, und der Rest der Bewegung geht entlang der Fläche weiter. Pro Move höchstens 4 Kontakte nacheinander (Ecke Boden/Wand = 2). - **Friction nach Coulomb:** Der Anteil entlang der Fläche wird um Friction × Anteil in die Fläche gekürzt. Friction eines Paares = Mittelwert beider Collider, wie Rapier bei Dynamic Bodies (Antwort 2). Ein Tile zählt als 0,5. - **Neue Ausgänge am `Move`-Node:** `hit`, `normal x`, `normal y` (erster Kontakt, Weltkoordinaten, weg von der Fläche, wie `On collision enter`). `blocked x/y` bleiben und melden, gegen welche eigene Achse etwas stand: bergab auf einer Schräge ist x frei, bergauf nicht. - **Schieben von Dynamic Bodies** geht entlang der Normale: eine an der Ecke getroffene Kiste weicht schräg aus. - **`Move` ohne eigenen Collider** bewegt sich frei (Antwort 4). - **Editor-Run und Play** laufen beide über `Solids`, alle Kollisionstests laufen gegen beide Systeme. - Bewusst nicht drin: Stufen, Grenzwinkel, Bounciness, Character-Controller. ## Abweichung vom Issue: keine Geometrie über `castShape` Gemessen: Rapier rechnet in f32 und landet bei 200 px Weg **3·10⁻⁶ px hinter** der Kontaktfläche, also schon in der Wand. Unsere Überlappungsprüfung in f64 würde das als Eindringen werten, bündige Werte gäbe es nicht mehr. Treffpunkt und Normale kommen deshalb aus der eigenen f64-Geometrie (`contactBetween` in `collider.ts`). Preis: Polygone aus Stufe 2 brauchen dort ein eigenes SAT. ## Nahtstellen (Antwort 3) Kein Haken gemessen: Boden aus 12 Blöcken (Box und Kreis, 100 Frames, jeden Frame exakt 2,25 weiter, bündig, nie `blocked x`), Wand aus 3 gestapelten Blöcken, Tilemap-Wand. Ein Umriss für die Tilemap war deshalb nicht nötig. ## Tests - `npm run check`: 104 Files, 1469 Tests (vorher 1454). - Neue Unit-Tests gegen beide Systeme: Kreis rollt über die Kante; 30°-Schräge rutscht pro Frame genau g·sin − μ·g·cos (μ = 0 / 0,5 / 0,6, bei 0,6 bleibt er liegen); schräg gegen die Wand gleitet mit Friction 0 voll, mit 0,5 gar nicht; Nähte; Kiste an der Ecke geschoben (vy/vx = Normale); `contactBetween`; Node-Katalog; Objekt ohne Collider. - Geänderte alte Werte: Diagonale gegen die Wand endet bei y = 32 statt 40 (Friction), zwei Tilemap-Gleittests landen genau auf −200 + 60·(6,7 − 0,5·3,1). Gerade gegen eine Wand bleibt alles exakt. - Gegenprobe mit 8 eingebauten Fehlern, alle gefangen. - e2e 41d (2 neue Checks, echtes Play): der Ball rollt über die Kante auf die Oberseite, der Graph loggt `normal` = −0,8/−0,6. ## CI zu `1b5e386` - Gate (Task 138): 104 Testdateien grün, `CHECK EXIT 0`. - Suite (Task 139): `ALL PASSED – 1057 checks`, die Summe ist damit gemessen. - Smokes 24 / 19 / 21: Die drei Beispielspiele laufen auch mit Friction am Boden und an Wänden grün. #144 bleibt offen bis Stufe 2.
admin moved this to Done in Development on 2026-09-18 22:18:54 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#150