Fix mobile restart and swipe aiming while firing
This commit is contained in:
+19
-1
@@ -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
|
- left half: floating joystick (its base recenters where the finger lands), feeds the analog
|
||||||
`move_*` actions;
|
`move_*` actions;
|
||||||
- right half: look area, drag rotates the view through `player.apply_look()`;
|
- 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,
|
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:
|
grenade and jump are read by polling (`Input.is_action_just_pressed`) instead of by event handling:
|
||||||
|
|||||||
+1
-1
@@ -146,7 +146,7 @@ func _process(_dt: float) -> void:
|
|||||||
if player.touch_mode:
|
if player.touch_mode:
|
||||||
hud.text += "\ntrascina a sinistra: muovi · a destra: guarda"
|
hud.text += "\ntrascina a sinistra: muovi · a destra: guarda"
|
||||||
if player.hp <= 0:
|
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:
|
func zombies_for_wave(number: int) -> int:
|
||||||
return 4 + number * 2
|
return 4 + number * 2
|
||||||
|
|||||||
+28
-1
@@ -68,6 +68,16 @@ func _ready() -> void:
|
|||||||
if DisplayServer.get_name() != "headless" and not OS.has_feature("web"):
|
if DisplayServer.get_name() != "headless" and not OS.has_feature("web"):
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
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:
|
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:
|
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
|
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(camera, "rotation:x", 0.08, 0.9)
|
||||||
tween.tween_property(death_overlay, "color:a", 1.0, 1.8).set_delay(0.7)
|
tween.tween_property(death_overlay, "color:a", 1.0, 1.8).set_delay(0.7)
|
||||||
var message := Label.new()
|
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.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
message.add_theme_font_size_override("font_size", 36)
|
message.add_theme_font_size_override("font_size", 36)
|
||||||
message.modulate.a = 0
|
message.modulate.a = 0
|
||||||
layer.add_child(message)
|
layer.add_child(message)
|
||||||
message.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
message.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||||
tween.tween_property(message, "modulate:a", 1.0, 0.5).set_delay(2.2)
|
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())
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const JOY_RADIUS := 92.0
|
|||||||
const KNOB_RADIUS := 40.0
|
const KNOB_RADIUS := 40.0
|
||||||
const BTN_RADIUS := 46.0
|
const BTN_RADIUS := 46.0
|
||||||
const BTN_FIRE_RADIUS := 62.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 EDGE := 24.0
|
||||||
|
|
||||||
const COLOR_BASE := Color(1, 1, 1, 0.09)
|
const COLOR_BASE := Color(1, 1, 1, 0.09)
|
||||||
@@ -137,7 +138,7 @@ func _input(event: InputEvent) -> void:
|
|||||||
elif event is InputEventScreenDrag:
|
elif event is InputEventScreenDrag:
|
||||||
index = event.index
|
index = event.index
|
||||||
pos = event.position
|
pos = event.position
|
||||||
relative = event.relative
|
relative = event.screen_relative
|
||||||
is_drag = true
|
is_drag = true
|
||||||
elif mouse_fallback and event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
elif mouse_fallback and event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
index = MOUSE_INDEX
|
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)
|
_joy_knob = (pos - _joy_origin).limit_length(JOY_RADIUS)
|
||||||
_apply_move()
|
_apply_move()
|
||||||
_root.queue_redraw()
|
_root.queue_redraw()
|
||||||
Role.LOOK:
|
Role.LOOK, Role.FIRE:
|
||||||
if player and player.has_method("apply_look"):
|
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).
|
## Traduce il vettore del joystick nelle azioni di movimento (analogiche).
|
||||||
func _apply_move() -> void:
|
func _apply_move() -> void:
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ func _drag(index: int, pos: Vector2, relative: Vector2) -> void:
|
|||||||
ev.index = index
|
ev.index = index
|
||||||
ev.position = pos
|
ev.position = pos
|
||||||
ev.relative = relative
|
ev.relative = relative
|
||||||
|
ev.screen_relative = relative
|
||||||
Input.parse_input_event(ev)
|
Input.parse_input_event(ev)
|
||||||
|
Input.flush_buffered_events()
|
||||||
|
|
||||||
func _steps(count: int) -> void:
|
func _steps(count: int) -> void:
|
||||||
for i in count:
|
for i in count:
|
||||||
@@ -128,6 +130,12 @@ func run_test() -> void:
|
|||||||
_touch(0, fire_pos, true)
|
_touch(0, fire_pos, true)
|
||||||
await _steps(10)
|
await _steps(10)
|
||||||
assert(player.ammo < 30)
|
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
|
var after_fire: int = player.ammo
|
||||||
_touch(0, fire_pos, false)
|
_touch(0, fire_pos, false)
|
||||||
await _steps(10)
|
await _steps(10)
|
||||||
@@ -166,6 +174,30 @@ func run_test() -> void:
|
|||||||
# --- rilevamento: su questo contesto (desktop headless) la UI non e' automatica ---
|
# --- rilevamento: su questo contesto (desktop headless) la UI non e' automatica ---
|
||||||
assert(not TouchScript.should_auto_enable())
|
assert(not TouchScript.should_auto_enable())
|
||||||
print("PASS: TouchControls.should_auto_enable() = false su desktop -> la UI compare solo se serve")
|
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")
|
print("PASS: controlli touch completi")
|
||||||
world.queue_free()
|
world.queue_free()
|
||||||
await _frame()
|
await _frame()
|
||||||
|
|||||||
Reference in New Issue
Block a user