Files
pygame-physics-opus5.5/docs/PHYSICS.md
T
2026-09-24 15:45:31 +02:00

476 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Physik der Sandbox
Dieses Dokument erklärt, *was* die Physik-Engine berechnet und *warum* sie es so tut. Alle
Formeln finden sich im Code wieder, meist mit derselben Bezeichnung. Die Zahlenwerte stehen
in [`src/sandbox/config.py`](../src/sandbox/config.py) (`PhysicsConfig`).
Die Physik-Schicht (`src/sandbox/physics/`) verwendet ausschließlich das Standardmodul
`math` sowie `typing`/`dataclasses` für Typangaben. Ein Architektur-Test stellt sicher,
dass dort nie pygame importiert wird.
## Inhalt
1. [Einheiten und Koordinaten](#1-einheiten-und-koordinaten)
2. [Körper: Masse und Trägheit](#2-körper-masse-und-trägheit)
3. [Integration mit festem Zeitschritt](#3-integration-mit-festem-zeitschritt)
4. [Ablauf eines Simulationsschritts](#4-ablauf-eines-simulationsschritts)
5. [Kollisionserkennung](#5-kollisionserkennung)
6. [Impulsbasierte Kollisionsauflösung](#6-impulsbasierte-kollisionsauflösung)
7. [Restitution (Elastizität)](#7-restitution-elastizität)
8. [Reibung und Rollen](#8-reibung-und-rollen)
9. [Rollwiderstand](#9-rollwiderstand)
10. [Positionskorrektur](#10-positionskorrektur)
11. [Ruhe ohne Zittern: Schlafmodus](#11-ruhe-ohne-zittern-schlafmodus)
12. [Tunneln verhindern](#12-tunneln-verhindern)
13. [Ziehen und Werfen](#13-ziehen-und-werfen)
14. [Parameterübersicht](#14-parameterübersicht)
---
## 1. Einheiten und Koordinaten
| Größe | Einheit |
|---|---|
| Länge | Pixel (px) |
| Zeit | Sekunde (s) |
| Geschwindigkeit | px/s |
| Masse | Fläche × Dichte (px² · Dichte; nur Verhältnisse zählen) |
Der Ursprung liegt oben links in der Welt, **x zeigt nach rechts, y nach unten** – wie auf
dem Bildschirm. Die Schwerkraft ist deshalb `g = (0, +980) px/s²`; bei einem Maßstab von
100 px pro Meter entspricht das 9,8 m/s².
Weil y nach unten zeigt, bedeutet ein positiver Drehwinkel eine Drehung **im
Uhrzeigersinn** auf dem Bildschirm. Die Formeln sind davon unabhängig, solange man
konsequent `a × b = a.x·b.y − a.y·b.x` verwendet.
## 2. Körper: Masse und Trägheit
Jeder Körper (`Body`) hat eine Form, eine Position (Schwerpunkt), eine Geschwindigkeit und
bei Kreisen zusätzlich Winkel und Winkelgeschwindigkeit.
**Masse = Fläche × Dichte.** Damit sind größere Körper bei gleicher Dichte schwerer:
| Form | Fläche | Trägheitsmoment |
|---|---|---|
| Kreis (Radius r) | `π·r²` | `I = ½·m·r²` (homogene Scheibe) |
| Rechteck (halbe Kanten hw, hh) | `4·hw·hh` | unendlich (dreht nie) |
Herleitung des Scheiben-Trägheitsmoments: Die Scheibe zerfällt in dünne Ringe mit Radius s
und Masse `dm = m/(π·r²) · 2π·s·ds`. Jeder Ring trägt `s²·dm` bei:
```text
I = (2m / r²) · ∫₀ʳ s³ ds = (2m / r²) · r⁴/4 = ½·m·r²
```
Rechtecke sind laut Aufgabenstellung **achsenparallel**. Ein unendliches Trägheitsmoment
(also `1/I = 0`) garantiert, dass kein Drehimpuls sie jemals drehen kann.
**Inverse Massen.** Gespeichert werden zusätzlich `1/m` und `1/I`. Wände haben `1/m = 0`
(unendliche Masse). So passen bewegliche und unbewegliche Körper ohne Sonderfälle in
dieselben Formeln: Ein Impuls P ändert die Geschwindigkeit um `P/m` – bei einer Wand um 0.
## 3. Integration mit festem Zeitschritt
### Semi-impliziter Euler
Pro Schritt `dt = 1/120 s`:
```text
v ← v + g·dt (zuerst die Geschwindigkeit …)
x ← x + v·dt (… dann die Position mit der NEUEN Geschwindigkeit)
θ ← θ + ω·dt
```
Der explizite Euler (Position mit der *alten* Geschwindigkeit) gewinnt bei Schwingungen und
Umlaufbahnen systematisch Energie und wird instabil. Der semi-implizite (symplektische)
Euler kostet genauso wenig, bleibt aber energetisch stabil.
Für den freien Fall ergibt sich nach n Schritten exakt `v = n·g·dt` und
`s = g·dt²·n(n+1)/2`. Das weicht nur um den Faktor `(1 + 1/n)` vom analytischen
`s = ½·g·t²` ab; nach 0,5 s sind das 1,7 %. Genau diese Beziehung prüft ein Test.
### Fester Zeitschritt, entkoppelt von der Bildrate
Die Stabilität hängt von `dt` ab. Mit der schwankenden Frame-Dauer als `dt` würde sich die
Simulation auf jedem Rechner anders verhalten. Ein Ruckler von 200 ms ließe Körper außerdem
durch andere hindurchspringen. Deshalb sammelt ein Akkumulator (`FixedTimestep`) die real
vergangene Zeit und gibt sie in festen Portionen weiter:
```text
akkumulator += frame_zeit
while akkumulator ≥ dt: world.step(dt); akkumulator −= dt
alpha = akkumulator / dt (Rest für die Interpolation)
```
- **Obergrenze** `max_steps_per_frame`: Schafft der Rechner die Physik nicht in Echtzeit,
wird der Rückstand verworfen, statt ihn endlos mitzuschleppen ("Spiral of Death").
- **Interpolation:** Gezeichnet wird `lerp(vorige_position, position, alpha)`. Die Physik
läuft mit 120 Hz, der Bildschirm z. B. mit 60 Hz – ohne Interpolation ruckelt die
Bewegung sichtbar.
## 4. Ablauf eines Simulationsschritts
`World.step(dt)` führt nacheinander aus:
```mermaid
flowchart TD
A[Zustand merken<br/>für Interpolation] --> B[Geschwindigkeiten integrieren<br/>v += g·dt]
B --> C[Broadphase<br/>Sweep-and-Prune]
C --> D[Narrowphase<br/>Kontakte Körper–Körper]
D --> E[Berührte Schläfer wecken]
E --> F[Wandkontakte]
F --> G[Impulse lösen<br/>Normale · Reibung · Rollwiderstand]
G --> H[Geschwindigkeit begrenzen]
H --> I[Positionen integrieren<br/>x += v·dt]
I --> J[Positionskorrektur]
J --> K[Notbremse an den Wänden]
K --> L[Schlafmodus aktualisieren]
```
Vor jedem Schritt setzt die Anwendung die Geschwindigkeit eines gezogenen Körpers
(siehe [Abschnitt 13](#13-ziehen-und-werfen)).
## 5. Kollisionserkennung
Jede Prüfung liefert einen **Kontakt** mit
- **Normale n** – Einheitsvektor von Körper A zu Körper B,
- **Eindringtiefe** – Überlappung entlang n (≥ 0),
- **Kontaktpunkt** – Angriffspunkt der Kräfte.
### Broadphase: Sweep-and-Prune
Alle Paare genau zu prüfen kostet n·(n−1)/2 Tests – bei 150 Körpern über 11 000 pro
Schritt. Deshalb werden die umschließenden Boxen nach ihrer linken Kante sortiert. Für jede
Box müssen nur die Nachfolger geprüft werden, die links von ihrer rechten Kante beginnen.
Der Aufwand sinkt auf etwa O(n·log n + k) für k benachbarte Paare. Paare aus zwei
schlafenden Körpern werden übersprungen.
### Kreis–Kreis
```text
d = |p_B − p_A|, Kontakt, wenn d ≤ r_A + r_B
n = (p_B − p_A) / d, Tiefe = r_A + r_B − d
```
Der Kontaktpunkt liegt in der Mitte des Überlappungsbereichs. Liegen die Mittelpunkte exakt
aufeinander, ist die Richtung unbestimmt. Dann wird die feste Ersatznormale (0, −1)
verwendet, statt durch 0 zu teilen.
### Kreis–Rechteck
Den Kreismittelpunkt (relativ zum Rechteck) komponentenweise auf das Rechteck klemmen. Das
liefert den nächstgelegenen Punkt q des Rechtecks:
```text
q = (clamp(dx, −hw, hw), clamp(dy, −hh, hh))
Kontakt, wenn |Mittelpunkt − q| ≤ r
```
Sonderfall: Liegt der Kreismittelpunkt *im* Rechteck, ist q der Mittelpunkt selbst und
liefert keine Richtung. Dann wird der Kreis über die nächstgelegene Kante hinausgeschoben;
die Tiefe ist Radius + Abstand zu dieser Kante.
### Rechteck–Rechteck
Zwei achsenparallele Rechtecke überlappen genau dann, wenn sich ihre Projektionen auf
**beide** Achsen überlappen (Trennachsensatz):
```text
overlap_x = hw_A + hw_B − |dx|, overlap_y = hh_A + hh_B − |dy|
```
Getrennt wird entlang der Achse mit der kleineren Überlappung – das ist der kürzeste Weg
aus der Durchdringung. Bei Gleichstand gewinnt die y-Achse, weil aufeinanderliegende Körper
der häufigste Fall sind.
### Wände als Halbebenen
Boden, Decke und Seiten sind Halbebenen `n·p ≥ offset` (die Normale zeigt in die Welt).
Die Reichweite eines Rechtecks entlang n ist `|n.x|·hw + |n.y|·hh`, die eines Kreises r.
Weil eine Halbebene unendlich dick ist, wird ein Körper **auch dann erkannt, wenn er in
einem Schritt komplett hinter die Wandlinie springt** – er kann nicht hindurchtunneln.
## 6. Impulsbasierte Kollisionsauflösung
### Herleitung des Kollisionsimpulses
Relativgeschwindigkeit des Kontaktpunkts (B relativ zu A), mit den Hebelarmen r vom
Schwerpunkt zum Kontaktpunkt und `ω × r = ω·(−r.y, r.x)`:
```text
v_rel = (v_B + ω_B × r_B) − (v_A + ω_A × r_A), v_n = v_rel · n
```
`v_n < 0` bedeutet: Die Körper nähern sich an. Ein Impuls `P = λ·n` wirkt auf B positiv und
auf A negativ (actio = reactio):
```text
v_A' = v_A − λ·n/m_A ω_A' = ω_A − (r_A × λn)/I_A
v_B' = v_B + λ·n/m_B ω_B' = ω_B + (r_B × λn)/I_B
```
Eingesetzt ändert sich die Normalgeschwindigkeit linear in λ:
```text
v_n' = v_n + λ·K, K = 1/m_A + 1/m_B + (r_A × n)²/I_A + (r_B × n)²/I_B
```
Aus Newtons Stoßgesetz `v_n' = −e·v_n` folgt:
```text
λ = −(1 + e) · v_n / K
```
`1/K` ist die **effektive Masse** des Kontakts – die träge Masse, die der Stoß entlang n
"spürt". Für einen einzelnen zentralen Stoß erhält diese Formel den Gesamtimpuls exakt und
liefert die Lehrbuchergebnisse. Beim elastischen Stoß zweier Kreise mit Massenverhältnis
1 : 3 und Geschwindigkeiten 200 bzw. −100 px/s kommen zum Beispiel exakt −250 und 50 px/s
heraus. Ein Test prüft das.
**Besonderheit dieser Engine:** Bei Kreisen geht die Normale durch den Mittelpunkt
(r ∥ n, also r × n = 0), und Rechtecke drehen sich nie (1/I = 0). Die Drehterme von K
verschwinden daher für die Normale – **Normalimpulse erzeugen kein Drehmoment**. Nur die
Reibung bringt Kreise ins Rollen.
### Viele Kontakte: Sequential Impulses
Liegt ein Körper auf mehreren anderen, beeinflussen sich die Kontakte gegenseitig. Statt
ein Gleichungssystem zu lösen, werden die Kontakte reihum mehrfach aufgelöst
(Gauß-Seidel, `velocity_iterations = 8`). Zwei Details machen das Verfahren korrekt und
stabil:
1. **Akkumulierte Impulse klemmen.** Geklemmt wird der über alle Durchläufe *summierte*
Impuls (`P_n ≥ 0`), nicht der Teilimpuls eines Durchlaufs. Ein späterer Durchlauf darf
einen zu großen früheren Impuls also wieder zurücknehmen. Das Verfahren konvergiert so
gegen die physikalische Lösung, in der Kontakte nur drücken und nie ziehen.
2. **Warm-Starting.** Die akkumulierten Impulse des vorigen Schritts werden zu Beginn erneut
angewendet. Ruhende Kontakte ändern sich kaum, der Startwert ist also schon fast richtig –
Stapel stehen dadurch nach wenigen Durchläufen stabil. Hat sich die Normale stark
gedreht (Skalarprodukt < 0,95), wird der alte Impuls verworfen.
Reihenfolge je Kontakt und Durchlauf: **Normale → Reibung → Rollwiderstand.** Reibung und
Rollwiderstand sind durch den Normalimpuls begrenzt. Werden sie danach gelöst, gelten ihre
Grenzen nach jedem Durchlauf exakt für den aktuellen Normalimpuls.
## 7. Restitution (Elastizität)
Die Stoßzahl `e ∈ [0, 1]` ist das Verhältnis von Trenn- zu Aufprallgeschwindigkeit:
`e = 0` völlig unelastisch, `e = 1` vollkommen elastisch.
- **Energieverlust:** Beim zentralen Stoß geht `ΔE = ½·μ·(1 − e²)·v_rel²` verloren
(μ = m_A·m_B/(m_A + m_B), die reduzierte Masse). Ein Test prüft genau diesen Wert.
- **Sprunghöhe:** Ein fallender Ball erreicht nach dem Aufprall `e²` seiner Fallhöhe
(Energie ∝ v²).
- **Mischregel:** `e = max(e_A, e_B)` – ein Gummiball springt auch von einem Holzklotz
ab. Wände haben `e = 0`, sodass allein das Material des Körpers zählt.
- **Schwellwert (`restitution_threshold` = 50 px/s):** Unterhalb dieser
Aufprallgeschwindigkeit wird nicht abgeprallt. Sonst würden aufliegende Körper durch
winzige Dauer-Hüpfer zittern.
- **Aufprallgeschwindigkeit vom Schrittbeginn:** `v_n` wird aus den Geschwindigkeiten *vor*
Schwerkraft und Warm-Starting bestimmt (`Body.previous_velocity`). Nähme man die
Geschwindigkeit nach der Schwerkraft, prallte ein Ball mit `e·(v + g·dt)` statt `e·v` ab.
Ein vollkommen elastischer Ball (e = 1) gewönne dann mit jedem Aufprall Höhe – Energie
aus dem Nichts. So springt er dagegen immer wieder genau auf seine Fallhöhe; ein Test
prüft das über mehrere Sprünge.
- **Gehaltene Körper prallen nicht ab:** Die Hand fängt den Stoß ab. Andernfalls würde ein
gegen ein Hindernis gedrückter Körper in jedem Schritt zurückprallen, sofort wieder
herangezogen werden und dabei sichtbar zittern.
## 8. Reibung und Rollen
**Coulomb-Reibung:** Entlang der Tangente `t = (−n.y, n.x)` soll die Gleitgeschwindigkeit
verschwinden. Der Reibungsimpuls ist aber begrenzt:
```text
|P_t| ≤ μ · P_n
```
Reicht die Grenze, haften die Körper (Haftreibung). Sonst rutschen sie mit konstanter
Verzögerung `μ·g` (Gleitreibung): Eine Box mit 300 px/s kommt nach `v²/(2μg)` zum Stehen.
Gemischt wird mit dem geometrischen Mittel `μ = √(μ_A·μ_B)` – ein glatter Partner macht
den Kontakt glatt.
**Rollen:** Der Reibungsimpuls greift am Kontaktpunkt an, nicht im Schwerpunkt. Bei Kreisen
erzeugt er daher ein Drehmoment `r × P_t`, die effektive Masse enthält den Term
`(r × t)²/I`. Eine rutschende Scheibe wird gebremst und zugleich in Drehung versetzt, bis
die Rollbedingung `v = ω·r` gilt. Drehimpulserhaltung um den Kontaktpunkt liefert die
Endgeschwindigkeit – unabhängig vom Reibwert:
```text
v_end = v₀ / (1 + I/(m·r²)) = v₀ / 1,5 = ⅔ · v₀
```
Auch das prüft ein Test. Damit man die Drehung sieht, zeichnet der Renderer in jeden Kreis
einen Radius-Strich.
## 9. Rollwiderstand
Reines Rollen verbraucht keine Reibungsenergie – ein rollender Kreis würde ewig rollen. Ein
Rechteck auf einem kleinen Kreis führe darauf wie auf einer Rolle davon. Real verformen sich
Kreis und Unterlage ein wenig, was ein bremsendes Moment erzeugt:
```text
M ≤ μ_r · N · r (μ_r = rolling_resistance = 0,1)
```
Pro Schritt entspricht das einem Drehimpuls `≤ μ_r · P_n · r`. Umgesetzt ist das wie die
Reibung: als begrenzter Impuls, hier aber auf die relative Winkelgeschwindigkeit
`ω_B − ω_A`, mit der effektiven Masse `1/(1/I_A + 1/I_B)`. Weil die Grenze mit der
Normalkraft wächst, kommen schwer belastete "Rollen" schnell zur Ruhe, während ein frei
rollender Ball weit rollt.
Für einen rollenden Ball ergibt sich eine konstante Verzögerung:
```text
a = μ_r·g / (1 + I/(m·r²)) = μ_r·g / 1,5 ≈ 65 px/s²
```
Ein Ball mit 200 px/s rollt so etwa 300 px weit. Der Test misst genau diese Verzögerung.
## 10. Positionskorrektur
Impulse korrigieren nur Geschwindigkeiten. Durch den diskreten Zeitschritt überlappen
Körper trotzdem ein wenig: Ein fallender Körper "landet" meist schon etwas in der
Unterlage. Ohne Korrektur würden Stapel langsam ineinander einsinken.
Die Korrektur verschiebt die **Positionen** direkt ("Projektion"), im Verhältnis der
inversen Massen:
```text
C = factor · max(Tiefe − slop, 0) (höchstens max_position_correction)
p_A −= n · C · (1/m_A) / (1/m_A + 1/m_B)
p_B += n · C · (1/m_B) / (1/m_A + 1/m_B)
```
- **Warum Positionen statt Zusatzgeschwindigkeit (Baumgarte)?** Eine Geschwindigkeit, die
Körper auseinandertreibt, bleibt nach der Trennung erhalten und schleudert sie davon –
besonders bei großen Überlappungen, etwa wenn ein Objekt mitten in einen Haufen gesetzt
wird. Die Projektion erzeugt keine Bewegungsenergie.
- **Slop (0,5 px):** Eine kleine Resttiefe bleibt stehen. So wird der Kontakt im nächsten
Schritt sicher wieder erkannt, statt ständig zwischen "Kontakt" und "kein Kontakt" zu
wechseln – auch das wäre sichtbares Zittern.
- **Aktuelle Tiefe ohne erneute Kollisionsprüfung:** Tiefe bei der Erkennung minus die
seither erfolgte Relativverschiebung entlang n. Das ist exakt für Wände und Rechtecke und
eine sehr gute Näherung für Kreise.
- **Notbremse:** Kein Körper darf mehr als `max_wall_penetration` (12 px) in eine Wand
ragen. Der Wert liegt bewusst über der größten Eindringtiefe, die ein frei fliegender
Körper in einem Schritt erreichen kann (`MAX_SPEED · dt ≈ 10,8 px`). Normale Aufpralle
löst deshalb immer der Solver; die Bremse greift nur in Extremfällen, etwa wenn ein
gezogenes Objekt einen Körper gegen die Wand quetscht. Würde sie schon bei normalen
Aufprallen greifen, verschöbe sie Positionen und fügte dabei Lageenergie hinzu.
## 11. Ruhe ohne Zittern: Schlafmodus
Ein Solver mit endlich vielen Iterationen hinterlässt immer winzige Restbewegungen. Ein
Stapel "atmet" dadurch sichtbar oder kriecht. Deshalb werden ruhende Körper eingefroren:
1. Jeder Körper zählt, wie lange er ununterbrochen fast stillsteht
(`|v| ≤ 4 px/s` und `|ω| ≤ 0,1 rad/s`).
2. Körper, die sich direkt oder über andere berühren, bilden eine **Insel** (Union-Find über
die Kontakte). Wände verbinden keine Inseln, sonst wäre alles am Boden eine einzige Insel.
3. Eine Insel schläft ein, wenn **alle** ihre Körper seit `time_to_sleep` (0,5 s) ruhen.
Jeder Körper merkt sich dabei seine Insel (`Body.sleep_island`).
4. Schlafende Körper werden weder integriert noch aufgelöst – das spart auch Rechenzeit.
5. **Aufwecken – immer die ganze Insel:** bei Berührung durch einen wachen, beweglichen
Körper, beim Greifen, beim Umschalten der Schwerkraft und wenn ein Körper der Insel
gelöscht wird.
Warum Inseln statt einzelner Körper? Sonst könnte ein schlafender Körper unter einem wachen
liegen. Der wache weckt ihn, beide ruhen, einer schläft früher ein, der andere weckt ihn
wieder – ein endloses Hin und Her.
Warum wacht die ganze Insel auf? Zwei schlafende Körper werden nicht auf Kontakte
untereinander geprüft. Wird die unterste Kiste eines schlafenden Stapels geweckt und im
selben Schritt weggestoßen – oder im Pausenmodus weggezogen –, bemerkte sonst niemand, dass
die Kisten darüber ihre Stütze verloren haben. Sie schliefen in der Luft weiter. Weil alle
Körper, die einander berühren, gemeinsam eingeschlafen sind, genügt es, ihre Insel
gemeinsam zu wecken.
Zusammen mit Schwellwert (Abschnitt 7), Slop (Abschnitt 10), Warm-Starting (Abschnitt 6)
und Rollwiderstand (Abschnitt 9) kommen Stapel und Haufen vollständig zur Ruhe. Ein Test
lässt 60 zufällige Körper fallen und verlangt, dass nach 8 s alle schlafen.
**Grenze des Verfahrens:** Wie jeder Schlafmodus mit Geschwindigkeitsschwellen kann er
eine Lage einfrieren, die kein echtes Gleichgewicht ist. Ein Beispiel: Ein Kreis liegt
wenige Pixel außermittig auf einem anderen Kreis. Ist die Kontaktnormale steiler geneigt,
als der Rollwiderstand hält (tan α > μ_r), rollt er ab. Anfangs so langsam, dass er
0,5 s lang unter 4 px/s bleibt und einschläft. Er bleibt dann liegen, bis ihn etwas
berührt, und rollt erst danach weiter. Die Auswirkung beschränkt sich auf wenige Pixel.
Strengere Schwellen würden solche Fälle seltener machen, ließen dafür aber auch ruhende
Haufen später einschlafen.
## 12. Tunneln verhindern
Die Kollisionserkennung ist diskret: Sie sieht nur die Positionen am Ende jedes Schritts.
- **Wände** sind Halbebenen und damit ohnehin tunnelsicher (Abschnitt 5).
- **Körper untereinander:** Fliegen zwei der kleinsten Körper (Radius `MIN_BODY_SIZE` =
12 px) frontal aufeinander zu, darf ihre Relativbewegung pro Schritt die Summe ihrer
Radien nicht übersteigen. Sonst springen die Mittelpunkte aneinander vorbei:
```text
2·v·dt < 2·MIN_BODY_SIZE ⇒ v < MIN_BODY_SIZE / dt = 1440 px/s
```
Mit Sicherheitsfaktor 0,9 ergibt das `MAX_SPEED ≈ 1296 px/s`. Jede Geschwindigkeit wird
nach dem Lösen der Impulse darauf begrenzt, ebenso die Wurfgeschwindigkeit. Aus demselben
Grund ist bei Rechtecken keine *halbe* Kantenlänge kleiner als `MIN_BODY_SIZE`, jede
Seite ist also mindestens 24 px lang.
- Auch die Winkelgeschwindigkeit ist begrenzt (`MAX_SPEED / MIN_BODY_SIZE`).
## 13. Ziehen und Werfen
Ein gezogener Körper wird **nicht** an die Mausposition teleportiert. Dann könnte er durch
andere Körper und Wände hindurchgeschoben werden. Stattdessen setzt `DragController` vor
jedem Physikschritt die Geschwindigkeit in Richtung Ziel:
```text
v = (Ziel − Position) · rate (rate = 60/s; rate·dt ≤ 1 verhindert Überschwingen)
```
Danach löst der Solver die Kontakte ganz normal. Das gezogene Objekt **kollidiert also
weiterhin**: Leichtere Körper werden weggeschoben, Wände und schwere Stapel bremsen es. Das
Ziel wird so geklemmt, dass der Körper ganz in die Welt passt. Gehaltene Körper unterliegen
keiner Schwerkraft ("die Hand trägt sie"), prallen nicht ab ("die Hand fängt den Stoß ab",
siehe [Abschnitt 7](#7-restitution-elastizität)) und schlafen nie ein. Beim Greifen wird
die Schlaf-Insel des Körpers geweckt.
Folge der fehlenden Restitution: Schlägt man mit einem gehaltenen Objekt einen Ball, stößt
es ihn wie ein unelastischer Schläger. Der Ball fliegt deshalb etwas langsamer davon als
bei einem freien, elastischen Stoß (in einer Messung des Reviews um etwa 13 %). Dafür
steht ein gegen ein Hindernis gedrücktes Objekt ruhig.
**Werfen:** Beim Loslassen erhält der Körper die Mausgeschwindigkeit der letzten Frames.
`VelocityTracker` glättet sie in zwei Stufen:
1. **Zeitfenster:** mittlere Geschwindigkeit über die letzten 0,1 s (Weg / Zeit zwischen
ältester und neuester Probe) – unabhängig von der Bildrate.
2. **Exponentielle Glättung:** `v ← v + (v_roh − v)·(1 − e^(−Δt/τ))` mit τ = 0,05 s. Der
Exponentialterm macht die Glättung ebenfalls bildratenunabhängig.
Hält man die Maus vor dem Loslassen still, klingt die Geschwindigkeit schnell ab, und das
Objekt fällt einfach herunter. Im Pausenmodus wird direkt verschoben und nicht geworfen.
## 14. Parameterübersicht
Alle Werte stehen in [`config.py`](../src/sandbox/config.py) und lassen sich dort anpassen.
| Parameter | Wert | Bedeutung |
|---|---|---|
| `gravity` | (0, 980) px/s² | Schwerkraft |
| `time_step` | 1/120 s | fester Zeitschritt |
| `velocity_iterations` | 8 | Solver-Durchläufe pro Schritt |
| `position_iterations` | 2 | Durchläufe der Positionskorrektur |
| `restitution_threshold` | 50 px/s | darunter kein Abprallen |
| `penetration_slop` | 0,5 px | geduldete Resttiefe |
| `position_correction_factor` | 0,4 | Anteil der Korrektur pro Durchlauf |
| `max_position_correction` | 6 px | Obergrenze der Korrektur pro Durchlauf |
| `max_speed` | ≈ 1296 px/s | Tunnel-Schutz |
| `rolling_resistance` | 0,1 | Rollwiderstandsbeiwert μ_r |
| `sleep_linear_speed` / `sleep_angular_speed` | 4 px/s / 0,1 rad/s | Ruheschwellen |
| `time_to_sleep` | 0,5 s | Ruhezeit bis zum Einschlafen |
| `wall_friction` / `wall_restitution` | 0,6 / 0 | Wandmaterial |
| `max_wall_penetration` | 12 px | Notbremse an den Wänden (> `MAX_SPEED · dt`) |