diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ccb0a7 --- /dev/null +++ b/README.md @@ -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** (1–100 % → Faktor 0.01–1.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.