5. Actions, not keys
3 minRead input by name so remapping and controllers just work.
| Action | Bound to |
|---|---|
move_left | A← |
move_right | D→ |
jump | Space |
Input.get_axis("move_left", "move_right") Input.is_action_just_pressed("jump") false true for exactly one framemoved left moved right jumped 0/3
Gameplay code in Godot never asks which key is down. It asks for an action defined in the Input Map, and the map decides which keys, buttons and sticks count. Hold A and D, or the on-screen buttons, and watch what get_axis returns; tap Space and watch a one-shot action fire once.
Coming from Unity: Input Actions. Unreal: Enhanced Input. Set the actions up before writing the controller, not after.
To finish: Move both ways and jump once.
Notes
The written version of this lesson, for after you have done it.Gameplay code in Godot asks for a named action, never for a physical key. Actions live in Project Settings under Input Map, where each one lists the keys, mouse buttons, and controller inputs that count. A script that reads move_left keeps working when a player rebinds it or plugs in a gamepad, because the mapping changed and the code did not.
Polling and events are different tools. Input.get_axis and Input.get_vector are polling: every physics tick they report what is held right now, which is right for continuous movement. Input.is_action_just_pressed is true for exactly one frame, which is right for a jump or a shot. Reading a one-shot action by polling gives you the action once per frame for as long as the key is down.
Two details matter early. Analogue sticks report a small value at rest, so each action carries a dead zone Godot applies before your code sees anything. And get_vector returns a normalised vector, so holding two directions does not move the character faster than holding one; adding two separate axis reads gives a diagonal about 1.41 times too fast.
Where the reading happens matters as much as how. Held movement belongs in _physics_process, because velocity is applied on the physics tick. A one-shot press can be read there too, but handling it in _input(event) with event.is_action_pressed("jump") means the press is consumed exactly once, before any polling code sees it, and _unhandled_input is the place for gameplay input that UI has not already used. Controllers need nothing extra: bind a joypad button or axis to the same action and every line of gameplay code already works.
For two axes at once use the four-action form: `Input.get_vector("move_left", "move_right", "move_up", "move_down")` returns a Vector2 that is already normalised and already has the dead zone applied, so a top-down controller is one line of reading plus a multiply. Mouse buttons bind to actions exactly like keys, so `shoot` can be Left Click on desktop and a shoulder button on a controller with the same script.
Try it in the editor five minutes, in a real project
- Open Project Settings, go to the Input Map tab, and add an action called dash. Bind Shift and a gamepad button to it.
- In player.gd, inside _physics_process, add: if Input.is_action_just_pressed("dash"): velocity.x *= 3. Run the scene and tap Shift.
- Hold Shift instead. It fires once, because just_pressed is true for one frame. Change it to is_action_pressed and hold again to feel the difference.
- Plug in a controller if you have one. The same line works with the bound button and no code changed.
move_leftA, ←- One side of a movement axis. Read both sides at once with Input.get_axis("move_left", "move_right"), which returns -1 when only this side is held.
move_rightD, →- One side of a movement axis. Read both sides at once with Input.get_axis("move_left", "move_right"), which returns 1 when only this side is held.
jumpSpace- A one-shot action. Read it with Input.is_action_just_pressed, which is true for exactly one frame.