Use frontal steering-wheel roll for Space Shooter movement.

Drop the interactive accel calibration and map left/right ship motion from absolute pod roll while held facing the player.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-26 13:46:34 +02:00
co-authored by Cursor
parent 54d42dade9
commit 130532c4c9
3 changed files with 32 additions and 151 deletions
+2 -56
View File
@@ -1,7 +1,6 @@
[gd_scene format=3 uid="uid://bwayhk38lpl3u"] [gd_scene format=3 uid="uid://bwayhk38lpl3u"]
[ext_resource type="Script" path="res://scripts/space_shooter.gd" id="1_shooter"] [ext_resource type="Script" path="res://scripts/space_shooter.gd" id="1_shooter"]
[ext_resource type="Script" path="res://scripts/calibration_overlay.gd" id="2_calib"]
[node name="SpaceShooter" type="Node2D"] [node name="SpaceShooter" type="Node2D"]
script = ExtResource("1_shooter") script = ExtResource("1_shooter")
@@ -63,18 +62,6 @@ theme_override_font_sizes/font_size = 18
horizontal_alignment = 2 horizontal_alignment = 2
text = "Rechts · —" text = "Rechts · —"
[node name="RecalibButton" type="Button" parent="UiLayer"]
visible = false
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -240.0
offset_top = 12.0
offset_right = -130.0
offset_bottom = 44.0
grow_horizontal = 0
text = "Neu kalibrieren"
[node name="SetupPanel" type="VBoxContainer" parent="UiLayer"] [node name="SetupPanel" type="VBoxContainer" parent="UiLayer"]
anchors_preset = 8 anchors_preset = 8
anchor_left = 0.5 anchor_left = 0.5
@@ -102,7 +89,7 @@ layout_mode = 2
theme_override_colors/font_color = Color(0.6, 0.65, 0.75, 1) theme_override_colors/font_color = Color(0.6, 0.65, 0.75, 1)
theme_override_font_sizes/font_size = 14 theme_override_font_sizes/font_size = 14
horizontal_alignment = 1 horizontal_alignment = 1
text = "Split-Screen · Pod-Zuweisung" text = "Split-Screen · Pod frontal halten, Lenkrad drehen"
[node name="ClientGrid" type="GridContainer" parent="UiLayer/SetupPanel"] [node name="ClientGrid" type="GridContainer" parent="UiLayer/SetupPanel"]
layout_mode = 2 layout_mode = 2
@@ -145,45 +132,4 @@ layout_mode = 2
size_flags_horizontal = 4 size_flags_horizontal = 4
disabled = true disabled = true
theme_override_font_sizes/font_size = 18 theme_override_font_sizes/font_size = 18
text = "Kalibrierung starten" text = "Spiel starten"
[node name="CalibrationOverlay" type="Control" parent="UiLayer"]
visible = false
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 0
script = ExtResource("2_calib")
[node name="HintLabel" type="Label" parent="UiLayer/CalibrationOverlay"]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -420.0
offset_top = 80.0
offset_right = 420.0
offset_bottom = 180.0
grow_horizontal = 2
theme_override_colors/font_color = Color(0.9, 0.92, 0.98, 1)
theme_override_font_sizes/font_size = 20
horizontal_alignment = 1
autowrap_mode = 3
text = "Kalibrierung"
[node name="StartButton" type="Button" parent="UiLayer/CalibrationOverlay"]
layout_mode = 1
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -140.0
offset_top = -120.0
offset_right = 140.0
offset_bottom = -72.0
grow_horizontal = 2
text = "Kalibrierung starten"
+14 -2
View File
@@ -60,12 +60,24 @@ func _process(_delta: float) -> void:
_pod.basis = _pod.basis.slerp(_target_basis, SMOOTH) _pod.basis = _pod.basis.slerp(_target_basis, SMOOTH)
## Lenkrad-Roll in Radiant (links positiv). 0 wenn liegend oder in der Deadzone.
static func steer_roll(accel: Vector3, deadzone_rad: float = deg_to_rad(12.0)) -> float:
if accel.length() < MIN_ACCEL_LEN:
return 0.0
var n := accel.normalized()
var face_t := clampf(1.0 - maxf(n.z, 0.0), 0.0, 1.0)
if face_t < 0.25:
return 0.0
var roll := atan2(-n.x, -n.y)
if absf(roll) <= deadzone_rad:
return 0.0
return signf(roll) * (absf(roll) - deadzone_rad) * face_t
func _basis_from_accel(accel: Vector3) -> Basis: func _basis_from_accel(accel: Vector3) -> Basis:
var n := accel.normalized() var n := accel.normalized()
# 0 = liegend (+Z), 1 = aufgerichtet / gedreht (wenig +Z) # 0 = liegend (+Z), 1 = aufgerichtet / gedreht (wenig +Z)
var face_t := clampf(1.0 - maxf(n.z, 0.0), 0.0, 1.0) var face_t := clampf(1.0 - maxf(n.z, 0.0), 0.0, 1.0)
var b := _basis_flat.slerp(_basis_facing, face_t) var b := _basis_flat.slerp(_basis_facing, face_t)
# Lenkrad-Roll in der Sensor-XY-Ebene:
# facing (0,−1) → 0; links (−1,0) → +π/2; rechts (+1,0) → −π/2
var roll := atan2(-n.x, -n.y) var roll := atan2(-n.x, -n.y)
return Basis(Quaternion(Vector3.BACK, roll * face_t)) * b return Basis(Quaternion(Vector3.BACK, roll * face_t)) * b
+16 -93
View File
@@ -1,6 +1,6 @@
extends Node2D extends Node2D
enum GameState { SETUP, CALIBRATION, PLAYING } enum GameState { SETUP, PLAYING }
enum PlayerSide { LEFT, RIGHT } enum PlayerSide { LEFT, RIGHT }
@@ -18,6 +18,8 @@ const MAX_SHIP_SPEED := 720.0
const SHIP_FRICTION := 0.88 const SHIP_FRICTION := 0.88
const DEFAULT_SENSITIVITY := 12.0 const DEFAULT_SENSITIVITY := 12.0
const ACCEL_SMOOTH := 0.3 const ACCEL_SMOOTH := 0.3
## Maps steer-roll radians onto the old accel scale for ship velocity.
const STEER_GAIN := 10000.0
const BULLET_SPEED := 900.0 const BULLET_SPEED := 900.0
const FIRE_INTERVAL := 0.14 const FIRE_INTERVAL := 0.14
@@ -51,19 +53,14 @@ const LASER_SCALE := 1.4
@onready var right_client_option: OptionButton = $UiLayer/SetupPanel/ClientGrid/RightClientOption @onready var right_client_option: OptionButton = $UiLayer/SetupPanel/ClientGrid/RightClientOption
@onready var start_game_button: Button = $UiLayer/SetupPanel/StartGameButton @onready var start_game_button: Button = $UiLayer/SetupPanel/StartGameButton
@onready var setup_hint_label: Label = $UiLayer/SetupPanel/SetupHintLabel @onready var setup_hint_label: Label = $UiLayer/SetupPanel/SetupHintLabel
@onready var calibration_overlay: Control = $UiLayer/CalibrationOverlay
@onready var recalib_button: Button = $UiLayer/RecalibButton
var _socket := WebSocketPeer.new() var _socket := WebSocketPeer.new()
var _state := GameState.SETUP var _state := GameState.SETUP
var _available_clients: PackedInt32Array = PackedInt32Array() var _available_clients: PackedInt32Array = PackedInt32Array()
var _left_client_id := 0 var _left_client_id := 0
var _right_client_id := 0 var _right_client_id := 0
var _calib_side := PlayerSide.LEFT
var _stream_client_ids: PackedInt32Array = PackedInt32Array() var _stream_client_ids: PackedInt32Array = PackedInt32Array()
var _calibrations: Array[AccelCalibration] = [AccelCalibration.new(), AccelCalibration.new()]
var _thresholds: Array[float] = [3000.0, 3000.0]
var _sensitivities: Array[float] = [DEFAULT_SENSITIVITY, DEFAULT_SENSITIVITY] var _sensitivities: Array[float] = [DEFAULT_SENSITIVITY, DEFAULT_SENSITIVITY]
var _accel_filtered: Array[Vector3] = [Vector3.ZERO, Vector3.ZERO] var _accel_filtered: Array[Vector3] = [Vector3.ZERO, Vector3.ZERO]
var _ship_x: Array[float] = [0.0, 0.0] var _ship_x: Array[float] = [0.0, 0.0]
@@ -89,12 +86,9 @@ func _ready() -> void:
start_game_button.pressed.connect(_on_start_game_pressed) start_game_button.pressed.connect(_on_start_game_pressed)
left_client_option.item_selected.connect(_on_client_selection_changed) left_client_option.item_selected.connect(_on_client_selection_changed)
right_client_option.item_selected.connect(_on_client_selection_changed) right_client_option.item_selected.connect(_on_client_selection_changed)
recalib_button.pressed.connect(_restart_calibration)
calibration_overlay.calibration_finished.connect(_on_calibration_finished)
_hide_gameplay() _hide_gameplay()
setup_panel.visible = true setup_panel.visible = true
recalib_button.visible = false
_state = GameState.SETUP _state = GameState.SETUP
_init_visuals() _init_visuals()
@@ -165,10 +159,6 @@ func _physics_process(delta: float) -> void:
match _state: match _state:
GameState.SETUP: GameState.SETUP:
return return
GameState.CALIBRATION:
if calibration_overlay.is_active():
calibration_overlay.feed_accel(_accel_for_side(_calib_side))
return
GameState.PLAYING: GameState.PLAYING:
_update_boost_state(delta) _update_boost_state(delta)
_update_stars(delta) _update_stars(delta)
@@ -259,44 +249,17 @@ func _on_client_selection_changed(_idx: int) -> void:
elif _left_client_id == _right_client_id: elif _left_client_id == _right_client_id:
setup_hint_label.text = "Links und rechts müssen verschiedene Pods sein." setup_hint_label.text = "Links und rechts müssen verschiedene Pods sein."
else: else:
setup_hint_label.text = "Pod #%d spielt links · Pod #%d spielt rechts" % [_left_client_id, _right_client_id] setup_hint_label.text = (
"Pod #%d links · Pod #%d rechts · frontal halten, Lenkrad drehen"
% [_left_client_id, _right_client_id]
)
func _on_start_game_pressed() -> void: func _on_start_game_pressed() -> void:
if _left_client_id <= 0 or _right_client_id <= 0 or _left_client_id == _right_client_id: if _left_client_id <= 0 or _right_client_id <= 0 or _left_client_id == _right_client_id:
return return
setup_panel.visible = false setup_panel.visible = false
_start_calibration(PlayerSide.LEFT) _begin_playing()
func _start_calibration(side: PlayerSide) -> void:
_state = GameState.CALIBRATION
_calib_side = side
_hide_gameplay()
recalib_button.visible = false
var cid := _client_id_for_side(side)
calibration_overlay.set_context_label(
"Kalibrierung %s · Pod #%d\nZylinder senkrecht halten, dann links/rechts schwanken" % [
_side_name(side), cid
]
)
calibration_overlay.start()
_set_stream_for_calibration(cid)
if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
calibration_overlay.notify_connected()
func _on_calibration_finished(calibration: AccelCalibration) -> void:
_calibrations[_calib_side] = calibration
_thresholds[_calib_side] = calibration.suggested_threshold
_sensitivities[_calib_side] = DEFAULT_SENSITIVITY
if _calib_side == PlayerSide.LEFT:
_start_calibration(PlayerSide.RIGHT)
else:
_begin_playing()
func _begin_playing() -> void: func _begin_playing() -> void:
@@ -312,33 +275,20 @@ func _begin_playing() -> void:
_boost_cooldown_left = [0.0, 0.0] _boost_cooldown_left = [0.0, 0.0]
_boost_led_active = [false, false] _boost_led_active = [false, false]
_led_update_timer = 0.0 _led_update_timer = 0.0
_sensitivities = [DEFAULT_SENSITIVITY, DEFAULT_SENSITIVITY]
for side in [PlayerSide.LEFT, PlayerSide.RIGHT]: for side in [PlayerSide.LEFT, PlayerSide.RIGHT]:
_ship_x[side] = _side_play_rect(side).get_center().x _ship_x[side] = _side_play_rect(side).get_center().x
_show_gameplay() _show_gameplay()
recalib_button.visible = true
_enable_both_streams() _enable_both_streams()
_update_score_labels() _update_score_labels()
status_label.text = ( status_label.text = (
"Pod #%d · %s · Schwelle %.0f | Pod #%d · %s · Schwelle %.0f" "Pod frontal halten · wie Lenkrad drehen · Tap = Boost (%ds)"
% [ % int(BOOST_DURATION_SEC)
_left_client_id,
_calibrations[PlayerSide.LEFT].axis_name(),
_thresholds[PlayerSide.LEFT],
_right_client_id,
_calibrations[PlayerSide.RIGHT].axis_name(),
_thresholds[PlayerSide.RIGHT],
]
) )
func _restart_calibration() -> void:
_clear_entities()
_hide_gameplay()
_start_calibration(PlayerSide.LEFT)
func _show_gameplay() -> void: func _show_gameplay() -> void:
for ship in _ships: for ship in _ships:
ship.visible = true ship.visible = true
@@ -359,14 +309,6 @@ func _clear_entities() -> void:
_bullets.clear() _bullets.clear()
func _set_stream_for_calibration(client_id: int) -> void:
_disable_all_streams()
_stream_client_ids = PackedInt32Array([client_id])
_send({"type": "set_input_stream", "client_id": client_id, "enable": true})
_send({"type": "set_tap_notify", "client_id": client_id, "single": false, "double_tap": false, "triple": false})
_send({"type": "set_stream", "enable": true, "interval_ms": STREAM_INTERVAL_MS})
func _enable_both_streams() -> void: func _enable_both_streams() -> void:
_disable_all_streams() _disable_all_streams()
_stream_client_ids = PackedInt32Array([_left_client_id, _right_client_id]) _stream_client_ids = PackedInt32Array([_left_client_id, _right_client_id])
@@ -752,14 +694,10 @@ func _update_stars(delta: float) -> void:
func _effective_accel(side: int) -> float: func _effective_accel(side: int) -> float:
var cal: AccelCalibration = _calibrations[side] # Frontal halten, Lenkrad-Roll: links positiv → Schiff nach links (negativ).
var raw := cal.project(_accel_for_side(side)) var a := _accel_for_side(side)
var centered := raw - cal.center var roll := OrientationGizmo.steer_roll(Vector3(a))
var threshold := _thresholds[side] return -roll * STEER_GAIN
var abs_v := absf(centered)
if abs_v <= threshold:
return 0.0
return signf(centered) * (abs_v - threshold)
func _velocity_scale(side: int) -> float: func _velocity_scale(side: int) -> float:
@@ -782,10 +720,6 @@ func _client_id_for_side(side: PlayerSide) -> int:
return _left_client_id if side == PlayerSide.LEFT else _right_client_id return _left_client_id if side == PlayerSide.LEFT else _right_client_id
func _side_name(side: PlayerSide) -> String:
return "links" if side == PlayerSide.LEFT else "rechts"
func _update_connection_status() -> void: func _update_connection_status() -> void:
match _state: match _state:
GameState.SETUP: GameState.SETUP:
@@ -796,22 +730,11 @@ func _update_connection_status() -> void:
status_label.text = "Verbinde…" status_label.text = "Verbinde…"
_: _:
status_label.text = "Getrennt – erneuter Verbindungsversuch…" status_label.text = "Getrennt – erneuter Verbindungsversuch…"
GameState.CALIBRATION:
match _socket.get_ready_state():
WebSocketPeer.STATE_OPEN:
status_label.text = "Kalibrierung %s · Pod #%d" % [
_side_name(_calib_side), _client_id_for_side(_calib_side)
]
calibration_overlay.notify_connected()
WebSocketPeer.STATE_CONNECTING:
status_label.text = "Kalibrierung – verbinde…"
_:
status_label.text = "Kalibrierung – keine Verbindung"
GameState.PLAYING: GameState.PLAYING:
match _socket.get_ready_state(): match _socket.get_ready_state():
WebSocketPeer.STATE_OPEN: WebSocketPeer.STATE_OPEN:
status_label.text = ( status_label.text = (
"Tap/Doppel-Tap = Boost (%ds) · x2 Schaden · ♥×%d · Cooldown %ds" "Frontal halten · Lenkrad drehen · Tap = Boost (%ds) · ♥×%d · CD %ds"
% [int(BOOST_DURATION_SEC), MAX_LIVES, int(BOOST_COOLDOWN_SEC)] % [int(BOOST_DURATION_SEC), MAX_LIVES, int(BOOST_COOLDOWN_SEC)]
) )
WebSocketPeer.STATE_CONNECTING: WebSocketPeer.STATE_CONNECTING: