Skip to content

Articles

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 course

A 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 Sprite2D up by half its height.
  • Enable y_sort_enabled on the scene root that holds player, props and enemies — and on any nested container that holds several of them.
  • Never fake it with z_index per object. Two objects at the same z_index are 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.0

Keep 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 = false

Where 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.