Tutorials & Beginner-First Jul 18, 2026

Your First Godot 4.7 VirtualJoystick - Mobile Controls One Evening 2026

Godot 4.7 ships VirtualJoystick built in - CanvasLayer setup, Fixed, Dynamic, and Following modes, a desktop fallback, and a virtual_joystick_ok receipt.

By GamineAI Team

Your First Godot 4.7 VirtualJoystick - Mobile Controls One Evening 2026

Cute pixel-art chick character thumbnail - a small, friendly build metaphor for a first Godot 4.7 VirtualJoystick evening

For years, "how do I add a touch joystick in Godot" meant one answer: go find a community addon, drop it in addons/, hope the maintainer kept it updated across engine versions, and hand-wire it to your movement code. Godot 4.7 changes the default answer. The engine now ships a built-in VirtualJoystick control node - no plugin, no vendored GitHub repository, no addon-version mismatch the week before you export to Android or iOS (Godot 4.7 release notes).

This evening tutorial is not a novelty lap around a new node. Novelty fades the moment 4.8 ships something else shiny. What stays useful long after "4.7 added VirtualJoystick" stops being news is the discipline underneath it: a CanvasLayer that keeps your controls in screen space, a joystick that drives the same named Input actions your keyboard and gamepad already use, a visibility_mode decision that does not embarrass you on a Steam Deck, and a receipt that proves the whole rig works on a real device instead of only in the editor's remote debug window. That discipline is the durable part. The built-in node is just the delivery mechanism 2026 happens to hand you.

This is the beginner-first tutorial GamineAI's own Godot 4.7 upgrade lock playbook flagged as a sibling piece to watch for - that article covers the keep/hold/rewrite call on adopting VirtualJoystick near a ship date; this one covers the actual step-by-step build. It pairs with the node-vocabulary groundwork in the Godot RigidBody2D and Area2D guide and the movement-script habits from Your First Godot 4 Jolt Physics 2D Platformer. If you are brand-new to Godot's project structure, start with the Godot 4 project setup course lesson before opening this evening's scene.

Honest scope (read once): The built-in VirtualJoystick node does not expose a live output Vector2 or an is_pressed boolean the way some older community addons did. It works differently, and more usefully once you see the pattern: it drives Input actions directly, the same way a keyboard key or a gamepad axis does. You read it with Input.get_vector() using the action names you assigned, not by polling a custom property every frame. That single fact is the difference between a tutorial that copies old addon habits onto a node that does not have them, and one that teaches the pattern Godot actually shipped.

Why this matters now (July 2026)

Godot 4.7 landed on 2026-06-18 with the tagline "Lights, Camera, Action!," and the built-in VirtualJoystick was one of the release's callouts precisely because touch input has been a recurring gap for years - every mobile-facing Godot project either wrote a custom joystick rig from scratch or pulled in a third-party addon and hoped it tracked engine updates (release notes). The class reference for the new node is live in the official documentation today, with a full property, signal, and enum list (VirtualJoystick class reference).

What actually changed for micro-studios this month:

  1. "Which joystick addon do you use" stopped being the first mobile-controls question. Teams starting a new touch-facing prototype in July 2026 default to the built-in node first and only reach for a third-party option if they need a feature the built-in node genuinely lacks.
  2. Search and Discord traffic for "godot virtual joystick" is shifting from addon comparisons to built-in usage questions. The dominant search intent right now is "how do I use the new built-in node," and most of the results that rank are either the raw class docs or short single-mode walkthroughs - not a full evening covering modes, desktop fallback, and device visibility together.
  3. Old addon habits do not transfer cleanly. Developers who spent a year reading an addon's output Vector2 property every _physics_process frame try the same pattern on the built-in node and find nothing there. That confusion is common enough this month to be worth a dedicated Common Mistakes entry below.
  4. Receipt culture is spreading past uploads and physics. The same "prove it, do not just say it worked" habit GamineAI has documented for BUILD_RECEIPT and Jolt physics setup now applies to input rigs: a mobile control scheme that "feels right in the editor" but was never tested on a real touchscreen is not shipped, it is hoped.

Direct answer: Tonight, place a VirtualJoystick under a CanvasLayer, wire its action_up/action_down/action_left/action_right properties to the same Input Map actions your keyboard and gamepad already use, read movement every frame with Input.get_vector(), pick a visibility_mode that matches your actual ship platform, clear gates V1-V6, file godot_virtual_joystick_receipt_v1.json, and latch virtual_joystick_ok.

Who this evening is for

Audience What you get
Creators / beginners A CanvasLayer plus VirtualJoystick scene that moves a character in under an hour, no addon install
Developers The real Input-action-driven API, a desktop and gamepad fallback pattern, gates V1-V6, and a receipt schema
Companies / leads Diligence-ready device matrix and accessibility notes for a control scheme, not a Discord screenshot
Search Primary keyword: godot virtual joystick

Time budget: about 2.5-3.5 hours for a working, dual-input vertical slice (joystick plus keyboard/gamepad fallback, one receipt). Prerequisites: Godot 4.7 or newer installed; basic GDScript; a CharacterBody2D (or CharacterBody3D) you can already move with the keyboard, or willingness to build a minimal one tonight.

Who should skip this evening

  • Projects still pinned to Godot 4.6 or earlier that cannot upgrade this sprint - the built-in node does not exist there; either upgrade first (see the 4.7 upgrade lock playbook) or keep your existing addon.
  • Teams two weeks from a mobile ship date with a working, already-tuned custom joystick rig. Swapping input schemes that close to Gold is exactly the "new dependency near ship" risk the upgrade-lock playbook warns against - hold the migration for the next milestone.
  • Anyone who only needs menu navigation tonight, not in-world movement (build the core movement loop first, add the joystick once there is something to steer).

Evening schedule (three hours)

Hour Outcome
0.5 Confirm Godot 4.7+; read the honest-scope note; sketch the Input Map actions you will share across devices
1 CanvasLayer + VirtualJoystick scene; joystick visible and draggable in a running build
2 Character reads Input.get_vector(); desktop keyboard and gamepad still work through the same actions
3 Visibility mode decision; gates V1-V6; receipt JSON; virtual_joystick_ok latch

Non-repetition (what this URL owns)

Existing URL Owns This URL owns
Godot 4.7 upgrade lock Adopt-vs-hold call on VirtualJoystick near a ship date, plus HDR, AreaLight3D, and the Asset Store Step-by-step build, real property/signal API, desktop fallback, receipt
Jolt Physics 2D platformer evening CharacterBody3D movement script and jump physics under Jolt Input source (touch, keyboard, gamepad) feeding that same kind of movement script
RigidBody2D / Area2D guide Classic 2D node roles and collision vocabulary UI-layer node (CanvasLayer + Control) that sits above the 2D/3D world, not inside it

Forward cluster rows: Help row for VirtualJoystick install questions, Guide chapter forward on Godot 4.7 input, Resource roundup forward for touch-control checklists.


Prerequisites checklist (15 minutes)

Print or paste into your evening note:

  • [ ] Godot 4.7 or newer (Help -> About). VirtualJoystick does not exist as a built-in node before 4.7.
  • [ ] A project with at least one movable CharacterBody2D or CharacterBody3D, or a fresh scene you are willing to build one in tonight.
  • [ ] Folder release-evidence/virtual-joystick-evening/ at repo root.
  • [ ] A phone, tablet, or a device with a touchscreen for at least one real on-device test - the editor's mouse-drag simulation is not the same as a finger.
  • [ ] Official doc tab open: VirtualJoystick class reference

Beginner mindset: you are not building a full mobile control scheme with a jump button, a pause menu, and haptics tonight. You are proving one joystick drives one character reliably, on one real touch device, with a named fallback for keyboard and gamepad.


Step 1 - CanvasLayer and VirtualJoystick setup (V1 start)

Open your project and confirm the version first. Project -> Project Settings will simply not offer a VirtualJoystick node in the Create Node dialog on anything earlier than 4.7 - that failure is your version check.

Create a new scene, or open your existing gameplay scene, and build this branch:

Node2D or Node3D (your existing world root)
├── ... your level nodes ...
├── Player (CharacterBody2D or CharacterBody3D)
└── HUD (CanvasLayer)
    └── VirtualJoystick (VirtualJoystick)

Why the joystick lives under a CanvasLayer and not directly under your world root: Control nodes (and VirtualJoystick inherits from Control) are positioned in screen space by default when they sit under a CanvasLayer, so the joystick stays anchored to the bottom-left corner of the screen regardless of camera movement, zoom, or whether your game is 2D or 3D. Drop a VirtualJoystick directly under a Node2D or Node3D world root instead, and it inherits that node's world transform - the joystick can drift, scale, or rotate with the camera instead of staying pinned to the player's thumb. This is the single most common "why is my joystick in the wrong place" support question, and it is a one-line fix: move it under a CanvasLayer.

Select the VirtualJoystick node and set a starting size in the Inspector:

Property Starter value Why
joystick_size 150.0 Comfortable thumb target on a phone; too small invites mis-taps
tip_size 70.0 Roughly half the joystick size reads clearly against most themes
Anchor preset Bottom Left Standard mobile convention; adjust if your HUD needs the right side instead

Use the Layout -> Anchor menu in the 2D viewport toolbar to pin the VirtualJoystick's anchor to the bottom-left (or bottom-right, if that fits your HUD better) so it stays in a sane position across different screen resolutions and aspect ratios.

V1: Godot version confirmed 4.7+; VirtualJoystick created under a CanvasLayer; anchor preset set for a mobile aspect ratio.

Evidence: Screenshot canvaslayer_joystick_scene.png into release-evidence/virtual-joystick-evening/.


Step 2 - Wire actions and read movement (V2-V4)

This is the step that trips up anyone arriving from an older joystick addon, so read it slowly once.

The built-in VirtualJoystick does not hand you a live output Vector2 or an is_pressed boolean to poll each frame. Instead, it has four StringName properties - action_up, action_down, action_left, and action_right - that default to &"ui_up", &"ui_down", &"ui_left", and &"ui_right". When the joystick tip moves in a direction, it triggers those named Input actions, exactly the way a keyboard key press or a gamepad stick tilt triggers an action you have mapped in the Input Map. That means you read the joystick with the same call you already use for keyboard and gamepad input: Input.get_vector().

Open Project -> Project Settings -> Input Map and confirm (or create) four actions shared across every input device you plan to support - for example move_left, move_right, move_up, and move_down - each with a keyboard binding (WASD or arrow keys) and, if you support controllers, a gamepad axis binding.

Select the VirtualJoystick node and point its action properties at those same shared actions:

VirtualJoystick property Set to
action_left move_left
action_right move_right
action_up move_up
action_down move_down

Now attach a movement script to Player. For a top-down or twin-stick style CharacterBody2D:

extends CharacterBody2D

@export var speed: float = 220.0

func _physics_process(delta: float) -> void:
    var input_vector := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = input_vector * speed
    move_and_slide()

That is the entire integration. Input.get_vector() does not care whether the signal came from a finger dragging the VirtualJoystick, a keyboard's WASD keys, or a gamepad's left stick - all three now drive the same four named actions, so this one function call reads all of them. This is the pattern the honest-scope note promised: the durable lesson is not "how do I use this specific node," it is "input devices should converge on shared actions, and the built-in joystick is designed to plug into that convention instead of inventing its own."

If your game is a side-view or 3D-feeling project on CharacterBody3D, the same actions drive X/Z movement instead:

extends CharacterBody3D

@export var speed: float = 6.0

var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y -= gravity * delta

    var input_vector := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity.x = input_vector.x * speed
    velocity.z = input_vector.y * speed

    move_and_slide()

If you are also handling jump physics or floor-snap tuning on a CharacterBody3D, pair this section with the movement-script habits in Your First Godot 4 Jolt Physics 2D Platformer - the joystick is only the input source; the floor and jump logic there still applies unchanged.

Want to react to press, release, or a quick flick instead of continuous movement? The node also emits five signals you can connect for discrete events instead of (or alongside) Input.get_vector():

func _ready() -> void:
    var joystick := $HUD/VirtualJoystick
    joystick.pressed.connect(_on_joystick_pressed)
    joystick.released.connect(_on_joystick_released)
    joystick.flicked.connect(_on_joystick_flicked)

func _on_joystick_pressed() -> void:
    print("joystick touched")

func _on_joystick_released(input_vector: Vector2) -> void:
    print("released at strength ", input_vector.length())

func _on_joystick_flicked(input_vector: Vector2) -> void:
    print("flicked - use this for a dash or dodge input")

flicked fires when the tip moved past the deadzone and the joystick was released - a natural hook for a dash, dodge, or roll input layered on top of the continuous movement vector, without writing your own gesture detection.

V2: VirtualJoystick visible and draggable in a running build (desktop mouse-drag counts for this gate; device touch confirmation comes at V5). V3: action_up/action_down/action_left/action_right point at Input Map actions that also have keyboard (and, if applicable, gamepad) bindings - not left at the untouched ui_* defaults unless that is genuinely your shared scheme. V4: Player script reads movement with Input.get_vector() using those shared action names - not a nonexistent output property.


Joystick modes explained - Fixed, Dynamic, and Following

VirtualJoystick exposes a joystick_mode property (enum JoystickMode) with three values. Set it in the Inspector or from code with joystick.joystick_mode = VirtualJoystick.JOYSTICK_DYNAMIC (or whichever mode you pick).

Mode Behavior When to use it
Fixed (JOYSTICK_FIXED, value 0) The joystick base never moves from its anchored position; only the tip drags within it Predictable HUD layout; best when players glance down expecting the stick in the same spot every time (racing games, simple platformers)
Dynamic (JOYSTICK_DYNAMIC, value 1) The joystick base recenters to wherever the player first touches, then snaps back to its original anchored position on release Twin-stick shooters and action games where players do not want to hunt for an exact touch target under stress
Following (JOYSTICK_FOLLOWING, value 2) Like Dynamic, but if the finger drags outside the joystick's clamp radius, the base follows the touch instead of clamping the tip Fast-paced games where players drag their thumb a long way during a session and should never feel the stick "run out of room"

Beginner default: start with Fixed for your first evening. It is the easiest to reason about, the easiest to explain in a design doc, and the easiest to debug - the joystick base is always exactly where you drew it in the editor. Move to Dynamic or Following only once Fixed feels genuinely limiting for your specific game, not because a release-notes blog post made the fancier mode sound better.

Two properties tune the feel further, regardless of mode:

  • deadzone_ratio (float, default 0.0) - the fraction of the joystick's radius the tip must cross before it counts as active input. A deadzone near 0.15 to 0.2 filters out accidental thumb tremor without feeling numb. Note that Input Map actions can carry their own deadzone too; if both are set, the joystick's deadzone applies first, then the action's.
  • clampzone_ratio (float, default 1.0) - how far from center the tip is allowed to travel before being clamped to the joystick's edge. In Following mode, this same radius also decides when the base itself starts chasing the touch.

Step 3 - Desktop and gamepad fallback with Input.get_vector (V5)

Because the built-in VirtualJoystick triggers ordinary Input actions instead of inventing a separate touch-only API, the "desktop fallback" most tutorials treat as a separate afternoon of work is, in this case, already done the moment you wired action_left/action_right/action_up/action_down to shared actions in Step 2. There is no second code path to write. That is worth saying plainly, because it is the most durable, non-obvious lesson in this whole evening: the fallback is not a feature you add on top - it is a natural consequence of using shared action names from the start.

What you do need to verify explicitly is that nothing accidentally hardcoded a touch-only assumption elsewhere in your project. Walk this checklist:

  1. Disable or ignore the touch device (unplug a connected touchscreen, or simply do not touch the joystick) and confirm keyboard WASD or arrow keys still move the character through the exact same Player script - no if OS.has_feature("mobile") branch silently gating movement to touch-only.
  2. Plug in a gamepad and confirm the left stick moves the character too, through the same move_left/move_right/move_up/move_down actions, with no separate gamepad-specific script path.
  3. Run all three simultaneously (keyboard, gamepad, and the on-screen joystick, if your test device allows it) and confirm the last input to move away from zero wins, without a stale value from one device fighting a fresh value from another. Input.get_vector() reads the current combined action state each frame, so this generally just works - but confirm it on your project rather than assuming.
  4. Test with the touchscreen simulator off. Godot's "Emulate Touchscreen" project setting (input_devices/pointing/emulate_touch_from_mouse) can make a joystick look correct with mouse clicks while hiding real multi-touch or press-and-drag issues that only appear on an actual device - see the Common Mistakes section below for the failure this causes.

V5: Desktop keyboard and (if supported) gamepad both move the character through the same shared actions, verified with the touchscreen device disconnected or ignored - not merely "the code looks like it should work."


Visibility modes for Steam Deck, phone, and desktop

VirtualJoystick has a visibility_mode property (enum VisibilityMode) with two values, and this is the property that keeps you from shipping an on-screen thumbstick to players who are holding a mouse and keyboard.

Mode Behavior Best platform fit
Always (VISIBILITY_ALWAYS, value 0) The joystick is always drawn, whether or not it is being touched Phones and tablets, where the joystick is the primary and often only movement input
When Touched (VISIBILITY_WHEN_TOUCHED, value 1) The joystick only appears while actively being pressed, otherwise it is invisible Devices with a touchscreen that also support keyboard/gamepad, where you want the option available without a permanent HUD element cluttering the screen

Match the mode to the device honestly, not optimistically:

  • Phone or tablet-only export: VISIBILITY_ALWAYS. Players have no other input device; hiding the joystick just makes them hunt for it.
  • Steam Deck and other handhelds with physical sticks and buttons: do not show a touch joystick at all by default, even though the Deck has a touchscreen - players expect the physical sticks to work, and a permanently visible on-screen joystick over the physical controls reads as a mistake, not a feature. If you support Deck's touchscreen as a genuine accessibility option, gate it behind a settings toggle rather than showing it unconditionally.
  • Desktop with an optional touchscreen monitor: VISIBILITY_WHEN_TOUCHED, or hide the VirtualJoystick node entirely (visible = false) when DisplayServer.is_touchscreen_available() returns false at startup.
  • Cross-platform export from one project: detect the platform at runtime and set visibility_mode (or the node's visible property) accordingly in _ready(), rather than shipping one fixed HUD layout to every export target.

A minimal runtime check:

extends CanvasLayer

@onready var joystick: VirtualJoystick = $VirtualJoystick

func _ready() -> void:
    var is_touch_device := DisplayServer.is_touchscreen_available()
    joystick.visible = is_touch_device
    if is_touch_device:
        joystick.visibility_mode = VirtualJoystick.VISIBILITY_ALWAYS

V6 preview: your receipt should record which visibility_mode (or manual visible toggle) you chose per target platform, and why - not just that a joystick exists somewhere in the scene tree.


Common mistakes (and fast fixes)

Mistake 1 - Expecting an output Vector2 or is_pressed property

Symptom: Code from an older addon-based tutorial references joystick.output or joystick.is_pressed and throws a "property not found" error, or silently does nothing if you guarded it with has_method/get(). Fix: The built-in node does not have those properties. Read movement with Input.get_vector() using the action names you assigned, and use the pressed, released, flicked, tapped, and flick_canceled signals for discrete events. This is a different design, not a missing feature.

Mistake 2 - VirtualJoystick placed directly under the world root instead of a CanvasLayer

Symptom: The joystick drifts, scales, or rotates with the camera; it looks fine at the default zoom and breaks the moment the player zooms or the camera pans. Fix: Reparent it under a CanvasLayer. Screen-space UI belongs in screen space, not world space.

Mistake 3 - Only testing with "Emulate Touchscreen" and never a real device

Symptom: The joystick behaves perfectly with mouse clicks in the editor, then feels laggy, mis-registers taps, or fights multi-touch on an actual phone. Fix: Test on at least one real touchscreen device before calling the evening done. Mouse emulation is a convenience for iteration, not a substitute for device testing - this is the same "it worked on my laptop" trap other GamineAI evenings warn about for physics and builds.

Mistake 4 - Deadzone left at zero on a device with a shaky or imprecise touch panel

Symptom: The character twitches or drifts slightly even when the player believes their thumb is centered. Fix: Raise deadzone_ratio to roughly 0.15-0.2 and re-test. Do not overcorrect to a huge deadzone that makes small, deliberate movements feel unresponsive.

Mistake 5 - Leftover addon files still wired into the scene

Symptom: Two joystick implementations both listening for input; movement doubles up, or one silently overrides the other's action triggers. Fix: If you are migrating off a third-party addon, remove its scene instances and script references from your gameplay scenes entirely before wiring the built-in node - do not leave both installed "just in case."

Mistake 6 - Shipping the joystick visible on every platform, including desktop and Steam Deck's physical controls

Symptom: Desktop players and Deck players see an on-screen thumbstick permanently covering part of the play area, even though they are using a mouse, keyboard, or physical stick. Fix: Set visibility_mode and a platform check as shown above. A control scheme that ignores the device it is running on is not "mobile-ready," it is "mobile-only wearing a cross-platform label."


Step 4 - File the evening receipt (gates V1-V6)

Create release-evidence/virtual-joystick-evening/godot_virtual_joystick_receipt_v1.json:

{
  "schema": "godot_virtual_joystick_receipt_v1",
  "date_iso": "2026-07-18",
  "godot_version": "4.7.x",
  "scene_path": "res://scenes/gameplay_lab.tscn",
  "joystick_node_path": "HUD/VirtualJoystick",
  "canvas_layer_parent": true,
  "joystick_mode": "JOYSTICK_FIXED",
  "shared_actions": ["move_left", "move_right", "move_up", "move_down"],
  "deadzone_ratio": 0.15,
  "clampzone_ratio": 1.0,
  "visibility_mode_by_platform": {
    "phone": "VISIBILITY_ALWAYS",
    "tablet": "VISIBILITY_ALWAYS",
    "steam_deck": "hidden_physical_sticks_used",
    "desktop": "VISIBILITY_WHEN_TOUCHED"
  },
  "real_device_tested": true,
  "device_matrix": ["Android phone", "iPad", "desktop with mouse+keyboard"],
  "desktop_fallback_ok": true,
  "gamepad_fallback_ok": true,
  "gates": {
    "V1": true,
    "V2": true,
    "V3": true,
    "V4": true,
    "V5": true,
    "V6": true
  },
  "virtual_joystick_ok": true,
  "notes": "Built-in VirtualJoystick under CanvasLayer; shared Input actions drive keyboard, gamepad, and touch identically."
}

Wire a one-line pointer into your studio's BUILD_RECEIPT or evening pass note:

virtual_joystick: release-evidence/virtual-joystick-evening/godot_virtual_joystick_receipt_v1.json
virtual_joystick_ok: true

V6: Evidence folder contains at least one screenshot plus this receipt; virtual_joystick_ok latches only when V1-V5 are all true and the receipt's real_device_tested field is honest, not aspirational.


Gate catalog V1-V6 (printable facilitator card)

Gate Check RED if...
V1 Godot 4.7+ confirmed; VirtualJoystick created under a CanvasLayer Version unconfirmed, or joystick parented under a world-space node
V2 Joystick visible and draggable in a running build Only visible in the editor's 2D viewport, never actually run
V3 action_up/down/left/right point at shared Input Map actions with keyboard (and gamepad, if supported) bindings Actions left at untouched ui_* defaults with no other device bound, or a one-off custom action nothing else uses
V4 Movement script reads Input.get_vector() with those shared action names Script references a nonexistent output or is_pressed property
V5 Desktop keyboard and gamepad fallback verified with touch disconnected or ignored Only ever tested with the touch device connected
V6 Receipt filed with an honest real_device_tested value and per-platform visibility_mode; virtual_joystick_ok latched Missing receipt, or a true latch without a real device test

Latch rule: virtual_joystick_ok is true only when V1-V6 are all true. Partial progress stays false with a blocked_gate field filled in.


Beginner path vs developer path (same evening)

Beginner path (first 90 minutes):

  1. Confirm Godot 4.7+; create CanvasLayer and VirtualJoystick.
  2. Set joystick_size, tip_size, and anchor preset.
  3. Wire action_left/right/up/down to shared Input Map actions.
  4. Read movement in Player with Input.get_vector(). Celebrate the first drag-to-move.

Developer path (remaining hours):

  1. Pick a joystick_mode deliberately, not by default.
  2. Verify desktop and gamepad fallback with touch disconnected.
  3. Set visibility_mode per target platform; add a runtime DisplayServer.is_touchscreen_available() check if exporting to more than one platform.
  4. Connect pressed/released/flicked signals if you need a dash or dodge trigger layered on top of continuous movement.
  5. File the receipt; decide whether an existing third-party joystick addon gets removed this evening or held for the next milestone.

Both audiences share one URL and one scene - creators get a working thumbstick fast; developers get the fallback proof and the latch.


Enterprise and company diligence notes

When a publisher, porting partner, or QA lead asks "how does mobile input actually work in this build," answer with the receipt's fields, not a description of how it felt in a demo.

Diligence question Field
Which Godot version added this input path godot_version
Does the control scheme degrade gracefully on desktop desktop_fallback_ok
Was this tested on a real touch device or only the editor real_device_tested
Which platforms show the joystick, and which hide it visibility_mode_by_platform
Accessibility - can the deadzone be tuned for players with limited fine motor control deadzone_ratio, plus whether a settings-menu slider exposes it to players
Ownership Who signed virtual_joystick_ok in the evening note

Accessibility note for evaluators: deadzone_ratio and clampzone_ratio are not just feel-tuning knobs - they are the mechanism for supporting players with tremor or limited precision. A shipped game that exposes at least a coarse deadzone adjustment in its settings menu, rather than a single hardcoded value, is in materially better shape for accessibility review than one that is not.

Device matrix expectation: a diligence-ready device matrix names at least one phone, one tablet or larger-screen touch device if relevant, and confirms desktop keyboard/gamepad parity - not a single Android phone standing in for "mobile support" broadly.

Cost/ROI: the built-in node removes a recurring addon-maintenance line item from the budget entirely - no more re-vetting a third-party joystick repository against every engine upgrade. Governance: only a lead latches virtual_joystick_ok after V1-V6, the same discipline already applied to jolt_platformer_ok and BUILD_RECEIPT latches elsewhere on this site. Security/privacy: N/A beyond not committing device-identifying screenshots or account data into evidence folders.


Scenarios A-E (decision table)

ID Situation Action
A New mobile-only prototype, no existing input code Build directly on the built-in node; visibility_mode Always; Fixed mode first
B Existing project already has a working third-party joystick addon, shipping soon Hold the migration per the 4.7 upgrade lock guidance; schedule the swap for the next milestone, not this ship branch
C Cross-platform export - desktop, phone, and Steam Deck from one project Runtime DisplayServer.is_touchscreen_available() check; hide the joystick on Deck's physical-stick path by default
D Fast-paced twin-stick game where players drag far during a session Following mode with a tuned clampzone_ratio; test that the base genuinely follows rather than clamping awkwardly
E Publisher or QA asks for an accessibility pass on touch controls Expose deadzone_ratio in a settings menu; document the device matrix and fallback in the receipt before the call

How this pairs with the rest of the site


After tonight - what not to do Monday

  1. Do not leave a permanently visible on-screen joystick covering a Steam Deck's or desktop player's screen because "it already works on phone."
  2. Do not merge a joystick migration onto a ship branch two weeks from Gold without following the hold guidance in the upgrade-lock playbook.
  3. Do not claim "mobile support" in store copy before at least one real touchscreen device test, not just the editor's mouse emulation.
  4. Do not leave old addon files installed alongside the built-in node "just in case" - pick one, remove the other, and say so in the receipt.

Key takeaways

  • Godot 4.7 ships VirtualJoystick as a built-in Control node - no addon required, with Fixed, Dynamic, and Following modes via the joystick_mode property.
  • Place it under a CanvasLayer, not the world root, so it stays anchored in screen space regardless of camera movement.
  • The node does not expose an output Vector2 or is_pressed property. It drives named Input actions (action_up/down/left/right) directly - read movement with Input.get_vector() using the same shared action names your keyboard and gamepad already use.
  • Because the fallback uses shared actions from the start, desktop and gamepad support is largely already done once the joystick is wired correctly - verify it, do not rebuild it separately.
  • Choose visibility_mode deliberately per platform: Always for phone/tablet, When Touched or hidden for desktop, and generally hidden in favor of physical sticks on Steam Deck.
  • deadzone_ratio and clampzone_ratio tune feel and double as an accessibility lever - expose at least a coarse setting to players when you can.
  • Clear gates V1-V6, file godot_virtual_joystick_receipt_v1.json, and latch virtual_joystick_ok only after a real on-device test, not just an editor mouse-drag.
  • Companies want a device matrix and accessibility note; creators want a working thumbstick in under two hours; search wants godot virtual joystick.

FAQ

What is the built-in VirtualJoystick node in Godot 4.7?

It is a Control-derived node, added in Godot 4.7, that renders an on-screen joystick for touchscreen input and can trigger Input Map actions when dragged - removing the need for a third-party touch-control addon for most mobile projects (release notes).

How do I read input from Godot's VirtualJoystick node?

Assign its action_up, action_down, action_left, and action_right properties to Input Map actions that also have keyboard or gamepad bindings, then read movement every frame with Input.get_vector() using those same action names. The node does not expose a property you poll directly for continuous movement.

Does VirtualJoystick have an output Vector2 or is_pressed property?

No. Those properties exist on some older third-party joystick addons, but the built-in Godot 4.7 node uses a different design - it triggers named Input actions instead, which you read with Input.get_vector() for continuous movement or via the pressed, released, flicked, tapped, and flick_canceled signals for discrete events (VirtualJoystick class reference).

What is the difference between Fixed, Dynamic, and Following joystick modes?

Fixed keeps the joystick base stationary; only the tip drags within it. Dynamic recenters the base to the player's first touch point, then snaps back on release. Following also recenters to the first touch, but the base itself chases the finger if it drags beyond the clamp radius. Start with Fixed for your first evening.

Do I still need a third-party joystick addon in Godot 4.7?

For most new mobile-facing projects, no - the built-in node covers the common cases with less maintenance risk. Keep an existing addon only if you rely on a feature it has that the built-in node genuinely lacks, or if you are close enough to a ship date that swapping input schemes is not worth the risk this milestone.

How do I support both touch and keyboard/gamepad input at the same time?

Point the VirtualJoystick's action properties at the same Input Map actions your keyboard and gamepad bindings already use, then read all of them with a single Input.get_vector() call in your movement script. There is no separate code path to maintain for each device.

Should the joystick be visible on desktop and Steam Deck?

Generally no by default. Use visibility_mode set to When Touched (or hide the node entirely) on desktop, and prefer physical sticks on Steam Deck unless you are deliberately offering touch as an accessibility option behind a settings toggle.

What does virtual_joystick_ok mean?

It is the latch confirming gates V1 through V6 all passed - version confirmed, CanvasLayer placement correct, shared actions wired, movement read via Input.get_vector(), desktop/gamepad fallback verified, and a receipt filed after an honest real-device test.

How does this help a Steam Deck or mobile ship date?

It gives a dated, device-tested artifact so a QA lead, publisher, or new hire can confirm the control scheme actually works across the target device matrix - instead of relying on "it felt fine in the editor" the week before a build goes out.


Conclusion

Godot 4.7 turns "how do I add a touch joystick" from a plugin-shopping question into a built-in default, and that is genuinely good news for a Saturday evening's worth of mobile controls. But the built-in node rewards a small amount of care: park it under a CanvasLayer, wire it to the same shared Input actions your keyboard and gamepad already use, choose a visibility_mode that matches the device in the player's hands, and test on a real screen before you call it done. None of that discipline depends on 4.7 being new - it is the same habit that will still be correct when 4.8, 4.9, and whatever comes after ship their own new nodes. Clear V1-V6, file godot_virtual_joystick_receipt_v1.json, and let virtual_joystick_ok be a fact instead of a feeling.

Related reads