demo-game/scripts/main.gd
simon 9107dea6ff Fix WebSocket accel parsing for missing or nested JSON fields.
Avoid crashes when x/y/z are absent or nested under accel/data keys.

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

268 lines
7.7 KiB
GDScript
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.

extends Node2D
enum GameState { CALIBRATION, PLAYING }
@onready var platform: Polygon2D = $Platform
@onready var ball: Node2D = $Ball
@onready var status_label: Label = $UiLayer/StatusLabel
@onready var accel_label: Label = $UiLayer/AccelLabel
@onready var threshold_slider: HSlider = $UiLayer/ThresholdPanel/ThresholdSlider
@onready var threshold_caption: Label = $UiLayer/ThresholdPanel/ThresholdCaption
@onready var sensitivity_slider: HSlider = $UiLayer/ThresholdPanel/SensitivitySlider
@onready var sensitivity_caption: Label = $UiLayer/ThresholdPanel/SensitivityCaption
@onready var threshold_panel: Control = $UiLayer/ThresholdPanel
@onready var calibration_overlay: Control = $UiLayer/CalibrationOverlay
@onready var recalib_button: Button = $UiLayer/RecalibButton
var _socket := WebSocketPeer.new()
var _velocity_x := 0.0
var _accel := Vector3i.ZERO
var _state := GameState.CALIBRATION
var _calibration := AccelCalibration.new()
const WS_URL := "ws://localhost:9090/ws"
const MAX_SPEED := 900.0
const FRICTION := 0.9
const PLATFORM_HALF_WIDTH := 80.0
const MARGIN := 24.0
const PLAY_TOP := 160.0
func _ready() -> void:
_place_platform_bottom_center()
ball.reset_to(_play_area().get_center())
_set_playing_ui_visible(false)
threshold_slider.value_changed.connect(_on_threshold_changed)
_on_threshold_changed(threshold_slider.value)
sensitivity_slider.value_changed.connect(_on_sensitivity_changed)
_on_sensitivity_changed(sensitivity_slider.value)
calibration_overlay.calibration_finished.connect(_on_calibration_finished)
recalib_button.pressed.connect(_restart_calibration)
calibration_overlay.start()
var err := _socket.connect_to_url(WS_URL)
if err != OK:
status_label.text = "WebSocket connect failed: %s" % error_string(err)
else:
status_label.text = "Connecting to %s" % WS_URL
func _physics_process(delta: float) -> void:
_socket.poll()
_update_connection_status()
if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
while _socket.get_available_packet_count() > 0:
var packet := _socket.get_packet().get_string_from_utf8()
_handle_accel_message(packet)
if _state == GameState.CALIBRATION:
if calibration_overlay.is_active():
calibration_overlay.feed_accel(_accel)
_update_accel_label()
return
var eff := _effective_accel()
if eff == 0.0:
_velocity_x *= FRICTION
else:
var target_v := eff * _velocity_scale()
_velocity_x = move_toward(_velocity_x, target_v, maxf(absf(target_v) * 0.35, 120.0) * delta)
_velocity_x = clampf(_velocity_x, -MAX_SPEED, MAX_SPEED)
var next_x := platform.position.x + _velocity_x * delta
platform.position.x = _clamp_x(next_x)
ball.step(delta, platform.position, _velocity_x, _play_area())
_update_accel_label()
func _restart_calibration() -> void:
_state = GameState.CALIBRATION
_velocity_x = 0.0
_set_playing_ui_visible(false)
calibration_overlay.start()
if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
calibration_overlay.notify_connected()
else:
status_label.text = "Kalibrierung tippe „Kalibrierung starten“"
func _on_calibration_finished(calibration: AccelCalibration) -> void:
_calibration = calibration
threshold_slider.value = calibration.suggested_threshold
_on_threshold_changed(calibration.suggested_threshold)
_start_playing()
func _start_playing() -> void:
_state = GameState.PLAYING
_set_playing_ui_visible(true)
_velocity_x = 0.0
_place_platform_bottom_center()
ball.reset_to(_play_area().get_center())
status_label.text = "Kalibriert: Achse %s · Schwellwert %d" % [
_calibration.axis_name(), int(threshold_slider.value)
]
func _set_playing_ui_visible(visible: bool) -> void:
platform.visible = visible
ball.visible = visible
threshold_panel.visible = visible
recalib_button.visible = visible
func _on_threshold_changed(value: float) -> void:
threshold_caption.text = "Schwellwert |%s|: %d" % [_calibration.axis_name(), int(value)]
func _on_sensitivity_changed(value: float) -> void:
sensitivity_caption.text = "Bewegungsstärke: %d %%" % int(value)
func _velocity_scale() -> float:
return sensitivity_slider.value / 100.0
func _calibrated_raw() -> float:
return _calibration.project(_accel)
func _effective_accel() -> float:
var raw := _calibrated_raw()
var centered := raw - _calibration.center
var threshold := threshold_slider.value
var abs_v := absf(centered)
if abs_v <= threshold:
return 0.0
return signf(centered) * (abs_v - threshold)
func _update_connection_status() -> void:
if _state == GameState.CALIBRATION:
match _socket.get_ready_state():
WebSocketPeer.STATE_OPEN:
status_label.text = "Kalibrierung verbunden"
calibration_overlay.notify_connected()
WebSocketPeer.STATE_CONNECTING:
status_label.text = "Kalibrierung verbinde…"
_:
status_label.text = "Kalibrierung keine Verbindung"
return
match _socket.get_ready_state():
WebSocketPeer.STATE_OPEN:
var eff := _effective_accel()
status_label.text = "Connected · Achse %s · vx=%.0f · eff=%.0f" % [
_calibration.axis_name(), _velocity_x, eff
]
WebSocketPeer.STATE_CONNECTING:
status_label.text = "Connecting…"
WebSocketPeer.STATE_CLOSING, WebSocketPeer.STATE_CLOSED:
status_label.text = "Disconnected retrying…"
if _socket.get_ready_state() == WebSocketPeer.STATE_CLOSED:
_socket.connect_to_url(WS_URL)
func _handle_accel_message(text: String) -> void:
var data = JSON.parse_string(text)
if typeof(data) != TYPE_DICTIONARY:
return
if data.get("type") != "accel" or not data.get("success", false):
return
var parsed: Variant = _parse_accel_vector(data)
if parsed == null:
return
_accel = parsed as Vector3i
func _parse_accel_vector(data: Dictionary) -> Variant:
var source: Variant = _accel_fields_dict(data)
if source == null:
return null
return Vector3i(
_to_int(source.get("x")),
_to_int(source.get("y")),
_to_int(source.get("z"))
)
func _accel_fields_dict(data: Dictionary) -> Variant:
if data.has("x") and data.has("y") and data.has("z"):
return data
for key in ["accel", "values", "data", "payload"]:
var nested: Variant = data.get(key)
if nested is Dictionary and nested.has("x") and nested.has("y") and nested.has("z"):
return nested
return null
func _to_int(value: Variant) -> int:
match typeof(value):
TYPE_INT:
return value
TYPE_FLOAT:
return int(value)
TYPE_STRING:
return int(float(value))
return 0
func _play_area() -> Rect2:
var size := get_viewport_rect().size
var bottom_y := size.y - MARGIN
return Rect2(
MARGIN,
PLAY_TOP,
size.x - MARGIN * 2.0,
bottom_y - PLAY_TOP
)
func _place_platform_bottom_center() -> void:
var size := get_viewport_rect().size
platform.position = Vector2(_clamp_x(size.x * 0.5), size.y - 48.0)
func _platform_x_limits() -> Vector2:
var view_w := get_viewport_rect().size.x
return Vector2(PLATFORM_HALF_WIDTH + MARGIN, view_w - PLATFORM_HALF_WIDTH - MARGIN)
func _clamp_x(x: float) -> float:
var limits := _platform_x_limits()
var min_x := limits.x
var max_x := limits.y
if x < min_x:
if _velocity_x < 0.0:
_velocity_x = 0.0
return min_x
if x > max_x:
if _velocity_x > 0.0:
_velocity_x = 0.0
return max_x
return x
func _update_accel_label() -> void:
if _state == GameState.CALIBRATION:
accel_label.text = "x: %d y: %d z: %d\nKalibrierung läuft…" % [_accel.x, _accel.y, _accel.z]
return
var eff := _effective_accel()
accel_label.text = "x: %d y: %d z: %d\nAchse %s%.0f (Schw. %d, eff %.0f)" % [
_accel.x, _accel.y, _accel.z,
_calibration.axis_name(), _calibration.project(_accel),
int(threshold_slider.value), eff
]
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_SIZE_CHANGED:
platform.position.y = get_viewport_rect().size.y - 48.0
platform.position.x = _clamp_x(platform.position.x)
if _state == GameState.PLAYING:
ball.reset_to(_play_area().get_center())