demo-game/scripts/pong.gd
simon 70e66f4d60 Adapt game clients to the unified WebSocket input stream.
Replace separate accel/tap push handling with input messages, set_input_stream, and a single set_stream on port 8081.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 13:47:41 +02:00

324 lines
9.0 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
@onready var menu_button: Button = $UiLayer/MenuButton
var _socket := WebSocketPeer.new()
var _velocity_x := 0.0
var _accel := Vector3i.ZERO
var _state := GameState.CALIBRATION
var _calibration := AccelCalibration.new()
var _client_id := 0
var _stream_ready := false
const WS_URL := "ws://localhost:8081/ws"
const STREAM_INTERVAL_MS := 16
const MENU_SCENE := "res://scenes/menu.tscn"
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)
menu_button.pressed.connect(_return_to_menu)
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 _return_to_menu() -> void:
_disable_input_stream()
get_tree().change_scene_to_file(MENU_SCENE)
func _exit_tree() -> void:
_disable_input_stream()
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_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 _send(payload: Dictionary) -> void:
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
_socket.send_text(JSON.stringify(payload))
func _disable_input_stream() -> void:
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
_send({"type": "set_stream", "enable": false, "interval_ms": STREAM_INTERVAL_MS})
if _client_id > 0:
_send({"type": "set_input_stream", "client_id": _client_id, "enable": false})
_stream_ready = false
func _enable_input_stream() -> void:
if _client_id <= 0:
return
_send({"type": "set_input_stream", "client_id": _client_id, "enable": true})
_send({"type": "set_stream", "enable": true, "interval_ms": STREAM_INTERVAL_MS})
_stream_ready = true
func _handle_message(text: String) -> void:
var data = JSON.parse_string(text)
if typeof(data) != TYPE_DICTIONARY:
return
match data.get("type", ""):
"hello":
_send({"type": "list_clients"})
"client_list":
_on_client_list(data)
"input":
_on_input(data)
func _on_client_list(data: Dictionary) -> void:
if not data.get("success", false):
return
for entry in data.get("clients", []):
if typeof(entry) != TYPE_DICTIONARY:
continue
if not entry.get("available", false):
continue
var cid := int(entry.get("id", 0))
if cid > 0:
_client_id = cid
break
if _client_id > 0 and not _stream_ready:
_enable_input_stream()
func _on_input(data: Dictionary) -> void:
if not data.get("success", false):
return
for client in data.get("clients", []):
if typeof(client) != TYPE_DICTIONARY:
continue
var cid := int(client.get("client_id", 0))
if _client_id > 0 and cid != _client_id:
continue
if not client.get("valid", false):
continue
_accel = Vector3i(
_to_int(client.get("x")),
_to_int(client.get("y")),
_to_int(client.get("z"))
)
return
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())