84 lines
4.7 KiB
Markdown
84 lines
4.7 KiB
Markdown
# 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](https://docs.godotengine.org/en/stable/tutorials/export/feature_tags.html)).
|
|
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](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,
|
|
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
|
|
|
|
```bash
|
|
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.
|