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

7.4 KiB
Raw Permalink Blame History

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 (getestet mit 4.6.x)
  • Ein WebSocket-Server auf localhost:9090, Endpoint /ws
  • Nachrichtenformat (JSON, ca. alle 16 ms):
{"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

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)

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

# 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_ispread; 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):

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