Skip to content

G2.5 — Input

What you'll learn

  • Query input through the global Input singleton: 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_input and the InputEvent types.
  • Choose polling (held, continuous) versus events (discrete, one-shot), and why _unhandled_input is preferred for gameplay.

How it applies

  • Hardcoded keys cannot be rebound and break across devices. Checking for the physical W key everywhere means no remapping, no gamepad, and trouble on keyboard layouts where W is elsewhere. Named actions decouple intent ("move up") from the device, so rebinding and controllers work for free.
  • get_vector already solves the diagonal-speed bug. Building a movement vector by hand risks the faster-on-diagonals defect (L1.6); Input.get_vector returns a length-capped, normalized vector, so movement is even in every direction without you normalizing.
  • just_pressed is 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 _input runs before the UI gets a chance, so clicks "pass through" menus. _unhandled_input lets 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

  1. In Project Settings → Input Map, add actions move_left, move_right, move_up, move_down (map them to WASD or arrows) and attack (map it to a key or mouse button).
  2. On a Node2D with a visible sprite, poll movement: in _physics_process, build Input.get_vector("move_left", "move_right", "move_up", "move_down"), multiply by a speed and delta, and add to position. Run and move; confirm diagonals are not faster (get_vector normalized it).
  3. Handle the attack as an event: implement _unhandled_input(event) and, on event.is_action_pressed("attack"), print "attack". Confirm it fires once per press.
  4. Contrast held vs edge: add is_action_pressed("attack") polling in _physics_process printing 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.