Free Tutorial

Godot 4 Input Handling
Keyboard, Mouse, Gamepad & Touch

by ScriptSnapMar 8, 2026~16 min read

Every game starts with a button press. Whether your player taps a touchscreen, clicks a mouse, pushes a gamepad stick, or hammers a keyboard key, the engine needs to translate that physical action into game logic. Get input handling wrong and your game feels sluggish, unresponsive, or outright broken on certain devices. Get it right and players never even think about it — they just play.

Godot 4 ships with a powerful, flexible input handling system built around the Input Map — a project-level abstraction that maps physical buttons to named actions. You write Input.is_action_pressed("jump") once, and it works on keyboard, gamepad, and touch simultaneously. Combine that with the event-driven _input() and _unhandled_input() callbacks, and you can build anything from a simple platformer to a complex RTS with full godot controller support.

In this comprehensive godot 4 input tutorial, we cover the entire input pipeline: the godot input map, polling vs event-driven patterns, godot keyboard input, mouse handling, godot gamepad axes and vibration, touch gestures, input buffering, runtime remapping, and unified input architecture. If you've already built systems like a state machine or a UI system, or are building a platformer, the patterns here will slot right in. Let's make your game feel great on every device.

1 — Input Map & Project Settings

The godot input map is the foundation of every input system in Godot 4. Open Project → Project Settings → Input Map and you'll see a table where you define named actions like move_left, jump, or attack. Each action can have multiple physical bindings: a keyboard key, a mouse button, a gamepad button, a gamepad axis, or a touch gesture. This abstraction means your game code never asks "is the W key down?" — it asks "is the move_forward action pressed?"

Godot ships with a few default actions like ui_accept, ui_cancel, and ui_left/ui_right/ui_up/ui_down. These are used internally by UI controls (buttons, sliders, tree views) to handle navigation. You can modify them, but it's usually better to create your own game-specific actions and leave the UI defaults intact.

The Input Map also lives in your project.godot file, which means it's version-controlled. When a team member adds a new action, everyone gets it on the next pull. You can also define actions in code using InputMap.add_action() and InputMap.action_add_event() — useful for runtime remapping, which we'll cover in section 8.

Setting up actions in Project Settings
# In Project Settings → Input Map, add these actions:
#
#   move_left   → A key, Left arrow, Gamepad Left Stick Left
#   move_right  → D key, Right arrow, Gamepad Left Stick Right
#   jump        → Space, Gamepad A / Cross button
#   attack      → Left Mouse Button, Gamepad X / Square button
#   pause       → Escape, Gamepad Start

# You can also set deadzone per-action (default 0.5).
# Lower deadzone = more sensitive stick input.

A common mistake is hard-coding physical keys instead of using the Input Map. If you write Input.is_key_pressed(KEY_SPACE) everywhere, you lose the ability to let players rebind controls, and gamepad users are locked out entirely. Always use action names.

2 — Polling vs Event-Driven Input

Godot gives you two fundamentally different ways to read input. Understanding when to use each is the key to responsive, bug-free controls.

Polling (the Input singleton)

Inside _process() or _physics_process(), you call methods on the global Input singleton: Input.is_action_pressed(), Input.is_action_just_pressed(), Input.get_axis(), etc. This approach checks the current state of the input right now. It's perfect for continuous actions like movement, camera rotation, and anything that needs to happen every frame.

Event-Driven (_input, _unhandled_input)

Godot also pushes InputEvent objects through a callback chain. When a key is pressed, the engine calls _input(event) on every node in the tree (top-down), then _unhandled_input(event) on nodes that haven't consumed it. This is ideal for discrete actions like pausing, opening menus, or firing a single shot. You get the exact moment the button was pressed or released, with no risk of missing a frame-perfect tap.

player.gd — polling vs events
extends CharacterBody2D

@export var speed: float = 300.0

# Polling — runs every physics frame
func _physics_process(delta: float) -> void:
    var dir = Input.get_vector(
        "move_left", "move_right",
        "move_up", "move_down"
    )
    velocity = dir * speed
    move_and_slide()

# Event-driven — fires once per press
func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("pause"):
        get_tree().paused = not get_tree().paused
        get_viewport().set_input_as_handled()

Notice the call to set_input_as_handled() in the event-driven callback. This tells Godot to stop propagating the event down the tree. Without it, every node listening for "pause" would fire. The propagation order is: _input() → GUI controls → _unhandled_input(). Use _unhandled_input() for gameplay so that UI elements (like a text field) get first priority.

📧

Want more Godot tricks?

Get our free GDScript Cheat Sheet with 20+ copy-paste snippets for signals, exports, state machines & more.

GDSJoin 500+ Godot devs · No spam · Unsubscribe anytime

3 — Keyboard Input

Godot keyboard input is the most straightforward input type. Every key on the keyboard generates an InputEventKey with properties like keycode, physical_keycode, unicode, and modifier flags (shift_pressed, ctrl_pressed, etc.). In most cases you'll use action names instead of raw keycodes, but understanding the underlying events helps when you need advanced features like text input or hotkey combos.

The distinction between keycode and physical_keycode matters for non-QWERTY layouts. keycode gives you the logical key (what character it produces), while physical_keycode gives you the physical position on the keyboard. For gameplay (WASD movement), use physical_keycode or, better yet, just bind actions in the Input Map with "physical keycode" enabled. For text input, use keycode or unicode.

keyboard_examples.gd
extends Node

func _process(delta: float) -> void:
    # Continuous hold — run while held
    if Input.is_action_pressed("sprint"):
        speed_multiplier = 1.8
    else:
        speed_multiplier = 1.0

    # Single press — fire once per tap
    if Input.is_action_just_pressed("interact"):
        try_interact()

    # Release detection — charge attacks
    if Input.is_action_just_released("charge_attack"):
        release_charge()

func _unhandled_input(event: InputEvent) -> void:
    # Detect modifier combos (Ctrl+S for quicksave)
    if event is InputEventKey and event.pressed:
        if event.keycode == KEY_S and event.ctrl_pressed:
            quicksave()
            get_viewport().set_input_as_handled()

The three core methods are is_action_pressed() (true while held), is_action_just_pressed() (true for one frame on press), and is_action_just_released() (true for one frame on release). These cover about 95% of godot keyboard input use cases. For the remaining 5% — text fields, chat input, hotkey combos — drop down to raw InputEventKey handling.

Want to build clean game architecture that handles complex input flows? Our Signals course covers the event-driven patterns that keep your code manageable.

Grab the course — $4.99 →

4 — Mouse Input

Godot mouse input comes in three flavors: InputEventMouseButton (clicks and scroll wheel), InputEventMouseMotion (cursor movement), and the position/state queries on the Input singleton. Mouse buttons can also be bound to Input Map actions, so your "attack" action can respond to both left-click and gamepad X with zero extra code.

mouse_look.gd — first-person camera
extends Camera3D

@export var sensitivity: float = 0.002

func _ready() -> void:
    # Capture the mouse cursor for FPS controls
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        # Horizontal rotation — yaw
        rotate_y(-event.relative.x * sensitivity)
        # Vertical rotation — pitch (clamped)
        rotation.x = clamp(
            rotation.x - event.relative.y * sensitivity,
            deg_to_rad(-89), deg_to_rad(89)
        )

    # Toggle cursor visibility with Escape
    if event.is_action_pressed("ui_cancel"):
        Input.mouse_mode = Input.MOUSE_MODE_VISIBLE

The relative property on InputEventMouseMotion gives you the pixel delta since the last frame — perfect for mouselook. For UI interaction, use get_global_mouse_position() to get the cursor's world-space location. For zooming, listen for InputEventMouseButton where button_index is MOUSE_BUTTON_WHEEL_UP or MOUSE_BUTTON_WHEEL_DOWN.

The Input.mouse_mode property controls cursor behavior. Set it to MOUSE_MODE_CAPTURED for FPS games (hidden, locked to center), MOUSE_MODE_CONFINED to keep the cursor inside the window, or MOUSE_MODE_VISIBLE for standard cursor behavior. Toggle between them when opening menus or pause screens.

5 — Gamepad Input

Godot gamepad support works out of the box on Windows, macOS, Linux, and consoles. The engine uses SDL2's controller database, which means most popular controllers (Xbox, PlayStation, Switch Pro, Steam Deck) are automatically mapped. Gamepad buttons and axes can be assigned to Input Map actions just like keyboard keys.

Analog Axes & Deadzone

Thumbsticks report analog values between -1.0 and 1.0. The deadzone is the minimum stick deflection before the engine registers input. Too low and the character drifts when the stick is at rest; too high and the player has to push hard before anything happens. You set deadzone per-action in the Input Map (default is 0.5), or override it in code.

gamepad_movement.gd
extends CharacterBody3D

@export var move_speed: float = 8.0
@export var look_sensitivity: float = 3.0

func _physics_process(delta: float) -> void:
    # Left stick — movement (uses Input Map deadzone)
    var move_dir = Input.get_vector(
        "move_left", "move_right",
        "move_forward", "move_back"
    )
    velocity.x = move_dir.x * move_speed
    velocity.z = move_dir.y * move_speed
    move_and_slide()

    # Right stick — camera look
    var look_dir = Input.get_vector(
        "look_left", "look_right",
        "look_up", "look_down"
    )
    rotate_y(-look_dir.x * look_sensitivity * delta)
    $Camera3D.rotation.x = clamp(
        $Camera3D.rotation.x - look_dir.y * look_sensitivity * delta,
        deg_to_rad(-80), deg_to_rad(80)
    )

Vibration / Haptic Feedback

Godot exposes controller vibration through Input.start_joy_vibration(). You pass the device index, weak motor strength, strong motor strength, and duration in seconds. Use it for hit feedback, explosions, or landing impacts. Keep vibrations short (0.1–0.3 seconds) and provide a setting to disable them — some players find vibration distracting.

vibration_helper.gd
extends Node

# device 0 = first connected controller
func light_rumble() -> void:
    Input.start_joy_vibration(0, 0.3, 0.0, 0.15)

func heavy_rumble() -> void:
    Input.start_joy_vibration(0, 0.6, 0.8, 0.25)

func stop_rumble() -> void:
    Input.stop_joy_vibration(0)

To detect controller connection and disconnection, connect to the Input.joy_connection_changed signal. This lets you show "Controller Connected" prompts or swap button icons dynamically between keyboard and gamepad glyphs.

6 — Touch Input Basics

Godot touch input uses InputEventScreenTouch (finger down/up) and InputEventScreenDrag (finger movement). Each event carries a index property identifying which finger it is, so multi-touch is supported natively. On desktop, Godot can emulate touch from mouse events (and vice versa) via Project Settings → Input Devices → Pointing.

touch_controller.gd — virtual joystick
extends Control

@export var max_radius: float = 80.0

var touch_index: int = -1
var output: Vector2 = Vector2.ZERO

func _input(event: InputEvent) -> void:
    if event is InputEventScreenTouch:
        if event.pressed and _is_inside(event.position):
            touch_index = event.index
        elif event.index == touch_index:
            touch_index = -1
            output = Vector2.ZERO

    if event is InputEventScreenDrag:
        if event.index == touch_index:
            var diff = event.position - global_position
            output = (diff / max_radius).limit_length(1.0)

The virtual joystick above tracks a single finger, calculates the offset from center, and normalizes it to a -1 to 1 range. Your player controller reads output the same way it would read Input.get_vector(). For a fire button, add a second Control that watches for a tap event inside its bounds and calls your attack function. If you've built a UI system with anchors, you already know how to position touch controls that scale across screen sizes.

Organizing complex input states? Our State Machines course teaches the pattern that keeps player controllers clean and bug-free.

Start the course — $4.99 →

7 — Input Buffering Patterns

Input buffering (sometimes called "coyote time" for jumps) is the secret to making a game feel responsive. The idea: when the player presses a button slightly too early (before they can actually perform the action), you remember the input and execute it as soon as the action becomes available. Without buffering, players must press jump at the exact frame they land — which feels terrible. With a 100ms buffer, inputs feel forgiving and responsive.

input_buffer.gd
extends Node

# Buffer duration in seconds
const BUFFER_TIME: float = 0.12

var _jump_buffer: float = 0.0
var _coyote_timer: float = 0.0
var _was_on_floor: bool = false

func _physics_process(delta: float) -> void:
    var on_floor = owner.is_on_floor()

    # Coyote time: allow jump briefly after leaving edge
    if _was_on_floor and not on_floor:
        _coyote_timer = BUFFER_TIME
    _coyote_timer = max(_coyote_timer - delta, 0.0)

    # Jump buffer: remember press even if not grounded yet
    if Input.is_action_just_pressed("jump"):
        _jump_buffer = BUFFER_TIME
    _jump_buffer = max(_jump_buffer - delta, 0.0)

    # Execute jump if both conditions met
    var can_jump = on_floor or _coyote_timer > 0.0
    if _jump_buffer > 0.0 and can_jump:
        owner.velocity.y = -400.0
        _jump_buffer = 0.0
        _coyote_timer = 0.0

    _was_on_floor = on_floor

This same pattern works for any delayed action: attack combos (buffer the next attack during the current animation), dash (buffer while the dash is cooling down), or wall-jump (allow a brief window after leaving the wall). The buffer time is typically 80–150ms. Playtest and adjust until the controls feel tight but forgiving.

8 — Custom Input Remapping at Runtime

Players expect to rebind controls. Godot 4 makes runtime input remapping straightforward with the InputMap singleton. The workflow: erase the old binding, wait for the player to press a new key/button, then add the new event to the action. Store the mapping in a config file so it persists between sessions.

rebind_ui.gd — key rebinding screen
extends Control

var _action_to_rebind: String = ""
var _waiting_for_input: bool = false

# Called when the player clicks "Rebind" next to an action
func start_rebind(action: String) -> void:
    _action_to_rebind = action
    _waiting_for_input = true
    $PromptLabel.text = "Press a key or button…"

func _input(event: InputEvent) -> void:
    if not _waiting_for_input:
        return

    # Accept keyboard, mouse button, or gamepad button
    if event is InputEventKey or \
       event is InputEventMouseButton or \
       event is InputEventJoypadButton:
        if not event.pressed:
            return

        # Remove old events of the same type
        for old_event in InputMap.action_get_events(_action_to_rebind):
            if old_event.get_class() == event.get_class():
                InputMap.action_erase_event(_action_to_rebind, old_event)

        # Add the new binding
        InputMap.action_add_event(_action_to_rebind, event)
        _waiting_for_input = false
        _save_bindings()
        get_viewport().set_input_as_handled()

func _save_bindings() -> void:
    # Serialize to a ConfigFile or JSON and save to user://
    var config = ConfigFile.new()
    for action in InputMap.get_actions():
        if action.begins_with("ui_"):
            continue  # Skip built-in UI actions
        for i in InputMap.action_get_events(action).size():
            var ev = InputMap.action_get_events(action)[i]
            config.set_value(action, str(i), ev)
    config.save("user://input_bindings.cfg")

To load bindings on startup, read the ConfigFile and call InputMap.action_erase_events() followed by InputMap.action_add_event() for each stored event. If you've built a save system, the persistence pattern is identical — just with input events instead of game state.

9 — Combining Input Sources (Unified Input)

A polished game lets players switch between keyboard, gamepad, and touch seamlessly — sometimes mid-session. The pattern: track the "last used device" and swap UI prompts, button icons, and cursor visibility accordingly. Godot's Input Map already unifies the logic (actions work regardless of device), but your presentation layer needs to adapt too.

device_tracker.gd — autoload singleton
extends Node

enum Device { KEYBOARD, GAMEPAD, TOUCH }

signal device_changed(device: Device)

var current_device: Device = Device.KEYBOARD

func _input(event: InputEvent) -> void:
    var new_device = _detect_device(event)
    if new_device != current_device:
        current_device = new_device
        device_changed.emit(new_device)
        _update_cursor(new_device)

func _detect_device(event: InputEvent) -> Device:
    if event is InputEventKey or event is InputEventMouseMotion:
        return Device.KEYBOARD
    if event is InputEventJoypadButton or event is InputEventJoypadMotion:
        return Device.GAMEPAD
    if event is InputEventScreenTouch or event is InputEventScreenDrag:
        return Device.TOUCH
    return current_device

func _update_cursor(device: Device) -> void:
    match device:
        Device.KEYBOARD:
            Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
        Device.GAMEPAD:
            Input.mouse_mode = Input.MOUSE_MODE_HIDDEN
        Device.TOUCH:
            Input.mouse_mode = Input.MOUSE_MODE_HIDDEN

Register this as an autoload and any node in your game can connect to DeviceTracker.device_changed to swap button prompts, show/hide touch controls, or adjust aim-assist strength. This is especially important for Steam Deck compatibility, where players switch between gamepad and trackpad constantly. If you're using a signal bus, route the device change through it to keep your dependencies clean.

Master the architecture patterns that tie input, state, and UI together. The Bundle course covers signals, state machines, and more.

Get the Bundle — Save 40% →

10 — Common Patterns & Best Practices

After building input systems across dozens of Godot projects, these are the patterns that consistently lead to clean, maintainable, and player-friendly code.

Always Use Action Names

Never hard-code physical keys. Input.is_action_pressed("jump") works on every device. Input.is_key_pressed(KEY_SPACE) only works on keyboards and cannot be rebound. The Input Map exists for a reason — use it.

Use _unhandled_input for Gameplay

Let UI consume events first. If a LineEdit is focused and the player types "W", you don't want the character to walk forward. Put gameplay input in _unhandled_input() so the UI layer gets priority. Only use _input() for input that should bypass the UI, like pause toggles.

Use get_vector for Movement

Instead of four separate is_action_pressed() calls and manual vector math, use Input.get_vector("left", "right", "up", "down"). It automatically normalizes diagonal movement, applies deadzones for gamepad sticks, and returns a clean Vector2.

Separate Input Reading from Action Execution

Don't put movement physics inside _unhandled_input(). Read input into variables (velocity, direction, buffered actions) and then act on those variables in _physics_process(). This separation makes your code easier to test, replay, and extend with features like input recording or AI bots that inject synthetic input.

Action Strength for Analog Blending

Input.get_action_strength("accelerate") returns a float between 0.0 and 1.0. On a keyboard it's binary (0 or 1), but on a gamepad trigger it's a smooth analog value. Use this for racing games, camera zoom, or any action that benefits from proportional control. Your code handles both input types with zero branching.

Test on Multiple Devices Early

Don't wait until launch to plug in a gamepad. Test with keyboard, mouse, and at least one controller from the start. If you're targeting mobile, test touch controls on a real phone — emulators don't capture thumb ergonomics. Steam Deck compatibility is essentially free if you follow the unified input pattern from section 9.

input_cheat_sheet.gd — quick reference
# ─── Polling (in _process or _physics_process) ───
Input.is_action_pressed("action")       # true while held
Input.is_action_just_pressed("action")  # true one frame
Input.is_action_just_released("action") # true one frame
Input.get_action_strength("action")    # 0.0 to 1.0
Input.get_axis("left", "right")          # -1.0 to 1.0
Input.get_vector("l", "r", "u", "d")         # normalized Vector2

# ─── Event-driven (in _input or _unhandled_input) ───
event.is_action_pressed("action")       # just pressed
event.is_action_released("action")      # just released
event is InputEventKey                # keyboard
event is InputEventMouseMotion        # mouse move
event is InputEventJoypadButton       # gamepad button
event is InputEventScreenTouch        # touch down/up

# ─── Gamepad extras ───
Input.start_joy_vibration(device, weak, strong, duration)
Input.get_connected_joypads()         # Array of device IDs
Input.get_joy_name(device)            # "Xbox Controller"

# ─── Cursor ───
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
get_viewport().set_input_as_handled()        # consume event

Keep this cheat sheet handy. Between the Input Map, the Input singleton, and the event callbacks, Godot 4 gives you everything you need to ship professional-grade controls on every platform. Start with actions, add buffering for game feel, and layer in device tracking for a polished multi-platform experience.

Next Steps

You now have a complete foundation for godot 4 input handling across every device. From the Input Map to buffering, remapping, and unified device tracking — you're equipped to build controls that feel professional. Here are some paths forward:

  • Follow the platformer tutorial to apply everything you learned here — movement, jumping, and coyote time with CharacterBody2D.
  • Build a state machine to manage complex player states driven by input (idle, running, jumping, attacking).
  • Wire your input to a signal bus so game systems react to player actions without tight coupling.
  • Create a settings UI with the rebinding system from section 8 and expose sensitivity sliders.
  • Add animation blending driven by input strength for smooth walk-to-run transitions.
  • Persist your key bindings with the save system so players keep their custom controls across sessions.

Grab our free GDScript cheat sheet for a printable quick-reference of these input methods and more. Great controls are invisible. The player should never fight the input system — they should fight the enemies, solve the puzzles, and lose themselves in the world you've built. Start with the Input Map, add buffering for feel, and ship with confidence on keyboard, gamepad, and touchscreen.

Free download
+ weekly tips

Get the GDScript Input Cheat Sheet

Subscribe for a printable input reference card, plus weekly Godot tips and tutorials straight to your inbox.

20+ GDScript snippets
Weekly Godot tips
Zero spam, cancel anytime
>
GDSJoin 500+ Godot devs. Unsubscribe in one click.