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
+40 -4
View File
@@ -4,10 +4,12 @@ const ZombieScript = preload("res://scripts/zombie.gd")
const RoomScript = preload("res://scripts/room.gd")
const MUSIC_PATH := "res://assets/audio/music/zombie_industrial_loop.ogg"
const MUSIC_VOLUME_DB := -11.0
const TouchControls = preload("res://scripts/touch_controls.gd")
var player: CharacterBody3D
var room: Node3D
var music: AudioStreamPlayer
var music_started := false
var touch: Node
var hud: Label
var score := 0
var wave := 0
@@ -54,10 +56,14 @@ func _ready() -> void:
canvas.add_child(cross)
cross.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
_setup_music()
_setup_touch()
if OS.has_feature("web"):
awaiting_web_start = true
if touch != null:
touch.enabled = false # il primo tocco/click avvia la partita
start_label = Label.new()
start_label.text = "CLICCA PER INIZIARE\nWASD: movimento · clic sinistro: spara · destro: granata · R: ricarica"
var howto := "trascina a sinistra: muovi · destra: guarda · pulsanti: fuoco/granata/ricarica/salto" if player.touch_mode else "WASD: movimento · clic sinistro: spara · destro: granata · R: ricarica"
start_label.text = ("TOCCA PER INIZIARE" if player.touch_mode else "CLICCA PER INIZIARE") + "\n" + howto
start_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
start_label.add_theme_font_size_override("font_size", 24)
start_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
@@ -72,14 +78,42 @@ func _ready() -> void:
get_tree().quit())
func _unhandled_input(event: InputEvent) -> void:
if awaiting_web_start and event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
if not awaiting_web_start:
return
var started := false
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
started = true
elif event is InputEventScreenTouch and event.pressed:
started = true # primo tocco su mobile
if started:
awaiting_web_start = false
start_label.queue_free()
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# Il browser concede l'audio solo dopo un gesto dell'utente: qui il click.
if not player.touch_mode:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
if touch != null:
touch.enabled = true
# Il browser concede l'audio solo dopo un gesto dell'utente: qui il click/tocco.
_start_music()
_next_wave()
## Controlli on-screen: attivi su browser mobile o touchscreen, forzabili con
## --touch / --no-touch sulla riga di comando e con ?touch=1 / ?touch=0 sul Web.
func _setup_touch() -> void:
var forced := TouchControls.force_mode()
if not (forced > 0 or (forced == 0 and TouchControls.should_auto_enable())):
return
player.touch_mode = true
# Su un dispositivo touch il pointer lock non serve e su desktop forzato
# ruberebbe il cursore: nessuna cattura del mouse in modalita' touch.
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
touch = CanvasLayer.new()
touch.set_script(TouchControls)
touch.player = player
# Su desktop forzato con --touch i drag del mouse valgono come tocchi,
# cosi' la UI e' provabile anche senza dispositivo touch.
touch.mouse_fallback = forced > 0 and not DisplayServer.is_touchscreen_available()
add_child(touch)
## Colonna sonora: industrial metal strumentale su loop senza giunzione udibile.
func _setup_music() -> void:
music = AudioStreamPlayer.new()
@@ -109,6 +143,8 @@ func _process(_dt: float) -> void:
hud.text += "\nGRANATA: " + ("PRONTA" if player.grenade_cooldown <= 0 else "%d s" % ceili(player.grenade_cooldown))
if player.reloading:
hud.text += "\nRICARICA..."
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"