Tutorial 7 · Entities

This page lists every ready-made moving thing in the engine — and shows that inventing your own takes about twenty lines.

The design is deliberately not a framework: an entity is anything with an update() and a draw(), plus a hits(player) if touching it means something. No scene graph, no base class. Here's the full cast in one hostile room:

The cast

Patroller — the JSW guardian. Three behaviours: axis:'x', mode:'edge' walks until a wall or platform edge and turns (the classic); mode:'range' bounces between min..max pixels; axis:'y' bobs vertically (pistons, zappers). Deadly to touch.

Chaser — walks toward the player, with gravity, so it follows floors. careful: true (default) stops at ledges; false is a reckless pursuer that leaps after you — into lava, if that's where you went. The Bruce Lee ninja, essentially.

Turret — fires on a timer (every ticks, warmup offsets the first shot), either a fixed dir or aim: true to lead at the player. update(target) returns a new Projectile on the tick it fires — your game pushes that into its own list. The housing itself is deadly, and it glows as it reloads.

Projectile — a straight-line bolt (give it gravity for lobbed shots). It dies on life running out, or when you call kill(reason); hitsTile(map) tells you it hit a wall. Your update loop owns projectiles — see the Danger Room's for the canonical dozen lines.

Item — the reason to cross the room. points overrides the score table for this one item (the golden trinket); tag lets targets and gates count kinds (3 × 'key').

Gate — a barrier that stays shut until its demands are met, then rises like a portcullis. It works by stamping SOLID tiles over its rectangle — so the player and every tile-reading enemy treat it as wall, with zero collision code — remembering what each cell held and restoring it as it rises. requires: { tag, count } counts collected items; goal: 'id' names a Rules target. Judging the demands is the World's job.

Exit — the level ender. A doorway that wins the run the moment the player steps through it — if it lets them: it makes demands in the gate's exact vocabulary (requires: { tag, count }, goal: 'target-id') and stays barred, blinking its lock, until they're met. A world containing an exit completes only at the exit — collect-all becomes a demand you hang on the door; a world without one completes on the last collectible, as ever. The World announces exit-open, exit-denied (once per approach — your "NEED MORE KEYS" flash) and complete when stepped through.

Hopper — the bounding school: it travels in leaps, resting between them, squashing as it lands and stretching as it flies. With chase: true every hop re-aims at the player (the Green Yamo's whole career); with chase: false it's a spring-loaded patroller, turning at walls.

Bouncer — a ball that never loses its bounce: it leaves the floor at exactly its bounce speed forever, ricochets off walls, drops off ceilings. Pang's balloons, in one entity.

Piston — the hydraulic press. Anchored to a surface, it waits, slams out to full length, holds, and withdraws — on a strict cycle you can read and learn. That reading is the game. Aim it down from a ceiling, up from a floor, or sideways from a wall; the whole shaft is deadly while it's out.

Crusher — the piston's terrible marriage: one jaw seated on the floor, one hung from the ceiling, coming together in the middle on their shared cycle — wait, slam, hold, part. You pass through the mouth between bites; the safe ground is the timing. The seam flashes when they meet, so the bite reads from across the room.

Vent — steam on a rhythm. The nozzle is always visible (fair warning); on its cycle it hisses a scalding jet for a while, then rests. Deadly only while hissing, and the billow is always drawn — nobody dies to invisible steam on this engine.

Boulder — the Boulder Dash school of entity gravity. It rests on solid ground; dig or crumble the ground away and it falls, deadly to whatever it lands on — but only while falling, a rested rock is furniture. Rested boulders also roll: side and diagonal both clear, and it slides off sideways and falls again, which is what makes a cave cascade. roll: false for rocks that stay put.

Each entity tells you its moments

Every entity lets you attach one small function to each of its interesting moments — "it turned around", "it fired", "it landed" — either when you create it or later:

Patroller / Chaser   onTurn(dir, self)          it changed direction
Turret               onFire(projectile, self)   the muzzle-flash moment
Projectile           onDeath(reason, self)      'expired' | 'tile' | 'hit' | yours
Gate                 onOpen(self)               fully risen, way clear
Boulder              onFall(self)               the ground went — it's loose
                     onLand(impact, self)       thudded down (impact = fall speed)

The Danger Room's muzzle flashes and impact bursts are nothing but these.

Writing your own

The whole contract, as the World sees it:

class Guard {
  update(player) { /* move; read the map via map.flagsAt(cx, cy) */ }
  hits(player)   { /* AABB overlap → touching me is deadly */ }
  draw(video, camX, camY, tick) { /* rects in palette colours */ }
}

const world = new World({
  rooms, start, legend,
  spawnEntity: (def, map) => def.type === 'guard' ? new Guard(map, def) : null,
});

That Guard is real — it's the twenty-line homing soldier from the overhead example. Chan Can Kick goes further: its ninjas are a five-state machine (chase, windup, strike, recover, down) and both they and the Green Yamo carry a game-side knockdown() method, wired to the player's attack-hit event — the engine lands the hit (the moveset), the game decides it means floored. Extra state and extra methods on your entities are yours.

Six newer natives worth knowing. The Pendulum swings a bladed bob on a rope (only the bob bites — cross on the beat; its sway is pure arithmetic, lockstep-safe by design), the Dripper forms a visible drop before it falls (the forming IS the warning), and the Dart rests, warns with chevrons, then crosses its row at speed — Manic Miner's arrow, faithfully. The Generator is Gauntlet's defining mechanic as one entity: it pours Stalker hunters (top-down homing walkers) into the world every few ticks up to a cap, takes several hits through fell(), and its brood joins world.mobs directly — so attacks, shoves and shots treat them like anyone else. The Fan is machinery that moves you instead of hurting you: point it up under a gap and force beyond gravity makes it an updraft (Sisters of the Crown's Static Storm crosses an unjumpable spike field on two of them); duty-cycle it with every/on like a vent. And when any mob dies inside a World, its departure plays — the Outcomes layer (engine/outcomes.js) runs a box-scaled, deterministic death animation (mob.outcome names the style; 'puff' by default). Nothing vanishes silently any more.

And then the bestiary of yore arrived — nine more natives, every one lifted from the machines that raised us and every one obeying the house law: a telegraph before every bite. The Sporecap mushroom swells and brightens (leave), then bursts a fountain of drifting spores that cannot be killed — no fell(), nothing to hit — only avoided; the cap itself is rooted and harmless ({ dir: 'down' } hangs one from a ceiling, gills first). The Snapper is the plant in the pit: leaf-tips rustle, then it rises to bite — and it politely refuses to erupt while you stand on its hole (fair's fair). The Wraith ignores walls, floors and gravity and simply comes, undulating through the architecture at the nearest player. The Bat hangs asleep — pure decor — until you walk beneath: a two-beat shiver, then a dive, and it never sleeps again. The Riser lies buried and unhittable (its box is zero while it's soil); the earth churns, a body hauls out and shambles, roams, reburies. The Spider flurries its legs, drops on a silk thread, dangles, reels home — the thread is theatre, only the body bites. The Mimic draws as any collectible through the real Item shape table but never twinkles — the twinkle rule doing real design work — and the first touch springs it rather than kills you. The Crawler edge-follows the architecture itself — floor, wall, ceiling, corners inner and outer — spikes out on every side. And the Boulder (Boulder Dash's own) now spawns from a World def too: deadly falling, furniture at rest, and anchored — a rock is beneath argument. All nine are one JSON def away in any World, and all nine live in the Game Designer's palette with their dials.

One flag the engine does read: anchored. The machines — pistons, crushers, vents, turrets — set it, and it means player attacks pass over them entirely: no kick fells a piston, no roar slides a turret down the corridor. Set it on any mob of yours that's bolted to the architecture. (The living unkillables set it too: the sporecap, the snapper, the designed boulder — you time those, you don't fight them.)

One more contract, engine-wide now: a custom spawnEntity factory only has to know its own creatures — anything it answers null for falls back to the World's built-in factory, so a game factory adds to the roster, never replaces it.

Two conventions worth copying from the built-ins: a pixel of forgiveness on deadly hit-boxes (deaths should feel earned), and reading the world only through map.flagsAt so your entity works on any map ever drawn.

What you've learned

Twelve entities cover the C64 canon; everything else is twenty lines away. Callbacks fire at the moments effects want; hits() in a loop — or a World doing it for you — is the entire combat system.

Tutorial 8 · Camera, Video & the Loop →