This episode covers input thoroughly: the input map and action-based input, keyboard, mouse, touch, and gamepad support, the difference between handling UI input versus game input, and mobile input patterns and virtual controls for touch screens.

In episode 10, you brought sound to life. Now it's the player's hands' turn — Input & Controls. This is one of the biggest differentiators between a game that feels "connected" to the player and one that feels stiff. In episode 11, you'll learn to build an input system that isn't just functional, but also portable: the same set of actions works with keyboard, mouse, touch, and gamepad.
Episode 11's roadmap: we start with the input map and the action-based input concept, explore the various input devices from keyboard to gamepad, distinguish UI input handling from game input, and finish with mobile input patterns and virtual controls for touch screens.
The most common beginner mistake is reading keys directly in code — for example if event is InputEventKey and event.keycode == KEY_SPACE. This is fragile: if you want to change a key or add gamepad support, all the code has to be reworked. The solution is action-based input.
Godot provides an Input Map in Project Settings. There you define actions (abstract names like move_left or jump) and bind them to one or more physical inputs. The left arrow key and the A key can both trigger the move_left action. Your code doesn't care which key was pressed — it just asks "is this action active?".
extends Node
func _ready() -> void:
if not InputMap.has_action("move_left"):
InputMap.add_action("move_left")
var event := InputEventKey.new()
event.keycode = KEY_LEFT
InputMap.action_add_event("move_left", event)Actions are usually created in the editor via the Input Map, not from code — the example above only shows that the Input Map can also be configured programmatically, for example for a control remap feature from inside the game. Actions are the shared language between input devices and gameplay logic.
Warning
Never put raw keycodes in gameplay logic. Always ask about actions — Input.is_action_pressed("jump") — because that's the only way multi-device support works without changing code. Keep action configuration in the editor's Input Map so it can be tweaked without touching scripts.
An action in the Input Map can be bound to any kind of input at once: keyboard keys, mouse buttons, InputEventScreenTouch for touch screens, and InputEventJoypadButton for gamepads. All of them trigger the same action — this is the portability we're after.
But not all input is an action. Some need position or vector data:
Input.get_mouse_position(){gdscript} gives the cursor position on screen; Input.is_action_pressed("aim") for held buttons.Input.get_vector("move_left", "move_right", "move_forward", "move_back"){gdscript} returns an analog vector — an important difference, since a stick isn't like an on/off button.InputEventScreenTouch for touches, InputEventScreenDrag for drags.Input.get_vector(){gdscript} is the golden function for movement: it reads two pairs of actions and returns a normalized vector. One call, working for WASD, arrow keys, and gamepads alike:
extends CharacterBody2D
@export var speed := 200.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()Notice that there isn't a single keycode in the code above. You can add a gamepad, touch screen, or new keyboard keys to the Input Map and the character responds immediately — without touching the script at all.
In episode 8, you made UI with Button responding to clicks. In episode 6, you made a player responding to keys. Both use input, but with different rules. How does Godot decide who gets the event? The answer is the event propagation order:
_input — runs for all nodes, first._gui_input — specific to Control nodes that "capture" the event (button clicks)._unhandled_input — only called if no UI consumed it.This is the key to distinguishing UI and game input. UI should capture clicks over buttons; the game should receive the remaining input. If game logic is attached to _input, it can react twice or steal events from the UI. The correct pattern:
extends CharacterBody2D
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("jump") and is_on_floor():
velocity.y = -400.0With _unhandled_input{gdscript}, events already consumed by UI buttons won't reach the player — clicking "Start" won't make the character jump. For input the game must handle even when UI is open (like the pause button), use _input carefully, or give explicit priority. Rule of thumb: default to _unhandled_input for gameplay.
There are two styles of reading input in Godot, each with its own use:
Input.is_action_pressed("move_left") every frame. Good for continuous movement like walking — the result is stable and predictable.InputEvent once through a callback. Good for single-moment actions like pressing pause or choosing from a menu.extends Node
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept"):
print("accept pressed")
elif event.is_action_released("ui_accept"):
print("accept released")The difference between pressed and released also matters: an action triggered while held vs when released. For controls like a charge attack, you need to hear released. For movement, polling makes more sense. Combine the two deliberately: polling for continuous state, events for discrete moments.
Phones don't have keyboards or gamepads — the touch screen is the only medium. Common mobile input patterns:
Godot translates all of these as InputEventScreenTouch and InputEventScreenDrag. The basic swipe detection pattern:
extends Node
var touch_start := Vector2.ZERO
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventScreenTouch and event.pressed:
touch_start = event.position
elif event is InputEventScreenDrag:
var delta := event.position - touch_start
if delta.length() > 40.0:
print("swipe direction: %s" % delta.normalized())
touch_start = event.positionThe first touch stores the starting position, then drags are measured against it. When the distance exceeds a threshold, it counts as a swipe and its direction is normalized. This threshold is important — without it, accidental finger movements would be counted as swipes.
Mobile players can't press physical buttons. The solution is virtual controls — buttons and sticks drawn on the screen. Godot provides a special node: TouchScreenButton. What's special: TouchScreenButton emits pressed/released signals, and can directly send events as actions.
extends TouchScreenButton
func _ready() -> void:
texture_normal = preload("res://assets/ui/btn_jump.png")
action = "jump"With the action property filled in, the virtual button automatically emulates the jump action when touched — and because it's the action that gameplay code reads (the action-based pattern from the start of this episode), the character jumps without needing to know the input came from a touch screen. This is the beauty of action-based input: a virtual control and a gamepad trigger the same action.
Info
Place virtual controls in a CanvasLayer with TouchScreenButton, and disable them when the device has a keyboard or gamepad (check with DisplayServer.has_feature) so desktop players don't see annoying on-screen buttons. One set of actions, all devices served.
For a virtual joystick, TouchScreenButton alone isn't enough — you need finger position tracking. A common pattern: detect touches on the left area of the screen, then vector from the initial touch point to the current finger position, normalize by a maximum radius, and use that vector as the movement value. That way analog movement controls are available even without a physical device.
Episode 11 completed the input journey: the input map with action-based input as the foundation, keyboard, mouse, touch, and gamepad support in one set of actions, the distinction between UI and game input handling through event propagation order, polling and event-driven styles, mobile input patterns like tap and swipe, and virtual controls for touch screens.
The key takeaways:
Input.get_vector and Input.is_action_pressed are the keys to portability.InputEvent carries device info (keyboard, mouse, touch, gamepad) in one API._unhandled_input so it doesn't steal events from UI.InputEventScreenTouch and InputEventScreenDrag.TouchScreenButton with the action property makes virtual controls blend into the action system.With this, you've mastered the entire foundation of interaction: world, UI, state, audio, and input. In episode 12, we enter the polish phase: Animations & Visual Polish — dissecting AnimationPlayer, AnimationTree and blend trees, procedural animation, particles, and shaders and screen transitions to make your game feel premium. See you in the next episode!