M2.1 — Player Movement with CharacterBody2D¶
What you'll learn
- Use
CharacterBody2D(notRigidBody2D) for a code-driven character, with aCollisionShape2D. - Move in
_physics_processwithInput.get_vector, a speed constant, andmove_and_slide. - Keep motion frame-rate independent — the first felt payoff, a character you can walk around.
How it applies
- Movement feel is the genre's handshake. Before loot or stats, the player judges an ARPG by how
it feels to move. Frame-rate-independent motion (via
_physics_processanddelta) means the character covers the same ground per second on a 60 Hz laptop and a 144 Hz desktop — without it, the game is literally faster on better hardware, which speedrunners exploit and testers can't reproduce across machines. - Diagonal speed is a real bug. Naively adding X and Y input makes diagonal movement ~1.41×
faster than cardinal.
Input.get_vectornormalizes, so the player can't gain speed by walking diagonally — a fairness property in a genre about kiting enemies. - The collision body is the contract with the world.
CharacterBody2D+ a collision shape is what lets walls stop the player and, in M3, what enemies' attacks register against. Choosing the right body type now avoids fighting the physics engine later. - Determinism for QA. A fixed physics step means "hold right for 1 second" travels a repeatable distance every run — a property a tester can assert. Tie movement to the variable frame loop and that assertion drifts per machine.
Concepts¶
Which body type, and why¶
Godot 2D has three movable body archetypes. The choice is not stylistic:
RigidBody2D— fully physics-simulated (forces, mass, bounce). You nudge it; the engine decides where it ends up. Great for crates and ragdolls, wrong for a character you want under precise control — it will drift, spin, and slide in ways a player reads as "floaty."Area2D/ bareNode2D— no collision response at all. The character would walk through walls.CharacterBody2D— built for the case where your code decides the velocity each frame and the engine only resolves collisions and slides along surfaces. This is the standard choice for a player or enemy you steer directly.
Use CharacterBody2D. You will set its velocity and call move_and_slide(); the engine handles the
"stop at the wall, slide along it" part.
_physics_process and delta¶
Godot calls two per-step callbacks:
_process(delta)runs once per rendered frame — variable rate (60, 144, or whatever the monitor and load produce). Right for animation and UI._physics_process(delta)runs at a fixed rate (default 60 Hz) — the samedeltaevery step. Right for movement and anything physics-touching.
Put movement in _physics_process. The delta it passes is the seconds per step; multiplying motion by
delta makes distance-per-second independent of how often the callback fires. move_and_slide already
integrates velocity over the physics step internally, so you scale velocity by speed (a per-second
value) and let move_and_slide apply the step — you do not multiply velocity by delta yourself when
using move_and_slide. (You would multiply by delta if you were moving position by hand.)
Reading direction and applying it¶
The movement loop is three lines of intent:
- Get a normalized direction from the four
move_*actions withInput.get_vector. - Set
velocityto that direction times a speed constant (pixels per second). - Call
move_and_slide().
We build the script incrementally.
Example
Start with the class and a tunable speed. Attach this to the player root (a CharacterBody2D):
extends CharacterBody2D
## Movement speed in pixels per second. A tunable; 120-180 feels right for a top-down ARPG.
@export var speed: float = 150.0
@export surfaces speed in the Inspector, so a designer or tester can tune it without editing
code — the same "one knob, no recompile" idea the genre uses everywhere.
Example
Add the movement step. Input.get_vector returns Vector2.ZERO when nothing is held, so a released
key naturally stops the body (velocity becomes zero):
func _physics_process(_delta: float) -> void:
var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * speed
move_and_slide()
The _delta is prefixed with _ because this body does not use it directly — move_and_slide
consumes the physics step internally. Renaming it _delta documents "intentionally unused" and
silences the editor's unused-parameter hint.
Why the underscore, and why no manual delta¶
A frequent early mistake is velocity = direction * speed * delta and move_and_slide(). That
double-counts the step: move_and_slide already advances by the physics step, so multiplying velocity by
delta first makes the character crawl at 1/60 of the intended speed. Rule: with move_and_slide,
velocity is a per-second value; do not pre-multiply by delta.
The collision shape¶
A CharacterBody2D with no CollisionShape2D has nothing to collide with: it will pass through walls
and, later, nothing can register hits against its body. Add a CollisionShape2D child with a shape
sized to the character (a CapsuleShape2D or RectangleShape2D roughly covering the sprite's feet/body
is typical for top-down). The shape is the body's physical footprint, distinct from the visual sprite
(M2.2) and from the hurtbox you add in M3.
Walkthrough¶
You will build the player scene and walk it around the empty arena from M1.4.
Scene → New Scene → Other Node → CharacterBody2D. Rename the rootPlayer. Save asres://scenes/actor/player.tscn.- With
Playerselected, add childCollisionShape2D. In the Inspector, set its Shape to a newCapsuleShape2D(orRectangleShape2D); size it to roughly cover where the character will stand. - Add a temporary visual so you can see the body move: add a child
Sprite2D, and in the Inspector set its Texture to Godot's built-inicon.svg(or any placeholder). M2.2 replaces this with real animation. - Attach a script to
Player: right-clickPlayer→ Attach Script, save asres://scripts/player.gd. Write it incrementally — theextends/@exportblock, then the_physics_processbody shown above. Do not paste a finished file; type the pieces. - Put the player in the world: open
res://scenes/world/main.tscn, select theWorldnode, and from the FileSystem dock dragplayer.tscnonto it. ThePlayeris now a child ofWorld. - Press
F5. UseWASD(your M1.3 actions) to walk the placeholder around. Release the keys; the body stops. Walk into the window's edge — nothing stops you yet, because there are no walls (M8 adds the arena). The point of this chapter is that the body obeys input at a constant speed.
Optional sanity check
In the Inspector, change Player's speed from 150 to 400 while the game is not running,
relaunch, and confirm the character is faster — proof the @export knob works. Then walk
diagonally and confirm it is not faster than walking straight (proof get_vector normalized). Reset
speed to a value that feels right.
Self-check quiz¶
Q1 — Why does movement go in _physics_process rather than _process?
A. _process cannot read the Input singleton.
B. _physics_process runs at a fixed rate, so movement is frame-rate independent and reproducible
across machines; _process runs at the variable render rate.
C. move_and_slide only exists inside _process.
D. _physics_process is faster.
Reveal answer
B. The fixed step is the whole reason: the same delta every call means "hold right for a
second" covers the same distance on any monitor, and physics interactions resolve consistently.
A is false (Input is global). C is false (move_and_slide is a CharacterBody2D method,
callable from _physics_process, which is exactly where it belongs). D is not the reason — it's
about correctness, not speed.
Q2 — A learner writes velocity = direction * speed * delta and then calls move_and_slide(). The character crawls. Why?
A. speed is too low.
B. move_and_slide already advances by the physics step, so multiplying velocity by delta applies
the step twice — the body moves at roughly 1/60 of the intended speed.
C. direction is not normalized.
D. delta is negative on the first frame.
Reveal answer
B. With move_and_slide, velocity is a per-second value the method integrates over the
step; pre-multiplying by delta double-counts it. The fix is velocity = direction * speed. A
is unlikely to produce a precise crawl. C would affect diagonal speed, not overall crawl. D is
false — delta is the positive fixed step.
Q3 — Why use CharacterBody2D instead of RigidBody2D for the player?
A. RigidBody2D cannot collide with walls.
B. CharacterBody2D lets your code set velocity directly and only asks the engine to resolve
collisions/slide, giving precise control; RigidBody2D is force-simulated and feels floaty for a
directly-steered character.
C. RigidBody2D does not exist in 2D.
D. CharacterBody2D renders sprites and RigidBody2D does not.
Reveal answer
B. The distinction is who decides the motion: with CharacterBody2D your code does, and the
engine only resolves collisions; with RigidBody2D the physics solver does, which drifts and
slides in ways players read as loss of control. A is false (rigid bodies collide). C is false. D
is false (neither renders; a child Sprite2D does).
Integration question¶
Q4 — open
You now have a CharacterBody2D reading the move_* actions from M1.3 and living under the World
node from M1.4. Trace the chain from a W keypress to the character moving up, naming the M1
decision each link depends on. Then explain why the placeholder Sprite2D you added is the wrong
long-term approach and what M2.2 replaces it with.
Reveal expected answer
The chain: pressing W is read as the move_up action (M1.3's InputMap), which get_vector
turns into a Vector2 pointing up (−Y, because M1.1's 2D space has +Y downward); that direction
times speed becomes velocity, and move_and_slide advances the CharacterBody2D at the
fixed _physics_process rate; the body is a child of World (M1.4), so when the camera arrives
in M2.3 the player will scroll with the world rather than being pinned like the UI. The
placeholder Sprite2D with a static icon is wrong long-term because a character needs to show
idle vs running and which way it faces; M2.2 replaces the single static texture with an
AnimatedSprite2D (or AnimationPlayer) driven by the same velocity the movement code produces,
so the visuals reflect the motion rather than ignoring it.