From f246ede94f7eafc9715e6fcde0ed5e1fef24af8c Mon Sep 17 00:00:00 2001 From: enne2 Date: Sun, 11 Oct 2026 00:13:06 +0200 Subject: [PATCH] Fix mobile restart and swipe aiming while firing --- docs/TOUCH-CONTROLS.md | 20 +++++++++++++++++++- scripts/main.gd | 2 +- scripts/player.gd | 29 ++++++++++++++++++++++++++++- scripts/touch_controls.gd | 31 +++++++++++++++++++++++++++---- tools/test_touch_controls.gd | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 7 deletions(-) diff --git a/docs/TOUCH-CONTROLS.md b/docs/TOUCH-CONTROLS.md index 327e728..df8fedf 100644 --- a/docs/TOUCH-CONTROLS.md +++ b/docs/TOUCH-CONTROLS.md @@ -31,7 +31,25 @@ carries a single pointer and would break simultaneous aiming and firing. - left half: floating joystick (its base recenters where the finger lands), feeds the analog `move_*` actions; - right half: look area, drag rotates the view through `player.apply_look()`; -- bottom right: FIRE (hold to keep shooting), GRENADE, RELOAD, JUMP. +- bottom right: FIRE (hold and drag to shoot AND aim), GRENADE, RELOAD, JUMP. +- game over: controls release all actions and hide; a central RICOMINCIA button + handles native screen touches even with mouse emulation disabled. A 0.6-second + guard prevents the killing-frame gesture from accidentally restarting. + +### Swipe aiming + +Prefer direct swipe displacement to a rate-based right stick: releasing the thumb +stops rotation immediately. FIRE also acts as a look pad, so two thumbs can move, +aim and shoot simultaneously. This follows the combined action/touchpad pattern in +[Microsoft's designer guide](https://learn.microsoft.com/en-us/gaming/gdk/docs/features/common/game-streaming/building-touch-layouts/game-streaming-tak-designers-guide). + +Use `InputEventScreenDrag.screen_relative`, not stretch-scaled `relative`, as +[Godot explicitly recommends for touch aiming](https://docs.godotengine.org/en/stable/classes/class_inputeventscreendrag.html). +Normalize by the physical window width: one full-width swipe turns 180 degrees; +vertical gain is 75% for finer pitch control. No time delta or acceleration is +applied (these are displacements), no smoothing latency, desktop mouse unchanged. +Lift/reposition the finger without a camera jump. Constants `LOOK_TURN_RADIANS` +and `LOOK_VERTICAL_RATIO` tune the feel. Movement uses the existing InputMap actions, so keyboard, gamepad and touch share one path. Reload, grenade and jump are read by polling (`Input.is_action_just_pressed`) instead of by event handling: diff --git a/scripts/main.gd b/scripts/main.gd index 14aa5df..87a8688 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -146,7 +146,7 @@ func _process(_dt: float) -> void: if player.touch_mode: hud.text += "\ntrascina a sinistra: muovi ยท a destra: guarda" if player.hp <= 0: - hud.text += "\nSEI MORTO โ€” premi R per ricominciare" + hud.text += "\nSEI MORTO โ€” tocca RICOMINCIA" if player.touch_mode else "\nSEI MORTO โ€” premi R per ricominciare" func zombies_for_wave(number: int) -> int: return 4 + number * 2 diff --git a/scripts/player.gd b/scripts/player.gd index ca7aab4..f9191d0 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -68,6 +68,16 @@ func _ready() -> void: if DisplayServer.get_name() != "headless" and not OS.has_feature("web"): Input.mouse_mode = Input.MOUSE_MODE_CAPTURED +var restart_button: Button + +func _input(event: InputEvent) -> void: + # Touch nativo: funziona anche con emulate_mouse_from_touch disattivato. + if event is InputEventScreenTouch and event.pressed and is_instance_valid(restart_button): + if not restart_button.disabled and restart_button.get_global_rect().has_point(event.position): + restart_button.disabled = true + get_viewport().set_input_as_handled() + get_tree().call_deferred("reload_current_scene") + func _unhandled_input(event: InputEvent) -> void: if OS.has_feature("web") and not touch_mode and event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT and not death_started: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED @@ -211,10 +221,27 @@ func _die() -> void: tween.tween_property(camera, "rotation:x", 0.08, 0.9) tween.tween_property(death_overlay, "color:a", 1.0, 1.8).set_delay(0.7) var message := Label.new() - message.text = "SEI MORTO\nPremi R per ricominciare" + message.text = "SEI MORTO" if touch_mode else "SEI MORTO\nPremi R per ricominciare" message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER message.add_theme_font_size_override("font_size", 36) message.modulate.a = 0 layer.add_child(message) message.set_anchors_and_offsets_preset(Control.PRESET_CENTER) tween.tween_property(message, "modulate:a", 1.0, 0.5).set_delay(2.2) + if touch_mode: + var restart := Button.new() + restart_button = restart + restart.name = "RestartButton" + restart.text = "RICOMINCIA" + restart.add_theme_font_size_override("font_size", 28) + layer.add_child(restart) + restart.set_anchors_and_offsets_preset(Control.PRESET_CENTER) + restart.offset_left = -150 + restart.offset_right = 150 + restart.offset_top = 90 + restart.offset_bottom = 162 + restart.disabled = true + get_tree().create_timer(0.6).timeout.connect(func(): restart.disabled = false) + restart.pressed.connect(func(): + restart.disabled = true + get_tree().reload_current_scene()) diff --git a/scripts/touch_controls.gd b/scripts/touch_controls.gd index c1b56c5..0980c4f 100644 --- a/scripts/touch_controls.gd +++ b/scripts/touch_controls.gd @@ -15,7 +15,8 @@ const JOY_RADIUS := 92.0 const KNOB_RADIUS := 40.0 const BTN_RADIUS := 46.0 const BTN_FIRE_RADIUS := 62.0 -const LOOK_SENSITIVITY := 1.7 # moltiplicatore del trascinamento nell'area di puntamento +const LOOK_TURN_RADIANS := PI # swipe largo quanto lo schermo = 180 gradi +const LOOK_VERTICAL_RATIO := 0.75 const EDGE := 24.0 const COLOR_BASE := Color(1, 1, 1, 0.09) @@ -137,7 +138,7 @@ func _input(event: InputEvent) -> void: elif event is InputEventScreenDrag: index = event.index pos = event.position - relative = event.relative + relative = event.screen_relative is_drag = true elif mouse_fallback and event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: index = MOUSE_INDEX @@ -218,9 +219,31 @@ func _drag(index: int, pos: Vector2, relative: Vector2) -> void: _joy_knob = (pos - _joy_origin).limit_length(JOY_RADIUS) _apply_move() _root.queue_redraw() - Role.LOOK: + Role.LOOK, Role.FIRE: if player and player.has_method("apply_look"): - player.apply_look(relative * LOOK_SENSITIVITY) + player.apply_look(_look_delta(relative, _screen_width())) + +## Stessa frazione di swipe = stessa rotazione, senza stretch o delta temporale. +static func _look_delta(relative: Vector2, screen_width: float) -> Vector2: + return relative * Vector2(1.0, LOOK_VERTICAL_RATIO) * LOOK_TURN_RADIANS / maxf(screen_width, 1.0) / 0.0025 + +func _screen_width() -> float: + return _size.x if DisplayServer.get_name() == "headless" else float(DisplayServer.window_get_size().x) + +func release_all() -> void: + for index in _touches.keys(): + _release(index) + for action in MOVE_ACTIONS + ["shoot", "grenade", "reload", "jump"]: + Input.action_release(action) + +func _process(_dt: float) -> void: + if player and player.hp <= 0 and enabled: + release_all() + enabled = false + _root.hide() + +func _exit_tree() -> void: + release_all() ## Traduce il vettore del joystick nelle azioni di movimento (analogiche). func _apply_move() -> void: diff --git a/tools/test_touch_controls.gd b/tools/test_touch_controls.gd index 1de510c..63724e5 100644 --- a/tools/test_touch_controls.gd +++ b/tools/test_touch_controls.gd @@ -46,7 +46,9 @@ func _drag(index: int, pos: Vector2, relative: Vector2) -> void: ev.index = index ev.position = pos ev.relative = relative + ev.screen_relative = relative Input.parse_input_event(ev) + Input.flush_buffered_events() func _steps(count: int) -> void: for i in count: @@ -128,6 +130,12 @@ func run_test() -> void: _touch(0, fire_pos, true) await _steps(10) assert(player.ammo < 30) + var fire_yaw: float = player.rotation.y + _drag(0, fire_pos + Vector2(-80, 10), Vector2(-80, 10)) + await _frame() + assert(player.rotation.y > fire_yaw + 0.001) + assert(Input.is_action_pressed("shoot")) + print("PASS: trascinare FUOCO mira senza interrompere lo sparo") var after_fire: int = player.ammo _touch(0, fire_pos, false) await _steps(10) @@ -166,6 +174,30 @@ func run_test() -> void: # --- rilevamento: su questo contesto (desktop headless) la UI non e' automatica --- assert(not TouchScript.should_auto_enable()) print("PASS: TouchControls.should_auto_enable() = false su desktop -> la UI compare solo se serve") + assert(TouchScript._look_delta(Vector2(100, 20), 1000).is_equal_approx(TouchScript._look_delta(Vector2(200, 40), 2000))) + print("PASS: sensibilita' invariata per swipe proporzionali su schermi diversi") + _touch(0, start_pos, true) + player.take_damage(1000) + await _frame() + await _frame() + assert(not touch.enabled and touch._touches.is_empty()) + assert(not Input.is_action_pressed("shoot")) + assert(is_instance_valid(player.restart_button)) + await create_timer(0.7).timeout + assert(not player.restart_button.disabled) + current_scene = world + # Verifica anche il reload effettivo, non solo il pulsante. + var restart_event := InputEventScreenTouch.new() + restart_event.pressed = true + restart_event.position = player.restart_button.get_global_rect().get_center() + player._input(restart_event) + assert(player.restart_button.disabled) + await _frame() + await _frame() + assert(current_scene != world) + assert(current_scene.player.hp > 0) + world = current_scene + print("PASS: morte libera input, RICOMINCIA touch ricarica davvero la partita") print("PASS: controlli touch completi") world.queue_free() await _frame()