Godot 2D top-down game: the systems, in order
Build a 2D top-down game in Godot 4 in the order the systems depend on each other: character, depth sorting, tile collisions, camera, animation, then interaction.
The full 2D top-down crash courseA top-down game is not one system; it is six that only work in a particular order. Build them out of order and each one fights the last: the camera jitters because depth sorting moves the draw order, the attack misses because the hitbox is parented to the sprite instead of the body, the character snags on corners because the collision shape is a rectangle where the tile collision is a polygon.
Godot 4, CharacterBody2D and TileMapLayer. The order below is the one that stops the
rewrites. (The video's map chapters use TileMap; 4.3 splits that into one TileMapLayer per
layer. Everything else — body, sorting, camera, animation — is unchanged.)
1. The body before the picture
CharacterBody2D with one CollisionShape2D, and the sprite as a child. The body owns
position and collision; the sprite is decoration. Every mistake later — hitboxes in the wrong
place, enemies pushed by the player, the character clipping corners — comes from inverting that.
extends CharacterBody2D
@export var speed: float = 110.0
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()Input.get_vector already normalises diagonals, so a diagonal is not 41 % faster than a
straight line. If you write Vector2(Input.get_axis(...), Input.get_axis(...)), it is.
2. Depth before decoration
Top-down depth is a draw-order problem, and Godot solves it with y_sort_enabled on the
container plus the node origin at the character's feet. A 32×48 sprite anchored at its centre
sorts by its middle, so the character walks behind a table it is standing in front of.
- Put the sprite so the body's origin is at the feet: offset the
Sprite2Dup by half its height. - Enable
y_sort_enabledon the scene root that holds player, props and enemies — and on any nested container that holds several of them. - Never fake it with
z_indexper object. Two objects at the samez_indexare sorted by tree order, which is stable and wrong the moment something moves.
3. Tile collisions before the camera
A tile with a texture and no collision is scenery. In the TileSet editor, give the physics
layer a collision polygon per tile — the shape the player actually collides with, not the
sprite rectangle. A diagonal wall needs a triangle, or the player slides along an invisible box.
Then the layer itself: TileMapLayer with collision_enabled = true, and the player on a
CharacterBody2D using move_and_slide(). move_and_slide() handles the slide; you do not
need to project velocity yourself, and doing both produces stutter against corners.
4. Camera after the world
Camera2D with limits set to the map rectangle and position_smoothing_enabled on. Limits
matter more than smoothing: without them the camera shows the void past the map edge, and no
amount of smoothing hides it.
@onready var camera: Camera2D = $Camera2D
func _ready() -> void:
camera.limit_left = 0
camera.limit_top = 0
camera.limit_right = 64 * 40 # map width in cells
camera.limit_bottom = 64 * 30
camera.position_smoothing_enabled = true
camera.position_smoothing_speed = 8.0Keep the camera as a sibling of the player, not a child, and set its global_position from the
player each frame. A child camera inherits the player's collision jitter; a sibling does not.
5. Animation before interaction
Once movement and collision are stable, the animation has something honest to describe. An
AnimationTree with a state machine and a BlendSpace2D per locomotion state is the shape
that scales to eight directions; the conditions and blend positions are covered in
AnimationTree conditions and blend spaces.
The one rule that saves a week: drive the tree from the same direction and velocity values
that move_and_slide() used. A second, independent "is moving" flag drifts, and the character
slides while playing the idle animation.
6. Interaction and combat last
Interaction is Area2D, and it belongs on the body — not on the sprite, which flips. Two areas,
not one:
| Area | Monitors | Used for |
|---|---|---|
| Hurtbox | enemy hitboxes | taking damage |
| Hitbox | enemy hurtboxes | dealing damage, enabled only during active frames |
Toggle monitoring on the hitbox during the attack's active window instead of leaving it on.
A permanently-on hitbox hits twice on one swing, and the second hit is the bug report you get.
func _on_attack_active() -> void:
$Hitbox.monitoring = true
func _on_attack_recovery() -> void:
$Hitbox.monitoring = falseWhere this goes next
The movement and collision layers above are the base every build system sits on. If the game places furniture, walls or rooms, the grid overlay and the commit step are in Godot 2D grid building; if it only snaps props to tiles, snapping to a TileMap is the shorter path.
Common mistakes
- Sprite as the body. The sprite flips and scales; collision and hitboxes must not.
- Origin at the sprite centre. Y-sorting then compares the wrong point and the character walks through tables.
- Rectangle collision on diagonal tiles. The player stops in mid-air next to a slope.
- Camera as a child of the player. Inherits physics jitter, and smoothing cannot remove it.
- A permanently enabled hitbox. One swing, two hits, and a damage number nobody can explain.