Coding Quests
The Scroll Library
Tutorials

Godot 4.7 VirtualJoystick Tutorial: Mobile Controls Without a Plugin

June 16, 20265 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, and it's based on the addon a lot of us were already using, so it shows up fully grown.

This is the short version: add the node, pick a mode, read its output, feed that into your movement. 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 right or left thumb naturally sits, and size it for a real finger, not a mouse cursor.

That's the entire setup. No script on the joystick itself, no input map wiring required to get started.

Pick a mode

VirtualJoystick has a mode property with 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 appears wherever the player first touches (inside its area) 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.

Read the output

The joystick exposes its direction as a Vector2 through its output property, already normalized for you, and an is_pressed flag for whether the player is touching it. Reading it looks like this:

GDScript
extends CharacterBody2D
@export var speed: float = 220.0
@onready var joystick: VirtualJoystick = $"../UI/VirtualJoystick"
func _physics_process(_delta: float) -> void:
# output is a Vector2 in the range -1..1 on each axis
velocity = joystick.output * speed
move_and_slide()

That's a full mobile-controllable character. The joystick hands you a direction, you scale it by speed, you move. If the player isn't touching the stick, output is Vector2.ZERO and the character stops on its own.

For a 3D character, the idea is the same, you just map the 2D output onto the ground plane:

GDScript
func _physics_process(_delta: float) -> void:
var dir := joystick.output
velocity.x = dir.x * speed
velocity.z = dir.y * speed
move_and_slide()

Keep desktop input working too

Most games want keyboard and gamepad on desktop and the joystick on touch. Don't throw away your existing input. Use Input.get_vector() for the hardware path and add the joystick on top:

GDScript
func _physics_process(_delta: float) -> void:
var keyboard := Input.get_vector("move_left", "move_right", "move_up", "move_down")
# Prefer the touch stick when the player is actually using it
var dir := joystick.output if joystick.is_pressed else keyboard
velocity = dir * speed
move_and_slide()

Then hide the joystick on platforms that don't need it:

GDScript
func _ready() -> void:
joystick.visible = OS.has_feature("mobile")

A couple of gotchas

The classic one is the joystick eating 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.

The other one: test on a real phone early. A control that feels fine with a mouse in the editor can be cramped or mispositioned under an actual thumb. Build to your device and hold it the way a player will.

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. Wire the VirtualJoystick into it 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. The community addons still work, but you no longer need to install anything to get touch movement controls.

How do I read the direction from a VirtualJoystick?

Read its output property, which is a normalized Vector2. Multiply it by your speed and assign it to velocity, then call move_and_slide(). Check is_pressed if you need to know whether the player is currently touching the stick.

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. Read Input.get_vector() for hardware input and fall back to it when the joystick isn't pressed. That way one movement function handles desktop and mobile, and you can hide the on-screen stick on platforms that don't need it.

godotgodot-4-7mobiletutorial

Reading is the map. The quest is the territory.

Build this for real in the 2D Top-Down Movement Quest. It's free, no card needed.

Start the Quest
Written by Coding Quests

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