Add on-screen touch controls for mobile browsers

- scripts/touch_controls.gd: full-screen layer routing multi-touch by touch
  index: floating joystick on the left half (analog move_* actions), look area
  on the right half, FIRE/GRENADE/RELOAD/JUMP buttons. Shown automatically on
  web_android/web_ios or a touchscreen; overridable with --touch/--no-touch and
  ?touch=1/?touch=0. The root Control uses MOUSE_FILTER_IGNORE on purpose: a
  STOP control swallows the first click and the Web start gate never fires.
- scripts/player.gd: touch_mode disables mouse look and pointer lock; view
  rotation moved to apply_look() shared by mouse and touch; reload and grenade
  are polled now (is_action_just_pressed) like shoot/jump, because an
  InputEventAction synthesised inside another event's dispatch does not reach
  _unhandled_input; start_reload() extracted; get_vector uses an explicit 0.2
  dead zone so the joystick keeps its full travel.
- scripts/main.gd: creates the layer when needed, accepts a screen touch as the
  start gesture (which also unlocks browser audio) and enables the layer only
  afterwards.
- tools/test_touch_controls.gd: 11 assertions (joystick movement, look
  rotation, simultaneous multi-touch, fire hold/release, reload, grenade, jump,
  detection) runnable headless and on a real display.
- docs/TOUCH-CONTROLS.md: detection rules, input model, verification.
This commit is contained in:
enne2
2026-10-10 23:52:27 +02:00
parent 956a15a525
commit 6b2779223f
8 changed files with 619 additions and 26 deletions
+44 -22
View File
@@ -16,6 +16,9 @@ var weapon_anim: AnimationPlayer
var step_timer := 0.0
var death_started := false
var death_overlay: ColorRect
## True quando la partita e' guidata dai controlli on-screen (touch): il mouse
## emulato dai tocchi non deve muovere la visuale ne' sparare al posto del dito.
var touch_mode := false
const Blood = preload("res://scripts/blood_effects.gd")
const Audio = preload("res://scripts/hl_audio.gd")
@@ -66,30 +69,44 @@ func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _unhandled_input(event: InputEvent) -> void:
if OS.has_feature("web") 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
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED and hp > 0:
rotate_y(-event.relative.x * 0.0025)
camera.rotation.x = clampf(camera.rotation.x - event.relative.y * 0.0025, -1.4, 1.4)
if event is InputEventMouseMotion and not touch_mode and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
apply_look(event.relative)
if event.is_action_pressed("pause") and not death_started:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED else Input.MOUSE_MODE_CAPTURED
if event.is_action_pressed("grenade") and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED and not death_started:
## Ricarica: tempi invariati, riusata da tastiera (R) e dal pulsante on-screen.
func start_reload() -> void:
reloading = true
weapon_anim.play("reload")
Audio.play(self, "items/cliprelease1.wav")
get_tree().create_timer(0.8).timeout.connect(func():
if not death_started:
Audio.play(self, "items/clipinsert1.wav"))
get_tree().create_timer(1.57).timeout.connect(func():
if not death_started:
ammo = 30
reloading = false
weapon_anim.play("idle1"))
## Azioni a pressione singola lette a polling: stesso percorso per tastiera,
## joypad e pulsanti on-screen. Gli InputEventAction sintetici, generati dentro
## il dispatch di un altro evento (come fa un tocco), non arrivano affidabilmente
## a _unhandled_input; lo stato delle azioni invece si'.
func _handle_actions() -> void:
if Input.is_action_just_pressed("reload") and not reloading and ammo < 30:
start_reload()
if Input.is_action_just_pressed("grenade") and not death_started and (touch_mode or Input.mouse_mode == Input.MOUSE_MODE_CAPTURED):
throw_grenade()
if event.is_action_pressed("reload"):
if hp <= 0:
get_tree().reload_current_scene()
elif not reloading and ammo < 30:
reloading = true
weapon_anim.play("reload")
Audio.play(self, "items/cliprelease1.wav")
get_tree().create_timer(0.8).timeout.connect(func():
if not death_started:
Audio.play(self, "items/clipinsert1.wav"))
get_tree().create_timer(1.57).timeout.connect(func():
if not death_started:
ammo = 30
reloading = false
weapon_anim.play("idle1"))
## Rotazione della visuale. Unico punto in cui vivono sensibilita' e limiti di
## inclinazione, condiviso da mouse e controlli touch.
func apply_look(relative: Vector2) -> void:
if hp <= 0 or death_started:
return
rotate_y(-relative.x * 0.0025)
camera.rotation.x = clampf(camera.rotation.x - relative.y * 0.0025, -1.4, 1.4)
func _physics_process(dt: float) -> void:
cooldown -= dt
@@ -97,8 +114,13 @@ func _physics_process(dt: float) -> void:
flash.light_energy = maxf(0, flash.light_energy - dt * 60)
gun.position.z = lerpf(gun.position.z, 0, dt * 15)
if hp <= 0:
if Input.is_action_just_pressed("reload"):
get_tree().reload_current_scene() # R o pulsante Ricarica a partita finita
return
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
_handle_actions()
# Deadzone esplicita piccola (0.2): con la 0.5 delle azioni la corsa utile del
# joystick touch si dimezzerebbe, mentre per tastiera/joypad nulla cambia.
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_back", 0.2)
var direction := transform.basis * Vector3(input.x, 0, input.y)
velocity.x = direction.x * 5
velocity.z = direction.z * 5
@@ -112,7 +134,7 @@ func _physics_process(dt: float) -> void:
Audio.play(self, "player/pl_metal%d.wav" % randi_range(1, 4), false, -18)
if Input.is_action_just_pressed("shoot") and ammo <= 0 and not reloading:
Audio.play(self, "weapons/dryfire1.wav")
if Input.is_action_pressed("shoot") and cooldown <= 0 and ammo > 0 and not reloading and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
if Input.is_action_pressed("shoot") and cooldown <= 0 and ammo > 0 and not reloading and (touch_mode or Input.mouse_mode == Input.MOUSE_MODE_CAPTURED):
shoot()
func shoot() -> void: