Files
zombie-fps/docs/TOUCH-CONTROLS.md

4.7 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 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.

Use InputEventScreenDrag.screen_relative, not stretch-scaled relative, as Godot explicitly recommends for touch aiming. 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: 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.