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:

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.

Related fixes