Initial commit: Godot Pong game with WebSocket accelerometer control.
Includes platform steering via calibrated accel axes, ball physics, calibration overlay with axis detection, and runtime tuning sliders. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
bc25226a31
4
.editorconfig
Normal file
4
.editorconfig
Normal file
@ -0,0 +1,4 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
1
icon.svg
Normal file
1
icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
43
icon.svg.import
Normal file
43
icon.svg.import
Normal file
@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://clndwhsts0u65"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
30
project.godot
Normal file
30
project.godot
Normal file
@ -0,0 +1,30 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="alox_test_game"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[display]
|
||||
|
||||
window/stretch/mode="canvas_items"
|
||||
|
||||
[physics]
|
||||
|
||||
3d/physics_engine="Jolt Physics"
|
||||
|
||||
[rendering]
|
||||
|
||||
rendering_device/driver.windows="d3d12"
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
149
scenes/main.tscn
Normal file
149
scenes/main.tscn
Normal file
@ -0,0 +1,149 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://bmain2dscene01"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
||||
[ext_resource type="Script" path="res://scripts/ball.gd" id="2_ball"]
|
||||
[ext_resource type="Script" path="res://scripts/calibration_overlay.gd" id="3_calib"]
|
||||
|
||||
[node name="Main" type="Node2D"]
|
||||
script = ExtResource("1_main")
|
||||
|
||||
[node name="BgLayer" type="CanvasLayer" parent="."]
|
||||
layer = -10
|
||||
|
||||
[node name="Background" type="ColorRect" parent="BgLayer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0.08, 0.09, 0.14, 1)
|
||||
|
||||
[node name="Platform" type="Polygon2D" parent="."]
|
||||
color = Color(0.25, 0.72, 0.95, 1)
|
||||
polygon = PackedVector2Array(-80, -12, 80, -12, 80, 12, -80, -12)
|
||||
position = Vector2(576, 600)
|
||||
|
||||
[node name="Ball" type="Node2D" parent="."]
|
||||
position = Vector2(576, 320)
|
||||
script = ExtResource("2_ball")
|
||||
|
||||
[node name="UiLayer" type="CanvasLayer" parent="."]
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="UiLayer"]
|
||||
offset_left = 16.0
|
||||
offset_top = 12.0
|
||||
offset_right = 560.0
|
||||
offset_bottom = 40.0
|
||||
theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Starting…"
|
||||
|
||||
[node name="ThresholdPanel" type="VBoxContainer" parent="UiLayer"]
|
||||
offset_left = 16.0
|
||||
offset_top = 48.0
|
||||
offset_right = 420.0
|
||||
offset_bottom = 148.0
|
||||
|
||||
[node name="ThresholdCaption" type="Label" parent="UiLayer/ThresholdPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Schwellwert: 3000"
|
||||
|
||||
[node name="ThresholdSlider" type="HSlider" parent="UiLayer/ThresholdPanel"]
|
||||
custom_minimum_size = Vector2(380, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
min_value = 0.0
|
||||
max_value = 15000.0
|
||||
step = 50.0
|
||||
value = 3000.0
|
||||
tick_count = 16
|
||||
ticks_on_borders = true
|
||||
|
||||
[node name="SensitivityCaption" type="Label" parent="UiLayer/ThresholdPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Bewegungsstärke: 6 %"
|
||||
|
||||
[node name="SensitivitySlider" type="HSlider" parent="UiLayer/ThresholdPanel"]
|
||||
custom_minimum_size = Vector2(380, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
min_value = 1.0
|
||||
max_value = 100.0
|
||||
step = 1.0
|
||||
value = 6.0
|
||||
tick_count = 11
|
||||
ticks_on_borders = true
|
||||
|
||||
[node name="AccelLabel" type="Label" parent="UiLayer"]
|
||||
anchors_preset = 3
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -320.0
|
||||
offset_top = -72.0
|
||||
offset_right = -16.0
|
||||
offset_bottom = -20.0
|
||||
grow_horizontal = 0
|
||||
grow_vertical = 0
|
||||
theme_override_colors/font_color = Color(0.9, 0.92, 0.55, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
horizontal_alignment = 2
|
||||
text = "x: — y: — z: —"
|
||||
|
||||
[node name="RecalibButton" type="Button" parent="UiLayer"]
|
||||
visible = false
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -220.0
|
||||
offset_top = 12.0
|
||||
offset_right = -16.0
|
||||
offset_bottom = 44.0
|
||||
grow_horizontal = 0
|
||||
text = "Kalibrierung neu starten"
|
||||
|
||||
[node name="CalibrationOverlay" type="Control" parent="UiLayer"]
|
||||
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("3_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 = -360.0
|
||||
offset_top = 80.0
|
||||
offset_right = 360.0
|
||||
offset_bottom = 140.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"
|
||||
110
scripts/accel_calibration.gd
Normal file
110
scripts/accel_calibration.gd
Normal file
@ -0,0 +1,110 @@
|
||||
class_name AccelCalibration
|
||||
extends RefCounted
|
||||
|
||||
enum Axis { X, Y, Z }
|
||||
|
||||
var axis: Axis = Axis.X
|
||||
var sign: float = 1.0
|
||||
var center: float = 0.0
|
||||
var suggested_threshold: float = 3000.0
|
||||
|
||||
var _samples_right: Array[Vector3] = []
|
||||
var _samples_left: Array[Vector3] = []
|
||||
|
||||
|
||||
func reset() -> void:
|
||||
_samples_right.clear()
|
||||
_samples_left.clear()
|
||||
|
||||
|
||||
func record_right(accel: Vector3i) -> void:
|
||||
_samples_right.append(Vector3(accel))
|
||||
|
||||
|
||||
func record_left(accel: Vector3i) -> void:
|
||||
_samples_left.append(Vector3(accel))
|
||||
|
||||
|
||||
func is_ready() -> bool:
|
||||
return _samples_right.size() >= 20 and _samples_left.size() >= 20
|
||||
|
||||
|
||||
func analyze() -> bool:
|
||||
if not is_ready():
|
||||
return false
|
||||
|
||||
var mean_r := _mean(_samples_right)
|
||||
var mean_l := _mean(_samples_left)
|
||||
var diff := mean_r - mean_l
|
||||
|
||||
var diffs := [absf(diff.x), absf(diff.y), absf(diff.z)]
|
||||
axis = Axis.X
|
||||
var best: float = diffs[0]
|
||||
if diffs[1] > best:
|
||||
best = diffs[1]
|
||||
axis = Axis.Y
|
||||
if diffs[2] > best:
|
||||
best = diffs[2]
|
||||
axis = Axis.Z
|
||||
|
||||
var delta := _component(diff, axis)
|
||||
sign = 1.0 if delta >= 0.0 else -1.0
|
||||
|
||||
var all_samples: Array[Vector3] = []
|
||||
all_samples.append_array(_samples_right)
|
||||
all_samples.append_array(_samples_left)
|
||||
|
||||
var projections: Array[float] = []
|
||||
for sample in all_samples:
|
||||
projections.append(_component(sample, axis) * sign)
|
||||
projections.sort()
|
||||
center = projections[projections.size() / 2]
|
||||
|
||||
var deviations: Array[float] = []
|
||||
for value in projections:
|
||||
deviations.append(absf(value - center))
|
||||
|
||||
deviations.sort()
|
||||
var median_idx := deviations.size() / 2
|
||||
var spread := deviations[median_idx] if not deviations.is_empty() else 500.0
|
||||
suggested_threshold = clampf(spread * 1.35, 200.0, 12000.0)
|
||||
return true
|
||||
|
||||
|
||||
func project(accel: Vector3i) -> float:
|
||||
return _component(Vector3(accel), axis) * sign
|
||||
|
||||
|
||||
func axis_name() -> String:
|
||||
match axis:
|
||||
Axis.X:
|
||||
return "x"
|
||||
Axis.Y:
|
||||
return "y"
|
||||
Axis.Z:
|
||||
return "z"
|
||||
return "?"
|
||||
|
||||
|
||||
func _mean(samples: Array[Vector3]) -> Vector3:
|
||||
if samples.is_empty():
|
||||
return Vector3.ZERO
|
||||
var sum := Vector3.ZERO
|
||||
for s in samples:
|
||||
sum += s
|
||||
return sum / float(samples.size())
|
||||
|
||||
|
||||
func _mean_scalar(samples: Array[Vector3], ax: Axis) -> float:
|
||||
return _component(_mean(samples), ax)
|
||||
|
||||
|
||||
func _component(v: Vector3, ax: Axis) -> float:
|
||||
match ax:
|
||||
Axis.X:
|
||||
return v.x
|
||||
Axis.Y:
|
||||
return v.y
|
||||
Axis.Z:
|
||||
return v.z
|
||||
return 0.0
|
||||
1
scripts/accel_calibration.gd.uid
Normal file
1
scripts/accel_calibration.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c0x3puwhmk8f3
|
||||
81
scripts/ball.gd
Normal file
81
scripts/ball.gd
Normal file
@ -0,0 +1,81 @@
|
||||
extends Node2D
|
||||
|
||||
const RADIUS := 10.0
|
||||
const SPEED := 420.0
|
||||
const PLATFORM_HALF := Vector2(80.0, 12.0)
|
||||
|
||||
var velocity := Vector2.ZERO
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
velocity = Vector2(320.0, -360.0).normalized() * SPEED
|
||||
|
||||
|
||||
func reset_to(center: Vector2) -> void:
|
||||
position = center
|
||||
var dir := Vector2(randf_range(-0.6, 0.6), -1.0).normalized()
|
||||
velocity = dir * SPEED
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func step(delta: float, platform_pos: Vector2, platform_vx: float, bounds: Rect2) -> void:
|
||||
position += velocity * delta
|
||||
_bounce_walls(bounds)
|
||||
_bounce_platform_top(platform_pos, platform_vx)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _bounce_walls(bounds: Rect2) -> void:
|
||||
var min_x := bounds.position.x + RADIUS
|
||||
var max_x := bounds.end.x - RADIUS
|
||||
var min_y := bounds.position.y + RADIUS
|
||||
var max_y := bounds.end.y - RADIUS
|
||||
|
||||
if position.x < min_x:
|
||||
position.x = min_x
|
||||
velocity.x = absf(velocity.x)
|
||||
elif position.x > max_x:
|
||||
position.x = max_x
|
||||
velocity.x = -absf(velocity.x)
|
||||
|
||||
if position.y < min_y:
|
||||
position.y = min_y
|
||||
velocity.y = absf(velocity.y)
|
||||
elif position.y > max_y:
|
||||
position.y = max_y
|
||||
velocity.y = -absf(velocity.y)
|
||||
|
||||
_normalize_speed()
|
||||
|
||||
|
||||
func _bounce_platform_top(platform_pos: Vector2, platform_vx: float) -> void:
|
||||
var left := platform_pos.x - PLATFORM_HALF.x
|
||||
var right := platform_pos.x + PLATFORM_HALF.x
|
||||
var top := platform_pos.y - PLATFORM_HALF.y
|
||||
var bottom := platform_pos.y + PLATFORM_HALF.y
|
||||
|
||||
if position.x < left - RADIUS or position.x > right + RADIUS:
|
||||
return
|
||||
if velocity.y <= 0.0:
|
||||
return
|
||||
if position.y + RADIUS < top:
|
||||
return
|
||||
if position.y - RADIUS > bottom + 4.0:
|
||||
return
|
||||
|
||||
position.y = top - RADIUS
|
||||
velocity.y = -absf(velocity.y)
|
||||
velocity.x += platform_vx * 0.45
|
||||
_normalize_speed()
|
||||
|
||||
|
||||
func _normalize_speed() -> void:
|
||||
if velocity.length_squared() < 1.0:
|
||||
velocity = Vector2.RIGHT * SPEED
|
||||
else:
|
||||
velocity = velocity.normalized() * SPEED
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
draw_circle(Vector2.ZERO, RADIUS, Color(1.0, 0.88, 0.35))
|
||||
draw_arc(Vector2.ZERO, RADIUS, 0.0, TAU, 24, Color(1.0, 0.95, 0.7), 1.5)
|
||||
1
scripts/ball.gd.uid
Normal file
1
scripts/ball.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bnvlevjo1fp48
|
||||
149
scripts/calibration_overlay.gd
Normal file
149
scripts/calibration_overlay.gd
Normal file
@ -0,0 +1,149 @@
|
||||
extends Control
|
||||
|
||||
signal calibration_finished(calibration: AccelCalibration)
|
||||
|
||||
enum Phase { WAITING, RIGHT, LEFT, DONE }
|
||||
|
||||
const TRAVEL_PX := 140.0
|
||||
const PHASE_DURATION := 4.5
|
||||
const CIRCLE_RADIUS := 44.0
|
||||
const NEEDLE_LEN := 58.0
|
||||
|
||||
var _phase := Phase.WAITING
|
||||
var _phase_time := 0.0
|
||||
var _guide_offset := Vector2.ZERO
|
||||
var _instruction := ""
|
||||
var _calibration := AccelCalibration.new()
|
||||
var _connection_notified := false
|
||||
|
||||
@onready var _hint_label: Label = $HintLabel
|
||||
@onready var _start_button: Button = $StartButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
_start_button.pressed.connect(_on_start_pressed)
|
||||
_show_waiting()
|
||||
|
||||
|
||||
func start() -> void:
|
||||
_calibration.reset()
|
||||
_connection_notified = false
|
||||
_phase = Phase.WAITING
|
||||
_phase_time = 0.0
|
||||
_guide_offset = Vector2.ZERO
|
||||
visible = true
|
||||
_start_button.visible = true
|
||||
_show_waiting()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func is_active() -> bool:
|
||||
return _phase != Phase.DONE
|
||||
|
||||
|
||||
func feed_accel(accel: Vector3i) -> void:
|
||||
match _phase:
|
||||
Phase.RIGHT:
|
||||
_calibration.record_right(accel)
|
||||
Phase.LEFT:
|
||||
_calibration.record_left(accel)
|
||||
|
||||
|
||||
func notify_connected() -> void:
|
||||
if _phase != Phase.WAITING or _connection_notified:
|
||||
return
|
||||
_hint_label.text = "Verbunden – tippe „Kalibrierung starten“ oder warte…"
|
||||
# Automatisch starten sobald verbunden
|
||||
begin_calibration()
|
||||
|
||||
|
||||
func begin_calibration() -> void:
|
||||
if _phase == Phase.RIGHT or _phase == Phase.LEFT:
|
||||
return
|
||||
_connection_notified = true
|
||||
_start_button.visible = false
|
||||
_begin_phase(Phase.RIGHT)
|
||||
|
||||
|
||||
func _on_start_pressed() -> void:
|
||||
begin_calibration()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _phase == Phase.WAITING or _phase == Phase.DONE:
|
||||
return
|
||||
|
||||
_phase_time += delta
|
||||
var t := clampf(_phase_time / PHASE_DURATION, 0.0, 1.0)
|
||||
var ease := t * t * (3.0 - 2.0 * t)
|
||||
|
||||
match _phase:
|
||||
Phase.RIGHT:
|
||||
_guide_offset.x = TRAVEL_PX * ease
|
||||
_instruction = "Neige das Gerät nach rechts – folge dem Kreis →"
|
||||
if _phase_time >= PHASE_DURATION:
|
||||
_begin_phase(Phase.LEFT)
|
||||
Phase.LEFT:
|
||||
_guide_offset.x = -TRAVEL_PX * ease
|
||||
_instruction = "Neige das Gerät nach links – folge dem Kreis ←"
|
||||
if _phase_time >= PHASE_DURATION:
|
||||
_finish()
|
||||
|
||||
_hint_label.text = _instruction
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _begin_phase(phase: Phase) -> void:
|
||||
_phase = phase
|
||||
_phase_time = 0.0
|
||||
_guide_offset = Vector2.ZERO
|
||||
match phase:
|
||||
Phase.RIGHT:
|
||||
_instruction = "Neige das Gerät nach rechts – folge dem Kreis →"
|
||||
Phase.LEFT:
|
||||
_instruction = "Neige das Gerät nach links – folge dem Kreis ←"
|
||||
_hint_label.text = _instruction
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
_phase = Phase.DONE
|
||||
visible = false
|
||||
if _calibration.analyze():
|
||||
calibration_finished.emit(_calibration)
|
||||
else:
|
||||
_hint_label.text = "Kalibrierung fehlgeschlagen – zu wenig Daten"
|
||||
visible = true
|
||||
_start_button.visible = true
|
||||
_show_waiting()
|
||||
|
||||
|
||||
func _show_waiting() -> void:
|
||||
_instruction = "Warte auf Verbindung zu localhost:9090…"
|
||||
_hint_label.text = _instruction + "\n(Oder „Kalibrierung starten“ ohne Verbindung)"
|
||||
|
||||
|
||||
func _screen_center() -> Vector2:
|
||||
return get_viewport_rect().size * 0.5
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if _phase == Phase.DONE:
|
||||
return
|
||||
|
||||
var pos := _screen_center() + _guide_offset
|
||||
|
||||
draw_circle(pos, CIRCLE_RADIUS, Color(0.25, 0.72, 0.95, 0.2))
|
||||
draw_arc(pos, CIRCLE_RADIUS, 0.0, TAU, 64, Color(0.35, 0.82, 1.0), 2.5)
|
||||
|
||||
var needle_tip := pos + Vector2(0.0, -NEEDLE_LEN)
|
||||
draw_line(pos, needle_tip, Color(1.0, 1.0, 1.0, 0.95), 3.0)
|
||||
draw_circle(needle_tip, 5.0, Color(1.0, 1.0, 1.0))
|
||||
|
||||
var center := _screen_center()
|
||||
if _phase == Phase.RIGHT:
|
||||
draw_line(center, center + Vector2(TRAVEL_PX, 0.0), Color(1.0, 1.0, 1.0, 0.12), 1.0)
|
||||
elif _phase == Phase.LEFT:
|
||||
draw_line(center, center + Vector2(-TRAVEL_PX, 0.0), Color(1.0, 1.0, 1.0, 0.12), 1.0)
|
||||
1
scripts/calibration_overlay.gd.uid
Normal file
1
scripts/calibration_overlay.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://crbm1833myr5l
|
||||
232
scripts/main.gd
Normal file
232
scripts/main.gd
Normal file
@ -0,0 +1,232 @@
|
||||
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
|
||||
|
||||
_accel = Vector3i(int(data["x"]), int(data["y"]), int(data["z"]))
|
||||
|
||||
|
||||
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())
|
||||
1
scripts/main.gd.uid
Normal file
1
scripts/main.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://g8bye3sv5ifc
|
||||
Loading…
x
Reference in New Issue
Block a user