Quick answer: Godot converts touches into mouse events by default (Emulate Mouse From Touch), so mouse-based code and Control nodes work on phones. If you turned that off, Controls still work but your mouse code does not. If you handle InputEventScreenTouch directly, enable Emulate Touch From Mouse to test it on desktop.
Input that works with a mouse can silently fail with a finger, and code written for touch can look broken in the editor. Most of it comes down to two project settings and which input events your scripts listen for.
The two emulation settings
Under Project Settings > Input Devices > Pointing:
- Emulate Mouse From Touch (on by default): touches also generate mouse button and motion events. Your
InputEventMouseButtonhandlers and all Control nodes work on phones. - Emulate Touch From Mouse (off by default): mouse clicks also generate touch events. Turn this on to test touch-only code on desktop.
If touch "works on the phone but not in the editor", you are handling touch events and need the second setting. If it "works in the editor but not on the phone", check whether the first was turned off.
Handle the right events
For multi-touch, handle touch events and use index to tell fingers apart:
func _unhandled_input(event):
if event is InputEventScreenTouch:
if event.pressed:
touches[event.index] = event.position
else:
touches.erase(event.index)
elif event is InputEventScreenDrag:
touches[event.index] = event.position
Mouse emulation only follows the first finger, so pinch-zoom and two-thumb controls need touch events.
TouchScreenButton setup
TouchScreenButton is a Node2D, not a Control. It needs a Texture Normal or a Shape to define where it can be pressed, and an Action to trigger Input Map actions. Its Visibility Mode "TouchScreen Only" hides it on desktop, which can look like it is missing.
Something is covering the screen
Touch is delivered to Controls first. A full-screen Control with mouse_filter = Stop (an invisible panel, a HUD root) eats every tap before _unhandled_input() sees it. Set non-interactive Controls to Ignore.
Coordinates are off
With stretch modes, event.position is in viewport coordinates, which is what you want for UI. To compare with world positions under a moving Camera2D, convert: get_canvas_transform().affine_inverse() * event.position. Taps that register "in the wrong place" are usually missing this conversion.
Testing on a device
Use Remote Debug (one-click deploy) so print() output and errors from the phone appear in the editor's Output panel. Logging the events you receive (print(event)) usually shows within a minute which of the above is happening.