Depth
Walk below a pillar and you’re in front of it. Walk above and the pillar covers you. That’s depth - a 2D trick that makes your top-down world feel solid and three-dimensional. Try it:
THE ILLUSION
Top-down games use a simple rule: things lower on the screen are closer to the viewer. Imagine looking down at a scene from above. A character further north (higher up on screen) is physically further away, so objects in the south overlap them.
When two objects share the same screen space, the one with the lowest foot point (bottom edge) wins - it renders on top because it’s “closer” to the camera.
[pillar top]
[ ] ← hero here: behind the pillar
[hero]→→→[ pillar ]
[ ] ← hero here: in front of the pillar
[foot]
This technique is called Y-sorting (or z-sorting, borrowing the depth axis from 3D graphics).
HOW PIXI SORTS OBJECTS 🖼️
PixiJS draws container children in the order they were added - later additions appear on top. In a static scene this is fine, but a moving player needs its render position to change dynamically.
Two PixiJS features solve this:
zIndex- a number on every display object. Higher = drawn in front.sortableChildren- a flag on containers. Whentrue, PixiJS re-sorts children byzIndexevery frame before drawing.
const world = new PIXI.Container();
world.sortableChildren = true; // enable Z-sorting for all children
// Now zIndex controls who's in front
wallTile.zIndex = 90; // renders behind the player (when player is south of it)
player.sprite.zIndex = 102; // renders in front (player's foot is lower)
Enable sortableChildren once at setup - PixiJS handles the sorting automatically every frame.
THE FOOT POINT RULE 👣
Every object needs a consistent sorting key. We use the foot point - the y coordinate of the bottom edge. Whoever’s feet are lower on screen is drawn on top.
footPoint = y + height
For a tile in row 2 (TILE_SIZE = 30): footPoint = (2 + 1) × 30 = 90
For the player at y=95, height=12: footPoint = 95 + 12 = 107
Since 107 > 90, the player renders in front - they’re south of the tile, so they’re closer.
Tiles get their zIndex set once when the map is built. The player’s zIndex updates every single frame:
// Build map - set tile zIndex once
for (let row = 0; row < map.length; row++) {
for (let col = 0; col < map[row].length; col++) {
if (map[row][col] === 1) {
const tile = new PIXI.Graphics().rect(0, 0, TILE_SIZE, TILE_SIZE).fill(0x8B4513);
tile.x = col * TILE_SIZE;
tile.y = row * TILE_SIZE;
tile.zIndex = (row + 1) * TILE_SIZE; // foot of this tile row
world.addChild(tile);
}
}
}
// Game loop - update player zIndex each frame
function gameLoop() {
// ... movement and collision ...
player.sprite.zIndex = player.y + player.height; // ← the key line
}
DRAWING TALL OBJECTS 🌲
For the effect to be visible, objects need to extend above their foot point. A 30×30 tile that fits perfectly inside its grid cell won’t visually overlap a player in the row above - there’s nothing to overlap.
Draw the tall portion above the tile’s grid position using a negative y offset in the Graphics rectangle:
// A pillar: grid position is row × TILE_SIZE, but it extends 20px above that
const pillar = new PIXI.Graphics()
.rect(5, -20, 20, 50) // x=5 (centered), y=-20 (above cell), 20px wide × 50px tall
.fill(0x8B4513);
pillar.x = col * TILE_SIZE;
pillar.y = row * TILE_SIZE; // grid position (the foot row)
pillar.zIndex = (row + 1) * TILE_SIZE; // foot point at bottom of tile
world.addChild(pillar);
The collision box still covers the tile grid (player can’t walk through it), but the visual stretches up. When the player walks just north of the pillar, the pillar’s upper half overlaps the player’s sprite - the depth effect clicks into place.
WHAT ABOUT GROUND TILES? 🌿
Ground tiles (grass, floor, dirt) should always render behind everything. Set them to zIndex = 0, or skip adding them as sprites entirely and use the canvas background color instead:
// Option A: set zIndex = 0 for ground tiles
groundTile.zIndex = 0;
// Option B: just set the background color in app.init and skip drawing ground tiles
await app.init({ canvas, width: 300, height: 240, backgroundColor: 0x5a8a3a });
Option B is simpler and slightly faster - fewer sprites, same visual result.
What you’ve built:
- ✅ Y-sort depth using PixiJS
zIndexandsortableChildren = true - ✅ Foot-point rule for consistent render ordering
- ✅ Tall objects that extend above their grid cell for visible depth
- ✅ Per-frame player
zIndexupdate for smooth depth transitions
Next up: Point and click to move! Next: Mouse to Move