Isometric Scroll
A 5Γ5 isometric map is a proof of concept. A real game needs room to breathe. Combine the World Container from the scrolling tutorials with the isometric transform and you get a large diamond world that follows the player:
Notice the diamond shape visible against the dark background at the edges. That’s the one catch with isometric scroll β the viewport is rectangular but the world is diamond-shaped.
WORLD CONTAINER IN ISO π
The World Container pattern from tutorials 17β18 carries over directly. The only change from tutorials 21β22 is dropping the static OFFSET_X/OFFSET_Y β the container’s own position handles the centering instead:
// Tutorials 21 & 22: static offset baked into isoToScreen()
function isoToScreen(worldX, worldY) {
return {
x: (worldX - worldY) + OFFSET_X, // β fixed center baked in
y: (worldX + worldY) / 2 + OFFSET_Y
};
}
// Tutorial 23: no static offset - the container moves instead
function isoToScreen(worldX, worldY) {
return {
x: worldX - worldY, // β raw iso coords only
y: (worldX + worldY) / 2
};
}
Build the whole isometric world inside a Container. Every tile and the player sprite use the offset-free isoToScreen(). The container shifts the entire scene each frame:
const world = new PIXI.Container();
world.sortableChildren = true;
app.stage.addChild(world);
THE CAMERA TARGET π·
The camera tracks the player’s isometric screen position inside the container β not their world coordinates. Convert worldX/worldY to iso coords first, then smooth-follow that:
// Initialise camera at the player's starting position
const startScreen = isoToScreen(player.worldX, player.worldY);
let camX = startScreen.x;
let camY = startScreen.y;
const SMOOTH = 0.1;
function gameLoop() {
// ... movement and collision in world space ...
// Player's iso position inside the container
const screen = isoToScreen(player.worldX, player.worldY);
player.sprite.x = screen.x - halfSpriteW;
player.sprite.y = screen.y - halfSpriteH;
// Ease the camera toward the player's iso position
camX += (screen.x - camX) * SMOOTH;
camY += (screen.y - camY) * SMOOTH;
// Shift the container to keep the camera target centred on screen
world.x = Math.round(SCREEN_W / 2 - camX);
world.y = Math.round(SCREEN_H / 2 - camY);
}
This is identical to the updateCamera() from tutorial 17 β just targeting screen.x/screen.y from the iso transform instead of player.x/player.y directly.
THE DIAMOND SHAPE PROBLEM π·
Here is the one genuine challenge with isometric scroll: the map is diamond-shaped, but the viewport is rectangular. The background shows through the corners as the player moves:
ββββββββββββββββββββββββββ
β [background] β
β /βΎβΎβΎβΎβΎβΎβΎ\ β
β / tiles \ β
β / world \ β
β \ / β
β \ / β
β \_______/ β
β [background] β
ββββββββββββββββββββββββββ
Three approaches, in order of complexity:
Option 1 β Embrace the diamond. Use a dark or thematic background colour. The diamond edges look intentional. Works great for space games, cave maps, or any dark-aesthetic game. This is what the demo above does.
Option 2 β Extra border tiles. Extend the map with enough extra rows and columns so the diamond fully covers the rectangle at all camera positions. Simple, but you’re building and sorting tiles the player will never see.
Option 3 β Clip with a PixiJS mask. Create a rectangle Graphics object and assign it as world.mask. PixiJS only renders what’s inside the mask β the diamond corners vanish cleanly:
const viewMask = new PIXI.Graphics()
.rect(0, 0, SCREEN_W, SCREEN_H)
.fill(0xffffff);
app.stage.addChild(viewMask);
world.mask = viewMask; // container only renders within this rectangle
The mask moves with the canvas (not the world container), so it stays fixed at the viewport bounds regardless of camera movement. This is the cleanest solution for a polished shipped game.
DEPTH SORTING IN A CONTAINER βοΈ
sortableChildren belongs on the container, not the stage. All depth relationships between tiles and the player stay correct as the camera scrolls:
const world = new PIXI.Container();
world.sortableChildren = true; // β on the container
// Tiles - set once when building the map
tile.zIndex = worldX + worldY;
// Player - updated every frame
player.sprite.zIndex = player.worldX + player.worldY + TILE_SIZE / 2;
The + TILE_SIZE / 2 offset ensures the player always renders in front of the floor tile they’re standing on, even when their worldX + worldY value equals a tile’s exactly.
What you’ve built:
- β Isometric world in a World Container β raw iso coords, container handles the centering
- β Camera target derived from the player’s iso screen position, not world position
- β Smooth camera easing identical to the orthographic scrolling tutorials
- β Three options for the diamond viewport problem: embrace it, fill it, or mask it
Next up: Give your hero a sense of direction. Next: Rotate Hero