GDScript Input Handling Cheat Sheet
Quick-reference for Godot 4 input handling. Each section includes copy-ready snippets with inline output comments for player movement, shooting, and UI controls.
Action-Based Input
Define input actions in Project Settings > Input Map. Use action names instead of raw keycodes for remappable controls.
func _physics_process(delta: float) -> void:
if Input.is_action_pressed("move_right"):
velocity.x = speed
if Input.is_action_pressed("sprint"):
velocity.x *= 2.0func _physics_process(delta: float) -> void:
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_force
if Input.is_action_just_pressed("attack"):
swing_sword()func _physics_process(delta: float) -> void:
if Input.is_action_just_released("charge_attack"):
release_charged_attack()Axis and Vector Input
get_axis() returns a float from -1 to 1. get_vector() returns a Vector2 — perfect for 2D movement.
# Returns -1, 0, or 1 (or analog value)
var horizontal := Input.get_axis("move_left", "move_right")
# -1 = left, 0 = idle, 1 = right
velocity.x = horizontal * speed# Returns a Vector2 — already handles diagonal normalization
var input := Input.get_vector(
"move_left", "move_right",
"move_up", "move_down"
)
velocity = input * speed
move_and_slide()get_vector() auto-normalizes diagonal input. No need to call .normalized() yourself.
# get_vector works with analog sticks too
var input := Input.get_vector(
"move_left", "move_right",
"move_up", "move_down"
)
# input.length() ranges 0.0 to 1.0 with analog sticks
if input.length() > 0.2: # deadzone
velocity = input * speed_input vs _unhandled_input
Godot routes input events through the tree. _input catches everything; _unhandled_input only gets events that UI nodes did not consume.
# Use for gameplay actions (movement, shooting, etc.)
# UI elements like Button consume events first
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("interact"):
interact_with_nearest()# Use for global shortcuts that should work even over UI
func _input(event: InputEvent) -> void:
if event.is_action_pressed("pause"):
toggle_pause()
get_viewport().set_input_as_handled()func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("interact"):
open_dialog()
# Prevent other nodes from also handling this event
get_viewport().set_input_as_handled()Call set_input_as_handled() to stop the event from propagating to other nodes.
InputEvent Types
Every input arrives as an InputEvent subclass. Check the type to handle specific devices.
func _input(event: InputEvent) -> void:
if event is InputEventKey:
print("Keyboard: ", event.keycode)
elif event is InputEventMouseButton:
print("Mouse button: ", event.button_index)
elif event is InputEventMouseMotion:
print("Mouse moved: ", event.relative)
elif event is InputEventJoypadButton:
print("Gamepad button: ", event.button_index)
elif event is InputEventJoypadMotion:
print("Gamepad axis: ", event.axis, " value: ", event.axis_value)func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed:
match event.keycode:
KEY_ESCAPE:
toggle_pause()
KEY_F11:
toggle_fullscreen()Mouse Input
Handle mouse position, clicks, scroll wheel, and cursor visibility.
# Screen coordinates
var screen_pos := get_viewport().get_mouse_position()
# World coordinates (2D)
var world_pos := get_global_mouse_position()
# Aim at mouse
look_at(get_global_mouse_position())func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
shoot()
elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
use_ability()
elif event.button_index == MOUSE_BUTTON_WHEEL_UP:
zoom_in()func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
# FPS camera look
rotate_y(-event.relative.x * mouse_sensitivity)
$Camera3D.rotate_x(-event.relative.y * mouse_sensitivity)Custom Mouse Cursor
Replace the default mouse cursor with a game-specific crosshair or pointer.
func _ready() -> void:
var crosshair := load("res://assets/ui/crosshair.png")
Input.set_custom_mouse_cursor(crosshair, Input.CURSOR_ARROW, Vector2(16, 16))
# Vector2(16, 16) = hotspot (center of 32x32 cursor)func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# Mouse is hidden and locked to center
func _input(event: InputEvent) -> void:
if event.is_action_pressed("pause"):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE# Mouse visible but cannot leave the game window
Input.mouse_mode = Input.MOUSE_MODE_CONFINEDMOUSE_MODE_CAPTURED is for FPS. MOUSE_MODE_CONFINED keeps cursor visible but trapped.
Gamepad and Controller
Godot automatically maps standard controllers. Use the same action names for keyboard and gamepad.
# In Project Settings > Input Map:
# "jump" -> Space key + Joypad Button 0 (A/Cross)
# "attack" -> Left Click + Joypad Button 2 (X/Square)
# Code is the same regardless of device:
if Input.is_action_just_pressed("jump"):
jump()func _input(event: InputEvent) -> void:
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
show_gamepad_prompts()
elif event is InputEventKey or event is InputEventMouseButton:
show_keyboard_prompts()# device_id is usually 0 for first controller
Input.start_joy_vibration(0, 0.5, 0.5, 0.3)
# (device, weak_magnitude, strong_magnitude, duration)
# Stop vibration
Input.stop_joy_vibration(0)Input Action Mapping in Code
Add or modify input actions at runtime for rebindable controls.
func _ready() -> void:
if not InputMap.has_action("screenshot"):
InputMap.add_action("screenshot")
var event := InputEventKey.new()
event.keycode = KEY_F12
InputMap.action_add_event("screenshot", event)func rebind_action(action_name: String, new_event: InputEvent) -> void:
# Clear existing bindings
InputMap.action_erase_events(action_name)
# Add new binding
InputMap.action_add_event(action_name, new_event)func get_action_key_name(action: String) -> String:
var events := InputMap.action_get_events(action)
if events.size() > 0 and events[0] is InputEventKey:
return OS.get_keycode_string(events[0].keycode)
return "Unbound"Input Best Practices
Patterns that avoid common input bugs in Godot 4.
# Movement and physics-affecting input:
func _physics_process(delta: float) -> void:
var input := Input.get_vector("left", "right", "up", "down")
velocity = input * speed
move_and_slide()
# One-shot actions can go in _unhandled_input:
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("interact"):
interact()Continuous input (movement) goes in _physics_process. One-shot actions (interact, pause) go in _unhandled_input.
var input_enabled := true
func _physics_process(delta: float) -> void:
if not input_enabled:
return
# Normal input handling...
func start_cutscene() -> void:
input_enabled = false
func end_cutscene() -> void:
input_enabled = truevar jump_buffer_timer := 0.0
const JUMP_BUFFER := 0.1 # 100ms buffer
func _physics_process(delta: float) -> void:
if Input.is_action_just_pressed("jump"):
jump_buffer_timer = JUMP_BUFFER
jump_buffer_timer -= delta
if jump_buffer_timer > 0.0 and is_on_floor():
velocity.y = jump_force
jump_buffer_timer = 0.0Input buffering lets the player press jump slightly before landing and still get the jump.
Can you write this from memory?
Define the lifecycle callback that runs at a fixed rate for physics.