Tutorial 10 · Scrollers — Stitching Rooms

You build a scrolling level the same way you build a room-by-room world (tutorial 4): keep adding rooms, and connect them. The difference is one function call — instead of the screen flipping at each doorway, the rooms are sewn together into one long level. Here's the destination, a scrolling shoot 'em up in three stitched rooms:

One call: stitchRooms

engine/stitch.js takes the exact room structure a World eats, walks the exits graph from a start room, lays every reachable room out on a lattice (right exit → one cell east, down exit → one cell south), and merges them into one big map:

import { stitchRooms } from '../engine/stitch.js';

const level = stitchRooms(ROOMS, 's1');
// level.rows      one big tile grid — 120×25 in the demo above
// level.entities  every room's entities, positions (and patrol ranges,
//                 and gate rectangles) offset into level space
// level.origins   roomKey → pixel offset, for placing your spawn
// level.skipped   rooms the exits graph never reached

Then play it as a single-room world — items, mobs, gates, deaths and the win all keep working; there's simply nothing left to flick to:

const world = new World({
  rooms: { run: { name: 'THE RUN', rows: level.rows, exits: {}, entities: level.entities } },
  start: 'run', legend: LEGEND, spawnEntity,
});

The rules are strict so a broken level fails loudly instead of playing wrong: stitched rooms must share one size, a spatially impossible exits graph ("non-Euclidean — it can flick but it cannot scroll") throws with the room names, and unreachable rooms are listed, not silently dropped. Vertical chains work identically — stitch rooms up for a tower.

The camera decides the genre

With the level stitched, a plain camera.follow(...) gives you Turrican — free scrolling, done (Tutorial 8 covers the clamp). A forced scroll gives you R-Type, and it's three moves in your update:

scrollX = Math.min(scrollX + SPEED, worldWidth - video.width);  // the window advances
camera.x = Math.round(scrollX);
player.x = Math.max(camera.x + 2,                                // the player is PINNED
  Math.min(player.x, camera.x + video.width - player.w - 2));    // inside the window
if (solidAt(player)) player.hurt({ type: 'crush' });             // walls don't negotiate

That last line is the genre's whole personality: the window can shove you into an asteroid, and that's a crush — routed through hurt(), so it fires contact like any other death and a shield could even cancel it. On respawn, re-arm scrollX near the player (the demo does it in a room-enter listener) so a death doesn't rewind the run to the title.

The demo's ship is nothing new: a movement: 'topdown' profile (no gravity — Tutorial 6), a custom player.sprite, and since fire isn't a jump on the floor plane, holding it shoots Projectiles. The golden core at the end is the level's only true collectible — the pods are bonus items (Tutorial 7), so grabbing the core is the win condition, no extra code.

Ending a level

Scrollers want a destination, and the engine has a first-class one: the Exit entity — a doorway at the end of the run that wins the level when stepped through, optionally barred until a demand is met (requires: { count: 9 }, or a Rules target by id). Put it at the far end of a stitched level and the forced window literally delivers you to your ending; the stitcher offsets it into level space like any other entity. The World announces exit-open, exit-denied and complete — details in Tutorial 7.

The other shmup: don't scroll at all

Fixed-screen shooters — Space Invaders, Galaxian — never move the camera: the playfield floats and the enemies come to you. The editor's Shmup — fixed screen, waves attack view does exactly that with rooms as the authoring unit turned sideways: the start room is the arena, and every other room is a wave — its enemy layout is the formation, flying in from the right in tab order, with fire as your trigger. Clear every wave and the run is won.

Each wave-room's panel is a formation designer: pick its entry patternMarch (straight, mobs keep their own motion, so bobbing patrollers bob), Sine wave, Zigzag, Swoop (dive in, level off) or Dive (it hunts your altitude) — plus an amplitude and a per-wave speed override. The formation flies as one body; the pattern owns its altitude on the way in. Same rooms, third genre.

No code at all

The Map Editor does all of this from a dropdown: World → Play as offers Flick screens, Scroller — camera follows, Scroller — forced → and Scroller — forced ↑ (with a speed dial). Same rooms, same exits — the mode travels in the export JSON.

What you've learned

Rooms are the authoring unit for every world shape: flick them, stitch them, or stitch them and take the camera away. The engine's parts didn't change — a scroller is a big TileMap, a shmup is a top-down profile plus a camera with somewhere to be.