Godot 4 Input Handling
Keyboard, Mouse, Gamepad & Touch
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.
Table of Contents
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.
# 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.
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.
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.
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.
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.
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_VISIBLEThe 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.
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.
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.
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.
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.
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_floorThis 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.
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.
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_HIDDENRegister 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.
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.
# ─── 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 eventKeep 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.
Get the GDScript Input Cheat Sheet
Subscribe for a printable input reference card, plus weekly Godot tips and tutorials straight to your inbox.
Clean patterns that make input, state, and signals work together.
Mastering GDScript Signals
Signal buses, typed signals, async patterns — the definitive signals course for Godot 4.
$4.99 · 15 min →State Machines in GDScript
Clean game logic with reusable state machines. Perfect for AI, UI, and game flow.
$4.99 · 15 min →Godot Pro Pack Bundle
All courses at a discount. Signals, shaders, state machines, multiplayer, and more.
$12.99 · Save 50%+ →Keep Reading
Godot 4 Platformer Tutorial
Apply your input handling skills — build a full platformer with movement, jumping, and coyote time.
Godot 4 State Machine Tutorial
Build clean, scalable AI and player states driven by input with pure GDScript.
Godot 4 UI Tutorial
Build responsive menus, HUDs, and settings screens — including key rebind UIs.
Godot 4 Animation Tutorial
AnimationPlayer, AnimationTree, and blending driven by player input.
Godot 4 Save System Tutorial
Persist game data and custom key bindings with JSON and Resources.