# 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.