Files
zombie-fps/docs/TOUCH-CONTROLS.md
T
enne2 6b2779223f 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.
2026-10-10 23:52:27 +02:00

3.5 KiB

Touch controls (mobile browser)

On-screen controls for phones and tablets, shown automatically when needed.

When they appear

scripts/touch_controls.gd decides, in this order:

  1. OS.has_feature("web_android") or OS.has_feature("web_ios") — browser on a phone/tablet. This is the documented way: in a Web export OS.has_feature("mobile") is false, and the web_* tags are the only ones that describe the browser's host OS (feature tags).
  2. DisplayServer.is_touchscreen_available() — native mobile, or a desktop/laptop touchscreen. Careful: display_server_web.cpp returns true here also when emulate_touch_from_mouse is on, so it is never used alone.
  3. Overrides: --touch / --no-touch on the command line, and ?touch=1 / ?touch=0 on the Web URL. With --touch on a desktop the mouse drags are treated as touches, so the UI can be tried without a touch device.

When touch mode is on, main.gd sets player.touch_mode, no pointer lock is requested and Input.set_emulate_mouse_from_touch(false) is applied: the emulated mouse would otherwise move the view twice and fire the weapon on any touch.

Layout and input model

One full-screen Control routes multi-touch by touch index (InputEventScreenTouch / InputEventScreenDrag), which is what the browser sends (display_server_web.cpp feeds exactly these events into parse_input_event); emulated mouse events are not used, because mouse emulation 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.

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: an InputEventAction synthesised inside the dispatch of another event (which is what a touch looks like) does not reliably reach _unhandled_input. Note that such a press is seen by _physics_process one physics frame later.

Input.get_vector(..., 0.2) uses an explicit small dead zone: the 0.5 dead zone configured for the actions would waste half of the joystick travel (keyboard input is digital, so nothing changes for it).

Verification

godot --headless --path . --script res://tools/test_touch_controls.gd -- --touch
godot --headless --path . --script res://tools/test_touch_controls.gd -- --no-touch
DISPLAY=:0 godot --display-driver x11 --rendering-driver opengl3 --path . \
  --script res://tools/test_touch_controls.gd -- --touch

The first and third run the same assertions (joystick movement, look rotation, simultaneous multi-touch, fire hold/release, reload, grenade, jump); the third goes through the real event pipeline. In --headless the DisplayServer window is 64x36, so the canvas_items stretch transform rescales injected coordinates (a (200,500) touch arrives as (4000,9720)); the test detects this and drives the handlers directly in viewport space instead.

Web build notes

  • The first tap also unlocks audio in the browser, so main.gd accepts either a mouse press or a screen touch as the start gesture and enables the touch layer only after it.
  • The HTML shell already ships user-scalable=no; no extra page-level CSS is required.