Input — the Input Map and action-based system
How to read the keyboard, mouse, gamepad, and touch through a single action system.
In Godot, input is a single system: you describe actions in Project Settings and access them from any script. There are no two competing APIs as in Unity (legacy Input vs Input System).
Input Map
Project → Project Settings → Input Map. You create an action (for example, jump) and bind to it
any number of keys / gamepad buttons / mouse / touch:
move_forward ← W, ↑, left-stick-Y+
move_back ← S, ↓, left-stick-Y−
move_left ← A, ←, left-stick-X−
move_right ← D, →, left-stick-X+
jump ← Space, A (gamepad)
fire ← left click, RT (gamepad)
crouch ← Ctrl, B (gamepad)
This is the only configuration — there is nothing else to set up.
Polling input through Input
In a script, you read an action through the global Input (singleton):
func _physics_process(delta: float) -> void:
# a 1D axis from two actions: returns a float in [-1, 1]
var horizontal := Input.get_axis("move_left", "move_right")
# a 2D vector from four actions: returns a Vector2
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
# just a press check (repeats every frame while held)
if Input.is_action_pressed("crouch"):
crouch()
# only on the frame of the press
if Input.is_action_just_pressed("jump"):
velocity.y = jump_speed
# only on the frame of the release
if Input.is_action_just_released("fire"):
stop_charging()
addEventListener('keydown', ...), addEventListener('keyup', ...) + your state object
that updates “which keys are held”.
Input.GetAxis("Horizontal") ↔ Input.get_axis("move_left", "move_right").
Input.GetButtonDown("Jump") ↔ Input.is_action_just_pressed("jump"). Similar, but the names
are fully configurable.
_input(event) and _unhandled_input(event)
Alternatively — an event-driven approach. Godot passes every event through the node tree:
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("pause"):
toggle_pause()
get_viewport().set_input_as_handled()
The difference between _input and _unhandled_input:
_input— receives all events before the UI handles them._unhandled_input— receives events that the UI did not consume. This is the right path for gameplay input, because clicking a menu button won’t fire a shot.
The propagation hierarchy:
_inputon all nodes.- The GUI handles the event if it touches a Control node.
_shortcut_input— for menu hotkeys._unhandled_key_input— keyboard only, after the GUI._unhandled_input— everything else, after the GUI.
Rotating the camera with the mouse
A classic FPS look:
extends CharacterBody3D
@export var sensitivity: float = 0.003
@export var max_pitch: float = 1.4 # ~80°
@onready var head: Node3D = $Head
@onready var camera: Camera3D = $Head/Camera3D
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED # hide the cursor and capture it
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
rotate_y(-event.relative.x * sensitivity)
head.rotate_x(-event.relative.y * sensitivity)
head.rotation.x = clamp(head.rotation.x, -max_pitch, max_pitch)
InputEventMouseMotion.relative is the delta compared to the previous frame, which is exactly what we need.
Without it, the mouse will leave the window on rotation. Captured mode hides the cursor and centers it every frame. ESC — the standard habit of releasing the cursor:
if event.is_action_pressed("ui_cancel"):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Raycast — where the player is looking
Two paths in Godot:
1. The RayCast3D node
Declaratively in the scene. You add a child RayCast3D and, in the Inspector, set target_position
(in local coordinates, e.g. (0, 0, -100) — 100 m forward). Enable Enabled.
@onready var aim_ray: RayCast3D = $Camera3D/AimRay
func shoot() -> void:
if aim_ray.is_colliding():
var hit = aim_ray.get_collider() # Node3D or Object
var point = aim_ray.get_collision_point()
var normal = aim_ray.get_collision_normal()
if hit.has_method("take_damage"):
hit.take_damage(10)
RayCast3D updates automatically in _physics_process.
2. A programmatic raycast through the PhysicsServer
If you need an “on-demand” ray with arbitrary parameters:
func shoot_from_camera() -> void:
var space = get_world_3d().direct_space_state
var from = camera.global_position
var to = from - camera.global_transform.basis.z * 100.0 # 100 m forward
var query = PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 1 # layer 1 only
query.exclude = [self] # don't hit yourself
var hit := space.intersect_ray(query)
if not hit.is_empty():
var collider = hit.collider
var point = hit.position
var normal = hit.normal
intersect_ray returns a Dictionary with the fields collider, position, normal, rid, shape,
or an empty dict on a miss.
Touch and multi-touch
Touch events arrive as InputEventScreenTouch (press/release) and InputEventScreenDrag
(movement). Each one has an index finger identifier:
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
if event.pressed:
print("Finger ", event.index, " at ", event.position)
For typical mobile controls (virtual joysticks, buttons) — the Control nodes TouchScreenButton
and VirtualJoystick (the latter — in 4.7).
The next chapter covers physics.