demo-game/README.md
simon 84b1a89f75 Add README with project overview and calibration technical docs.
Documents WebSocket protocol, architecture, calibration math, and gameplay.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 21:56:28 +02:00

200 lines
7.4 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.

# alox_test_game
Ein einfaches 2D-Pong-Spiel in **Godot 4.6**, gesteuert über Beschleunigungsdaten eines externen Geräts per **WebSocket**. Die Plattform unten wird horizontal bewegt; ein Ball prallt an Wänden, am Boden und an der Plattform ab.
## Voraussetzungen
- [Godot Engine 4.6](https://godotengine.org/) (getestet mit 4.6.x)
- Ein WebSocket-Server auf `localhost:9090`, Endpoint `/ws`
- Nachrichtenformat (JSON, ca. alle 16 ms):
```json
{"type":"accel","t":1779991116528186251,"success":true,"x":6772,"y":-2675,"z":14704}
```
Nur Nachrichten mit `"type": "accel"` und `"success": true` werden ausgewertet. Die Werte `x`, `y`, `z` sind ganzzahlige Rohwerte des Sensors (keine SI-Einheiten im Spiel).
## Start
```bash
godot --path /pfad/zu/alox-test-game
```
Beim Start läuft zuerst die **Kalibrierung**, danach das Spiel. Über **„Kalibrierung neu starten“** (oben rechts im Spiel) oder **„Kalibrierung starten“** (im Kalibrierungs-Overlay) kann die Kalibrierung jederzeit wiederholt werden.
## Projektstruktur
| Pfad | Rolle |
|------|--------|
| `scenes/main.tscn` | Hauptszene (UI, Plattform, Ball, Kalibrierungs-Overlay) |
| `scripts/main.gd` | WebSocket, Spielzustand, Plattformsteuerung, Ball-Update |
| `scripts/ball.gd` | Ballphysik (Wände, Boden, Plattformkollision) |
| `scripts/calibration_overlay.gd` | Kalibrierungs-UI und Phasenablauf |
| `scripts/accel_calibration.gd` | Auswertung der Messdaten → Achse, Vorzeichen, Schwellwert |
| `project.godot` | Projekteinstellungen, Viewport 1152×648 |
## Architektur (Überblick)
```mermaid
flowchart LR
WS[WebSocket Server :9090] -->|JSON accel| Main[main.gd]
Main -->|Roh-Vector3i| CalOverlay[calibration_overlay.gd]
CalOverlay -->|Samples L/R| AccelCal[AccelCalibration]
AccelCal -->|axis sign center threshold| Main
Main -->|effektive Steuerung| Platform[Plattform]
Main --> Ball[ball.gd]
```
**Zustände in `main.gd`:**
- `CALIBRATION` Overlay sichtbar, Plattform/Ball/Slider ausgeblendet, Messwerte werden nur für die Kalibrierung gesammelt.
- `PLAYING` normales Pong mit kalibrierter Achse und einstellbaren Slidern.
---
## Kalibrierung (technisch)
### Ziel
Das Gerät kann beliebig gehalten werden; Gravitation und Sensorausrichtung liefern oft große, konstante Anteile auf **x**, **y** und **z**. Die Kalibrierung ermittelt automatisch:
1. **Welche Achse** (`x`, `y` oder `z`) am stärksten auf *horizontale* Neigung reagiert, wenn der Nutzer dem Bildschirmanimation folgt.
2. **Welches Vorzeichen** „nach rechts neigen“ auf dem Bildschirm bedeutet.
3. **Ruhewert (`center`)** der gewählten Achse (Median aller Kalibrierungs-Samples).
4. **Vorgeschlagenen Schwellwert** für die Deadzone im Spiel (aus Streuung der Samples).
Ohne Kalibrierung wäre feste Nutzung von `accel.x` oft falsch oder würde in Ruhe dauerhaft eine Seitenrichtung erzeugen.
### Ablauf für den Nutzer
1. **WAITING** Verbindung zu `ws://localhost:9090/ws` (optional manuell starten).
2. **RIGHT** (4,5 s) Ein Kreis mit Nadel (Strich nach oben) bewegt sich mit Smoothstep von der Bildschirmmitte **140 px nach rechts**. Der Nutzer neigt das Gerät passend nach rechts.
3. **LEFT** (4,5 s) Gleiche Animation **140 px nach links** vom Zentrum; Nutzer neigt nach links.
4. **DONE** `AccelCalibration.analyze()`; bei Erfolg Signal `calibration_finished` → Spielstart.
Visuelle Animation (`calibration_overlay.gd`):
- Position des Kreises: `viewport_center + guide_offset`
- `guide_offset.x = ±TRAVEL_PX * smoothstep(t)` mit `t ∈ [0, 1]` über `PHASE_DURATION`
- Smoothstep: `t² * (3 - 2t)` (kein lineares Ruckeln)
Während RIGHT/LEFT ruft `main.gd` bei jedem empfangenen Accel-Paket `feed_accel(_accel)` auf; Samples landen in getrennten Arrays.
### Datensammlung
```gdscript
# accel_calibration.gd
record_right(accel) _samples_right
record_left(accel) _samples_left
```
Mindestens **20 Samples pro Phase** (`is_ready()`), typisch deutlich mehr bei ~60 Hz und 4,5 s Laufzeit.
### Auswertung (`analyze()`)
**Schritt 1 Mittelwerte pro Phase**
\[
\bar{a}_{\text{right}} = \frac{1}{N_r}\sum_{i} a_i,\quad
\bar{a}_{\text{left}} = \frac{1}{N_l}\sum_{i} a_i
\]
mit \(a_i = (x_i, y_i, z_i)\) als `Vector3`.
**Schritt 2 Differenzvektor**
\[
\Delta = \bar{a}_{\text{right}} - \bar{a}_{\text{left}}
\]
Die Achse mit dem **größten Betrag** in \(\Delta\) wird gewählt:
\[
\text{axis} = \arg\max_{c \in \{x,y,z\}} |\Delta_c|
\]
**Schritt 3 Vorzeichen**
\[
\text{sign} = \begin{cases}
+1 & \text{if } \Delta_{\text{axis}} \geq 0 \\
-1 & \text{if } \Delta_{\text{axis}} < 0
\end{cases}
\]
Damit rechts neigen im Kalibrierungsrecht zu **positiver** Projektion auf die Steuerachse wird.
**Schritt 4 Projektion (kalibrierter Skalar)**
Für jeden Rohvektor \(a\):
\[
p(a) = \text{sign} \cdot a_{\text{axis}}
\]
Implementiert als `project(accel)`.
**Schritt 5 Ruhewert `center`**
Alle Samples aus RIGHT- und LEFT-Phase werden projiziert, sortiert; **`center` = Median** dieser Werte. Der Median ist robuster gegen Ausreißer als der Mittelwert, wenn der Nutzer in einer Phase ungleichmäßig neigt.
**Schritt 6 Schwellwert-Vorschlag**
Für jede Projektion \(p_i\):
\[
d_i = |p_i - \text{center}|
\]
Median von \(d_i\) `spread`; vorgeschlagener Schwellwert:
\[
\text{threshold} = \mathrm{clamp}(\text{spread} \cdot 1.35,\ 200,\ 12000)
\]
Der Faktor 1.35 lässt etwas Spielraum über die typische Ruhestreuung; Grenzen verhindern extremes Verhalten.
### Nutzung im Spiel
Nach der Kalibrierung in `_effective_accel()` (`main.gd`):
```gdscript
raw = calibration.project(_accel) # p(a)
centered = raw - calibration.center
if abs(centered) <= threshold_slider:
return 0.0 # Deadzone
return sign(centered) * (abs(centered) - threshold)
```
Die Plattformgeschwindigkeit folgt dem effektiven Wert mit Skalierung über den Slider **Bewegungsstärke** (1100 % Faktor 0.011.0 auf die Zielgeschwindigkeit), Reibung in Ruhe, Begrenzung an den Bildschirmrändern.
### Grenzen und Annahmen
| Thema | Verhalten |
|--------|-----------|
| Nur eine Steuerachse | Es wird genau **eine** von x/y/z genutzt; kombinierte Neigung auf mehreren Achsen wird nicht modelliert. |
| Korrelation statt Regression | Achsenwahl über \(|\Delta_x|, |\Delta_y|, |\Delta_z|\), keine Kovarianzmatrix / PCA. |
| Symmetrische Phasen | Rechts- und Links-Phase gleich lang; Nutzer soll in beiden Phasen ähnlich stark neigen. |
| Keine Persistenz | Kalibrierung gilt nur bis Neustart der Anwendung (nicht in Datei gespeichert). |
| WebSocket optional in WAITING | Kalibrierung starten ohne Live-Daten liefert keine sinnvollen Samples `analyze()` schlägt fehl (< 20 Samples). |
### Kalibrierung neu starten
`main.gd` `_restart_calibration()`:
- Zustand zurück auf `CALIBRATION`
- Spiel-UI ausblenden, `calibration_overlay.start()`
- Bei offenem WebSocket automatisch `notify_connected()` Phasen starten
---
## Spielmechanik (Kurz)
- **Plattform:** horizontale Geschwindigkeit aus kalibriertem Accel (siehe oben).
- **Ball** (`ball.gd`): konstante Geschwindigkeit, Reflexion an linken/rechten/oben Wänden und am **unteren Spielfeldrand** (Viewport-Unterkante minus Rand). Kollision mit der **Oberseite** der Plattform; seitlicher Schwung durch `platform_vx * 0.45`.
- **Slider:** Schwellwert (Deadzone auf kalibrierter Achse), Bewegungsstärke (Empfindlichkeit).
## Lizenz / Hinweise
Privates Testprojekt; Server-Implementierung für `localhost:9090` ist nicht Teil dieses Repositories.