G2.5 — Input¶
What you'll learn
- Query input through the global
Inputsingleton:is_action_pressed,is_action_just_pressed,get_vector. - Define named actions in the InputMap and query those, not raw keys.
- Handle discrete events with
_input/_unhandled_inputand theInputEventtypes. - Choose polling (held, continuous) versus events (discrete, one-shot), and why
_unhandled_inputis preferred for gameplay.
How it applies
- Hardcoded keys cannot be rebound and break across devices. Checking for the physical
Wkey everywhere means no remapping, no gamepad, and trouble on keyboard layouts whereWis elsewhere. Named actions decouple intent ("move up") from the device, so rebinding and controllers work for free. get_vectoralready solves the diagonal-speed bug. Building a movement vector by hand risks the faster-on-diagonals defect (L1.6);Input.get_vectorreturns a length-capped, normalized vector, so movement is even in every direction without you normalizing.just_pressedis one frame; misusing it double-fires or misses. A "just pressed" check is true for a single frame; polling it in the wrong callback, or treating a held key as a press, fires an action twice or not at all. Matching the query to the intent (held vs edge) prevents both.- Handling gameplay input too early swallows the UI. Reading gameplay input in
_inputruns before the UI gets a chance, so clicks "pass through" menus._unhandled_inputlets UI consume first, fixing input that leaks behind open panels.
Concepts¶
The Input singleton and polling¶
Input is a global singleton (not a node) you can query from anywhere — typically each frame in
_physics_process:
func _physics_process(delta: float) -> void:
if Input.is_action_pressed("move_right"):
position.x += speed * delta
if Input.is_action_just_pressed("jump"):
jump()
is_action_pressed(action)— true for every frame the action is held. For continuous input (walking while a key is down).is_action_just_pressed(action)— true only on the frame the action went down. For one-shot input (a jump, a confirm).is_action_just_released(action)is its release counterpart.
Actions and the InputMap¶
The strings above ("move_right", "jump") are actions defined in Project Settings → Input
Map, each mapped to one or more physical inputs (keys, mouse buttons, gamepad buttons). Code queries
the action, never the raw key. This decoupling is why rebinding and gamepad support are possible at
all: the gameplay code says "jump," and the InputMap decides that "jump" is Space, or A on a gamepad,
or whatever the player remapped it to.
get_vector for movement¶
For 2D movement, Input.get_vector reads four directional actions and returns a ready-to-use vector:
var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * speed
The argument order is negative-x, positive-x, negative-y, positive-y. The returned vector is already
normalized (length capped at 1), so a diagonal does not exceed cardinal speed — get_vector builds
in the normalized() fix from L1.6. This is the idiomatic way to read movement input.
Events: _input and _unhandled_input¶
Polling suits continuous state; for discrete events Godot delivers InputEvent objects to callbacks:
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("attack"):
attack()
get_viewport().set_input_as_handled() # mark it consumed
_input(event)receives every input event, early, before the UI._unhandled_input(event)receives only events that nothing else consumed — crucially, after the UI has had its turn.
For gameplay, prefer _unhandled_input: it lets a Control/UI consume clicks and keys first, so a
click on an open menu does not also trigger a world action behind it. Use _input only when you must
see events before the UI (a global debug key, say). event.is_action_pressed("x") tests an event
against an action the same way the Input singleton does.
Example
Continuous movement by polling, discrete attack by event — each in its right place:
func _physics_process(delta: float) -> void:
var dir := Input.get_vector("left", "right", "up", "down")
position += dir * speed * delta # held movement: poll every physics frame
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("attack"): # discrete action: handle the event once
attack()
Movement is a state held over many frames, so it is polled; an attack is a moment, so it is an
event handled once. Polling the attack with is_action_just_pressed in _physics_process would also
work, but routing it through _unhandled_input lets the UI intercept it first.
Walkthrough¶
- In Project Settings → Input Map, add actions
move_left,move_right,move_up,move_down(map them to WASD or arrows) andattack(map it to a key or mouse button). - On a
Node2Dwith a visible sprite, poll movement: in_physics_process, buildInput.get_vector("move_left", "move_right", "move_up", "move_down"), multiply by a speed anddelta, and add toposition. Run and move; confirm diagonals are not faster (get_vector normalized it). - Handle the attack as an event: implement
_unhandled_input(event)and, onevent.is_action_pressed("attack"), print "attack". Confirm it fires once per press. - Contrast held vs edge: add
is_action_pressed("attack")polling in_physics_processprinting each frame the button is down, and compare with the single fire from the event handler.
Optional sanity check
Put a Button (under a CanvasLayer, G2.4) over your gameplay area. Handle the world attack in
_input and click the button — the world also attacks, because _input saw the event before the
UI. Move the handler to _unhandled_input and the button click no longer leaks to the world. That
is the UI-consumes-first reason for _unhandled_input.
Self-check quiz¶
Q1 — Why query an action like \"jump\" instead of checking for the physical Space key directly?
A. Querying keys is not possible in Godot. B. Actions decouple intent from device, so the same code supports rebinding, gamepads, and different keyboard layouts; a hardcoded key supports none of that. C. Actions are faster to query. D. Space is reserved by the engine.
Reveal answer
B. The InputMap maps an action to one or more physical inputs, so gameplay code expresses intent and the mapping (and the player's rebinds, and gamepad bindings) is handled separately. A is false (you can read raw keys; it is just inflexible). C is not the reason. D is false.
Q2 — You want a character to walk while a key is held. Which query, in which callback?
A. is_action_just_pressed in _ready.
B. is_action_pressed (or get_vector) polled each frame in _physics_process.
C. is_action_just_released in _input.
D. A signal connected to the key.
Reveal answer
B. Continuous, held movement is a state checked every physics frame; is_action_pressed
(or get_vector for a direction) in _physics_process is the idiom. A runs once and tests an
edge. C tests release. D is not how held movement is read. The held-vs-edge distinction is the
point.
Q3 — A click on an open menu also triggers a world action behind it. The world handler is in _input. Fix?
A. Move the handler to _unhandled_input, so the UI consumes the click first and only unconsumed events reach gameplay.
B. Disable the menu while playing.
C. Use is_action_pressed instead.
D. Nothing; this is unavoidable.
Reveal answer
A. _input sees events before the UI, so gameplay fires even when a menu should have eaten
the click; _unhandled_input runs only on events nothing consumed, letting the UI intercept
first. B breaks the feature to dodge the bug. C changes the query, not the ordering problem. D is
false — _unhandled_input is exactly the fix.
Integration question¶
Q4 — open
A character reads movement with if Input.is_key_pressed(KEY_W): position.y -= 5 (and similar for
each direction) in _process, and fires its attack from _input. Players report: the controls
cannot be rebound and do nothing on a gamepad; diagonal movement is faster than straight; movement
speed varies by monitor; and clicking a pause-menu button also swings the weapon. Map each complaint
to a concept from this chapter (and G2.1/L1.6) and give the corrected approach.
Reveal expected answer
No rebinding / no gamepad comes from reading raw keys (is_key_pressed(KEY_W)) instead of
actions; defining move_*/attack in the InputMap and querying those decouples intent from
device, enabling rebinds and controllers. Diagonals faster is the un-normalized-vector bug
(L1.6): summing per-key offsets yields a length-1.41 diagonal. Using
Input.get_vector("left","right","up","down") returns a normalized vector, so all directions
move at one speed. Speed varies by monitor is the frame-rate-dependence bug (G2.1): a fixed
-= 5 per _process frame moves more on faster displays. Move movement to _physics_process and
scale by delta (position += dir * speed * delta). The menu click also attacks is the input
ordering bug: handling attack in _input runs before the UI; move it to _unhandled_input (and
call set_input_as_handled() when consumed) so the menu Button eats the click first. Corrected,
movement is Input.get_vector(...) * speed * delta polled in _physics_process, and attack is an
action handled in _unhandled_input — each complaint resolved by matching the input mechanism to
the intent and respecting the engine's ordering.