Skip to main content
Coding Quests
The Scroll Library
Tutorials

Godot 4.7 VirtualJoystick Tutorial: Mobile Controls Without a Plugin

June 16, 2026Updated October 22, 20266 min read

For years, adding a thumbstick to a touchscreen game in Godot meant installing a community addon or building the whole thing yourself out of a TouchScreenButton and some trig. Godot 4.7 ends that. There's a VirtualJoystick node in the box now.

If you've used the popular community addon, the settings will look familiar, but you read the stick differently. The built-in node has no output property. It presses your input actions instead, the same way a gamepad stick does, so the movement code you already have keeps working.

This is the short version: add the node, pick a mode, point it at your move actions, and read them with Input.get_vector(). Let's do it.

Add the node

On-screen controls live in a UI layer so they don't move with the camera. Add a CanvasLayer to your scene, then add a VirtualJoystick as a child of it. Drag it to the corner of the screen where a thumb naturally sits.

VirtualJoystick is a Control, so its rectangle is the area that reacts to touch. Make that rectangle big enough for a real thumb, not a mouse cursor. Two properties set how it's drawn: joystick_size is the ring and tip_size is the knob, both in pixels.

No script goes on the joystick itself.

Pick a mode

The joystick_mode property has three options, and the right one depends on the feel you want:

  • Fixed: the stick never moves. The player taps inside the ring and drags. Best when you want a predictable, always-in-the-same-spot control, like a twin-stick shooter.
  • Dynamic: the stick jumps to wherever the player first touches inside its rectangle, and snaps home on release. Good for action games where you don't want the player hunting for a fixed pad.
  • Following: like dynamic, but if the thumb drags past the edge, the stick follows it. This feels the most forgiving for fast movement, since the player never "runs out" of stick.

For a first game, start with fixed. It's the easiest to reason about. Switch to dynamic or following later if the controls feel stiff.

Two more settings are worth a look. visibility_mode can hide the stick until a thumb lands on it (When Touched). deadzone_ratio is how far the knob has to move before it counts, as a fraction of the stick's size. It starts at 0, so even a tiny nudge moves the player.

Point it at your move actions

This is the part that works differently from the addon. The joystick doesn't hand you a vector to read. When the player drags the knob, it presses input actions with a strength from 0 to 1, just like an analog stick on a gamepad.

Four properties pick the actions: action_left, action_right, action_up and action_down. Out of the box they're ui_left, ui_right, ui_up and ui_down. Change them to the actions your player already uses. In this post those are move_left, move_right, move_up and move_down, set up in Project > Project Settings > Input Map.

The names have to exist in the Input Map. If one doesn't, Godot prints an error the moment the stick moves.

Read the direction

Now the movement code is the same code you'd write for a keyboard:

GDScript
extends CharacterBody2D
​
@export var speed: float = 220.0
​
func _physics_process(_delta: float) -> void:
var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * speed
move_and_slide()

Input.get_vector() combines the four actions into one Vector2, from -1 to 1 on each axis. Its length never goes past 1, so moving diagonally isn't faster. When nobody is touching the stick, it returns Vector2.ZERO and the character stops on its own.

Because the stick and the keyboard press the same actions, this one function covers both. Bind your keys and a gamepad stick to the same four actions in the Input Map, and you get keyboard, controller and touch movement with no extra code.

For a 3D character, the idea is the same. You map the 2D direction onto the ground plane:

GDScript
func _physics_process(_delta: float) -> void:
var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity.x = direction.x * speed
velocity.z = direction.y * speed
move_and_slide()

Know when the stick is held

Sometimes you need to know whether the player is touching the stick, for example to fade out a "drag to move" hint. There's no is_pressed property for that. Use the pressed and released signals instead:

GDScript
@onready var joystick: VirtualJoystick = $"../UI/VirtualJoystick"
var stick_held := false
​
func _ready() -> void:
joystick.pressed.connect(func(): stick_held = true)
joystick.released.connect(func(_direction: Vector2): stick_held = false)

The node has three more signals:

  • tapped fires when the player taps the stick without moving the knob.
  • flicked fires when the player lets go while the knob is pushed out. It hands you the last direction, which suits a dash.
  • flick_canceled fires when the knob slides back into the deadzone after being pushed out.

To hide the stick on devices without a touchscreen, add this line to _ready():

GDScript
joystick.visible = DisplayServer.is_touchscreen_available()

That covers phones and tablets, including web builds played in a phone's browser.

A few gotchas

It ignores the mouse. The joystick only listens for touch. Click and drag it in a desktop test run and nothing happens. To test without a phone, turn on Project Settings > Input Devices > Pointing > Emulate Touch From Mouse (switch on Advanced Settings if you don't see it). While that's on, is_touchscreen_available() returns true too, so the stick stays visible on desktop.

It doesn't send input events. The joystick presses actions directly, so code that checks input every frame sees it: Input.get_vector(), Input.is_action_pressed(). Code in _input() or _unhandled_input() waits for events, and the joystick never sends one. Keep stick-driven code in _physics_process().

Other UI can swallow touches. The joystick can eat touches meant for the rest of your UI, or the reverse. Keep the joystick in its own CanvasLayer and make sure other touch controls aren't sitting invisibly on top of it. If taps feel like they're getting swallowed, that overlap is almost always why.

Test on a real phone early. A control that feels fine in a desktop test run can be cramped or mispositioned under an actual thumb. Build to your device and hold it the way a player will.

Coming from the addon?

The popular community addon declares its script as class_name VirtualJoystick. In 4.7 that name belongs to the built-in node, so the addon's script stops loading with the error Class "VirtualJoystick" hides a native class. Remove the addon folder, then swap the built-in node into your scenes.

The modes and the four action settings have built-in equivalents. Here's what changes in your code:

  • joystick.output becomes Input.get_vector() with the joystick's four actions.
  • joystick.is_pressed becomes the pressed and released signals.
  • deadzone_size and clampzone_size were in pixels. The built-in deadzone_ratio and clampzone_ratio are ratios of the stick's size.

Where to go next

Movement is the foundation everything else sits on. Once a character moves cleanly with both keyboard and touch, you've got the base for combat, collection, the works. Our 2D Top-Down Movement quest builds that foundation properly, and the first quest is free. Point a VirtualJoystick at the same move actions and you've got a game that plays on a phone.

If you want the wider tour of what shipped in this version, I covered all of it in Godot 4.7: Everything New.

FAQ

Do I still need a virtual joystick addon in Godot 4.7?

No. VirtualJoystick is a built-in node now. In fact, the popular community addon clashes with it: its script uses the same class name, so it fails to load in 4.7. Remove the addon and use the built-in node.

How do I read the direction from a VirtualJoystick?

Set its action_left, action_right, action_up and action_down properties to your move actions. Then call Input.get_vector() with those same four actions in _physics_process(). The built-in node has no output property. Use its pressed and released signals if you need to know whether the player is touching it.

What's the difference between fixed, dynamic, and following mode?

Fixed keeps the stick in one spot. Dynamic spawns it wherever the player first touches and returns it on release. Following does the same but lets the stick chase the thumb past its edge for a more forgiving feel. Start with fixed, then try the others.

Can I use the same joystick code for keyboard and gamepad?

Yes, and you don't need any extra code for it. The joystick presses the same input actions your keys and gamepad stick are bound to, so one Input.get_vector() call reads all three.

Why doesn't my VirtualJoystick respond to the mouse?

It only listens for touch input. To test it with a mouse, turn on Emulate Touch From Mouse in Project Settings, under Input Devices > Pointing.

godotgodot-4-7mobiletutorial

Stop reading. Start building.

Write real Godot code in your browser, then build this for real in the 2D Top-Down Movement Quest.

Free, and no card needed. Built by a real person, with new quests every month.

Get the next Godot build in your inbox

New quests, project breakdowns, and game-dev tips.

You'll get emails from CodingQuests with Godot tips, new tutorials, course updates, and occasional offers. Unsubscribe anytime. Privacy Policy

Written by Omar

We teach Godot 4 by making you build complete systems: inventories, save systems, action roguelike controllers, enemy AI. The scrolls are free. The quests are where it sticks.