Quick answer: Z-index only controls draw order within the same CanvasLayer. Nodes on different CanvasLayers are sorted by the layer property of the CanvasLayer itself. To fix ordering between layers, adjust the CanvasLayer's layer property instead of the node's z_index.
Here is how to fix z index not working correctly Godot 2D. You set a sprite's z-index to 100, but it still renders behind another sprite with z-index 0. Or your player character disappears behind the tilemap no matter what z-index value you assign. Z-index ordering in Godot 4 has a few rules that are not immediately obvious, and once you understand them, the fix is straightforward.
The Symptom
Nodes are not drawing in the order you expect. A sprite with a high z-index appears behind one with a lower value. Or objects that should layer naturally in a top-down game — characters behind trees, items in front of the ground — are rendering in seemingly random order. Adjusting the z_index property in the inspector does not produce the expected result.
In some cases, the z-index seems to work correctly within one part of the scene tree but fails when comparing nodes in different branches. For example, a UI element on a CanvasLayer ignores the z-index of game-world sprites entirely, or a child node renders at an unexpected depth because its parent also has a z-index set.
You might also encounter a scenario in a top-down RPG where the player character should appear in front of objects above them on screen and behind objects below them, but z-index alone does not achieve this dynamic depth sorting.
What Causes This
There are three common reasons z-index does not behave as expected in Godot 4:
- Relative z-index inheritance — By default,
z_as_relativeistrueon all CanvasItem nodes. This means a node's effective z-index is its own value plus its parent's effective z-index. A child with z-index 10 under a parent with z-index -5 has an effective z-index of 5. This relative stacking can produce unexpected results when nodes in different branches of the scene tree have parents with different z-index values. - CanvasLayer isolation — Z-index only controls draw order within the same CanvasLayer. The default canvas is layer 0. If you place a node inside a CanvasLayer with
layer = 1, it draws above everything on layer 0, regardless of z-index. UI elements on a CanvasLayer will always render above or below game-world nodes based on the layer number, not z-index. - Missing y-sort for depth sorting — For top-down games, z-index alone cannot handle dynamic depth. You need
y_sort_enabledon a parent node, which sorts children by their Y position each frame. Without this, Godot draws children in scene tree order (or by z-index), which does not account for vertical position.
The Fix
Step 1: Check z_as_relative on parent nodes. Select the parent nodes of sprites that are ordering incorrectly. In the inspector under CanvasItem → Ordering, check the value of z_as_relative. If you want a node's z-index to be independent of its parent, set z_as_relative to false.
# Make a node's z-index absolute (not relative to parent)
extends Sprite2D
func _ready():
z_as_relative = false
z_index = 10 # This is now absolute, not parent + 10
Step 2: Verify CanvasLayer assignments. If two nodes are on different CanvasLayers, z-index will not affect their relative ordering. Check the scene tree — any node under a CanvasLayer node is isolated from nodes on other layers. Move nodes to the same layer if you need them to sort by z-index, or adjust the CanvasLayer's layer property to control overall layer ordering.
# CanvasLayer ordering example
# Game world (default canvas, layer 0)
# Player (z_index: 5)
# Enemy (z_index: 5)
# UI Layer (CanvasLayer, layer: 1)
# HealthBar (z_index: 0)
#
# HealthBar always draws above Player/Enemy
# because CanvasLayer.layer = 1 > default layer 0
# z_index is irrelevant across layers
Step 3: Enable y_sort_enabled for top-down depth. For games where objects should sort by vertical position, create a parent Node2D and enable y_sort_enabled. All direct children of this node will be drawn sorted by their Y position, with lower Y values (higher on screen) drawn first and higher Y values (lower on screen) drawn on top.
# Scene tree for a top-down game with y-sorting:
# YSortRoot (Node2D, y_sort_enabled = true)
# Player (Sprite2D)
# Tree1 (Sprite2D)
# Tree2 (Sprite2D)
# NPC (Sprite2D)
extends Node2D
func _ready():
y_sort_enabled = true
# Now children are drawn by Y position.
# A player at y=200 draws in front of a tree at y=150.
# Move the player above the tree (y=100) and they draw behind it.
For y-sorting to work correctly with characters, set the sprite's offset so the sort origin (the node's position) aligns with the character's feet, not the center of the sprite. This ensures depth sorting matches where the character is standing, not where their head is.
# Adjust sprite offset so Y position = feet position
extends CharacterBody2D
@onready var sprite = $Sprite2D
func _ready():
# For a 32x64 character sprite, offset up by half height
sprite.offset = Vector2(0, -32)
Why This Works
Godot's 2D rendering pipeline draws nodes in a specific order: first by CanvasLayer, then by z-index within each layer, then by scene tree order for nodes with the same z-index. Understanding this hierarchy explains why z-index alone sometimes fails.
z_as_relative exists so that you can group related nodes and move them as a unit through the draw stack. A "building" parent with z-index 5 and "window" children with z-index 1 results in windows at effective z-index 6, keeping them logically above the building. But when this inheritance is not what you want, setting z_as_relative = false breaks the chain.
y_sort_enabled replaces the static z-index with a dynamic sort that runs every frame. This is far more efficient and correct than trying to update z-index values manually in _process based on position, which is a common but fragile workaround.
"I was manually setting z_index in _process based on position.y for every character. Switching to y_sort_enabled on the parent node replaced 30 lines of code with one checkbox."
Related Issues
If your sprites are rendering in the correct order but have visual artifacts like black lines at tile edges, see Fix: Godot Sprites Showing Black Lines or Gaps Between Tiles.
For 3D scenes where models appear inside out or invisible, the depth sorting issue is entirely different. See Fix: Godot 3D Models Appearing Inside Out or Invisible.
If your Light2D nodes are not illuminating sprites on the expected layers, the light_mask system interacts with z-ordering. See Fix: Godot Light2D Not Illuminating Sprites.
Same layer. Right parent. Correct sort.