~2 min read

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()
Web

addEventListener('keydown', ...), addEventListener('keyup', ...) + your state object that updates “which keys are held”.

Unity

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:

  1. _input on all nodes.
  2. The GUI handles the event if it touches a Control node.
  3. _shortcut_input — for menu hotkeys.
  4. _unhandled_key_input — keyboard only, after the GUI.
  5. _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.

MOUSE_MODE_CAPTURED — a must-have for FPS

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.