Godot 2D Movement (4.x)
Build responsive 2D character controllers with CharacterBody2D and the argument-less
move_and_slide(). Targets Godot 4.7.
When to use
- Use when scripting a 2D player/enemy that moves and collides: platformer (gravity +
jump) or top-down (free 8-direction), slope walking, or wall/floor detection.
- Use when
move_and_slide() "does nothing", the character sinks through floors, or
is_on_floor() is always false.
When not to use: dynamic rigid bodies, areas, raycasts, collision layers →
godot-physics; tile-based levels → godot-tilemap; full platformer game template →
the platformer genre skill; engine-agnostic feel tuning → physics-tuning.
Core workflow
- Use
CharacterBody2D with a CollisionShape2D child. It is script-driven: it
does not fall or react to forces on its own.
- Set the
velocity property, then call move_and_slide() (no arguments in 4.x).
The method reads velocity, moves the body, slides along surfaces, and updates
velocity to reflect what actually happened.
- Do it in
_physics_process(delta) — move_and_slide() uses the physics step's
delta internally, so do not multiply velocity by delta yourself.
- Platformer: add gravity to
velocity.y each tick, jump by setting velocity.y
negative when is_on_floor().
- Top-down: build a direction from input and scale by speed; set
motion_mode = MOTION_MODE_FLOATING so there is no "floor/wall" distinction.
- Read results with
is_on_floor(), is_on_wall(), get_wall_normal(), and
get_slide_collision(i) after the call.
Patterns
1. Platformer controller (gravity, jump, slopes)
extends CharacterBody2D
@export var speed := 200.0
@export var jump_velocity := -400.0
@export var gravity := 1200.0 # pixels/sec^2 (tune to taste)
func _physics_process(delta: float) -> void:
# Apply gravity while airborne.
if not is_on_floor():
velocity.y += gravity * delta
# Jump only when grounded (Input action set in Project Settings > Input Map).
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
# Horizontal input: -1, 0, or 1.
var dir := Input.get_axis("move_left", "move_right")
if dir != 0.0:
velocity.x = dir * speed
else:
velocity.x = move_toward(velocity.x, 0.0, speed) # decelerate to a stop
move_and_slide() # 4.x: no arguments; uses and updates `velocity`
2. Top-down 8-direction movement
extends CharacterBody2D
@export var speed := 220.0
func _ready() -> void:
motion_mode = CharacterBody2D.MOTION_MODE_FLOATING # no floor/ceiling concept
func _physics_process(_delta: float) -> void:
# get_vector returns a normalized-ish Vector2 from four input actions.
var input := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = input * speed
move_and_slide()
3. Reading slide collisions after moving
func _physics_process(delta: float) -> void:
# ... set velocity ...
move_and_slide()
for i in get_slide_collision_count():
var c := get_slide_collision(i)
var other := c.get_collider()
if other and other.is_in_group("enemies"):
take_damage(1) # touched an enemy while moving
4. Coyote time (forgiving jump just after leaving a ledge)
@export var coyote_time := 0.1
var _coyote := 0.0
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
_coyote -= delta
else:
_coyote = coyote_time
if Input.is_action_just_pressed("jump") and _coyote > 0.0:
velocity.y = jump_velocity
_coyote = 0.0
# ... horizontal input + move_and_slide() ...
Pitfalls
- 3.x signature is gone.
move_and_slide(velocity) (returning the new velocity) was
removed. In 4.x set the velocity property and call move_and_slide() with no args.
move_and_slide(velocity, Vector2.UP) will not parse.
- Multiplying velocity by delta.
move_and_slide() already accounts for the physics
delta. Setting velocity = dir * speed * delta makes the body crawl.
- Movement in
_process instead of _physics_process causes frame-rate-dependent,
jittery motion. Always move in _physics_process.
is_on_floor() always false when up_direction is wrong (default Vector2.UP),
there is no CollisionShape2D, or you never called move_and_slide() this frame.
- Sinking through floors usually means the collision shape is missing/zero-sized, the
floor body is on a layer this body's mask doesn't include, or you moved the body via
position += instead of velocity + move_and_slide().
- Slope slipping while idle: keep
floor_stop_on_slope = true (default) and set a
floor_snap_length so the body sticks to downward slopes.
References
- For one-way platforms, moving platforms, jump buffering, variable jump height, and
move_and_collide() (manual collision response), read references/controller-recipes.md.
Related skills
godot-physics — collision layers/masks, areas, raycasts, rigid bodies.
godot-tilemap — building the levels this character walks on.
godot-animation — driving sprite/skeletal animation from movement state.
camera-systems — follow camera, deadzone, and look-ahead that track this character.
platformer / input-systems — full genre template and rebindable input.
1---
2name: godot-2d-movement
3description: >
4 Implement 2D kinematic character movement in Godot 4.7 with CharacterBody2D and
5 move_and_slide(): platformer run/jump with gravity, top-down 8-direction motion,
6 slope handling, and reading collisions. Use when coding a 2D player or enemy
7 controller, a platformer or top-down character, or fixing move_and_slide()/
8 is_on_floor() behavior in a .tscn with a CharacterBody2D.
9---
10
11# Godot 2D Movement (4.x)
12
13Build responsive 2D character controllers with `CharacterBody2D` and the argument-less
14`move_and_slide()`. Targets **Godot 4.7**.
15
16## When to use
17
18- Use when scripting a 2D player/enemy that moves and collides: platformer (gravity +
19 jump) or top-down (free 8-direction), slope walking, or wall/floor detection.
20- Use when `move_and_slide()` "does nothing", the character sinks through floors, or
21 `is_on_floor()` is always false.
22
23**When *not* to use:** dynamic rigid bodies, areas, raycasts, collision layers →
24`godot-physics`; tile-based levels → `godot-tilemap`; full platformer game template →
25the `platformer` genre skill; engine-agnostic feel tuning → `physics-tuning`.
26
27## Core workflow
28
291. **Use `CharacterBody2D`** with a `CollisionShape2D` child. It is script-driven: it
30 does not fall or react to forces on its own.
312. **Set the `velocity` property, then call `move_and_slide()`** (no arguments in 4.x).
32 The method reads `velocity`, moves the body, slides along surfaces, and updates
33 `velocity` to reflect what actually happened.
343. **Do it in `_physics_process(delta)`** — `move_and_slide()` uses the physics step's
35 delta internally, so do not multiply `velocity` by `delta` yourself.
364. **Platformer:** add gravity to `velocity.y` each tick, jump by setting `velocity.y`
37 negative when `is_on_floor()`.
385. **Top-down:** build a direction from input and scale by speed; set
39 `motion_mode = MOTION_MODE_FLOATING` so there is no "floor/wall" distinction.
406. **Read results** with `is_on_floor()`, `is_on_wall()`, `get_wall_normal()`, and
41 `get_slide_collision(i)` after the call.
42
43## Patterns
44
45### 1. Platformer controller (gravity, jump, slopes)
46
47```gdscript
48extends CharacterBody2D
49
50@export var speed := 200.0
51@export var jump_velocity := -400.0
52@export var gravity := 1200.0 # pixels/sec^2 (tune to taste)
53
54func _physics_process(delta: float) -> void:
55 # Apply gravity while airborne.
56 if not is_on_floor():
57 velocity.y += gravity * delta
58
59 # Jump only when grounded (Input action set in Project Settings > Input Map).
60 if Input.is_action_just_pressed("jump") and is_on_floor():
61 velocity.y = jump_velocity
62
63 # Horizontal input: -1, 0, or 1.
64 var dir := Input.get_axis("move_left", "move_right")
65 if dir != 0.0:
66 velocity.x = dir * speed
67 else:
68 velocity.x = move_toward(velocity.x, 0.0, speed) # decelerate to a stop
69
70 move_and_slide() # 4.x: no arguments; uses and updates `velocity`
71```
72
73### 2. Top-down 8-direction movement
74
75```gdscript
76extends CharacterBody2D
77
78@export var speed := 220.0
79
80func _ready() -> void:
81 motion_mode = CharacterBody2D.MOTION_MODE_FLOATING # no floor/ceiling concept
82
83func _physics_process(_delta: float) -> void:
84 # get_vector returns a normalized-ish Vector2 from four input actions.
85 var input := Input.get_vector("move_left", "move_right", "move_up", "move_down")
86 velocity = input * speed
87 move_and_slide()
88```
89
90### 3. Reading slide collisions after moving
91
92```gdscript
93func _physics_process(delta: float) -> void:
94 # ... set velocity ...
95 move_and_slide()
96 for i in get_slide_collision_count():
97 var c := get_slide_collision(i)
98 var other := c.get_collider()
99 if other and other.is_in_group("enemies"):
100 take_damage(1) # touched an enemy while moving
101```
102
103### 4. Coyote time (forgiving jump just after leaving a ledge)
104
105```gdscript
106@export var coyote_time := 0.1
107var _coyote := 0.0
108
109func _physics_process(delta: float) -> void:
110 if not is_on_floor():
111 velocity.y += gravity * delta
112 _coyote -= delta
113 else:
114 _coyote = coyote_time
115
116 if Input.is_action_just_pressed("jump") and _coyote > 0.0:
117 velocity.y = jump_velocity
118 _coyote = 0.0
119 # ... horizontal input + move_and_slide() ...
120```
121
122## Pitfalls
123
124- **3.x signature is gone.** `move_and_slide(velocity)` (returning the new velocity) was
125 removed. In 4.x set the `velocity` property and call `move_and_slide()` with no args.
126 `move_and_slide(velocity, Vector2.UP)` will not parse.
127- **Multiplying velocity by delta.** `move_and_slide()` already accounts for the physics
128 delta. Setting `velocity = dir * speed * delta` makes the body crawl.
129- **Movement in `_process`** instead of `_physics_process` causes frame-rate-dependent,
130 jittery motion. Always move in `_physics_process`.
131- **`is_on_floor()` always false** when `up_direction` is wrong (default `Vector2.UP`),
132 there is no `CollisionShape2D`, or you never called `move_and_slide()` this frame.
133- **Sinking through floors** usually means the collision shape is missing/zero-sized, the
134 floor body is on a layer this body's mask doesn't include, or you moved the body via
135 `position +=` instead of velocity + `move_and_slide()`.
136- **Slope slipping while idle:** keep `floor_stop_on_slope = true` (default) and set a
137 `floor_snap_length` so the body sticks to downward slopes.
138
139## References
140
141- For one-way platforms, moving platforms, jump buffering, variable jump height, and
142 `move_and_collide()` (manual collision response), read `references/controller-recipes.md`.
143
144## Related skills
145
146- `godot-physics` — collision layers/masks, areas, raycasts, rigid bodies.
147- `godot-tilemap` — building the levels this character walks on.
148- `godot-animation` — driving sprite/skeletal animation from movement state.
149- `camera-systems` — follow camera, deadzone, and look-ahead that track this character.
150- `platformer` / `input-systems` — full genre template and rebindable input.