Add README with project overview and calibration technical docs.

Documents WebSocket protocol, architecture, calibration math, and gameplay.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
simon 2026-05-28 21:56:28 +02:00
parent bc25226a31
commit 84b1a89f75

199
README.md Normal file
View File

@ -0,0 +1,199 @@
# 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.