Tutorial 5 · Maps & Tiles

This page explains maps and tiles completely — the floor everything else stands on. Come back to it whenever you wonder what a tile can do.

A Retro-64 room is a character grid: 8×8 tiles, 40×25 to a screen — the C64's native character resolution. Rows are strings, and a legend maps each character to behaviour and looks. You've been using this since Tutorial 1; here's all of it.

The eight things a tile can be

Every tile behaviour is a simple on/off switch (the code calls them flags), and one tile can switch on several at once:

export const SOLID    = 1;    // blocks movement from every side
export const PLATFORM = 2;    // blocks only when landing from above
export const HAZARD   = 4;    // kills on contact
export const CONVEYOR = 8;    // drags whatever stands on it  (tile.dir ±1, tile.beltSpeed)
export const CRUMBLE  = 16;   // decays while stood on        (tile.hp ticks), then gives way
export const LADDER   = 32;   // climbable while overlapping
export const ICE      = 64;   // solid but low-grip — you slide (tile.grip 0..1)
export const BOUNCE   = 128;  // solid but launches on landing (tile.bounceVel)

Mixing them is the point — that | symbol just means and also. A conveyor is SOLID | CONVEYOR: something you can stand on that also drags you. Ice is SOLID | ICE. Nothing stops you inventing HAZARD | CONVEYOR — a belt of blades — because the player only ever asks a tile "what are you switched on as?", never "what's your name?".

Anatomy of a legend entry

const LEGEND = {
  '>': {
    flags: SOLID | CONVEYOR,  // what it DOES  — read by player and entities
    kind: 'conveyor',         // how it DRAWS  — picks the renderer case
    dir: 1, beltSpeed: 0.5,   // behaviour parameters (per-flag, see above)
    base: C64.GREY,           // body colour   (palette index)
    top: C64.LIGHT_GREY,      // top-edge/detail colour
  },
};

kind selects a hand-pixelled drawing routine: brick, platform, spike, lava (animated bubbles), ice, bounce, shroom (the trampoline mushroom), conveyor (the chevrons crawl in the belt's direction — the animation is the signage), crumble (cracks appear as it wears), ladder, gate — plus a shelf of pure-scenery kinds (torch, banner, grass, flower, bush, tombstone, pillar, window, fence, mushroom, skull, chain) that draw but never collide: give them flags: 0 and dress your world. An unknown kind falls back to a plain filled square, so a new tile is playable before it's pretty.

Building the map

const map = new TileMap({
  rows,               // string[] — every row the same length, or it throws
  legend,
  tileSize: 8,        // the default; the editor uses 8 too
  openSides: false,   // see below
});

Useful properties and methods:

  • map.pixelWidth / map.pixelHeight — size in pixels (w × tileSize).
  • map.tileAt(cx, cy) → the legend entry (or null for air). map.flagsAt(cx, cy) → just the bits, 0 for air. These two are the whole collision API — the player, every entity, and your own code all read the world through them.
  • map.draw(video, camX, camY, tick) — draws only the tiles the camera can see; tick drives the animated kinds.

Edges: openSides and what lies beyond

What's off the map? The rules are asymmetric, and deliberately so:

  • Top and bottom are always open — sky above (you can jump out of frame), and the void below (falling out kills, via the player's 'void' death).
  • The sides depend on openSides. false (default): off the sides is solid wall — right for a self-contained, single-room game. true: the sides are open air, so a World can catch you half-way out and flick to the next room. The World sets this itself; you only think about it for single rooms.

Crumble: the one tile with memory

Crumbling floor is the only stateful tile. Each tick something stands on it, the player controller calls:

map.weaken(cx, cy);          // default amount 1; hp runs out → the cell becomes air
map.wearStageAt(cx, cy);     // 0 fresh · 1 cracking · 2 about to go — drives the cracks
map.onCrumble = (cx, cy, tile) => { /* dust puff here */ };

The state lives on the TileMap instance — build a fresh TileMap (or re-enter a World room) and the floor regrows. That's the Manor's crumbling bridge: no reset code, just reconstruction.

What you've learned

A map is strings + a legend; a tile is a bitmask + a drawing kind + a few parameters. Everything that reads the world does it through flagsAt, which is why new tile types need no changes anywhere else.

Tutorial 6 · The Player & Profiles →