Tutorial 2 · Enemies & Coins
A character in a room isn't a game until there's a reason to move and a reason to be careful. Here's what we're building — grab all three coins, avoid the red guardian:
This is the smallest complete game: a goal, a hazard, and a win state. It builds straight on Tutorial 1.
Entities: the things that aren't tiles
Tiles are the fixed world. Entities are everything that moves or can be picked up. The engine ships two you'll use constantly:
import { Patroller, Item } from '../../engine/entities.js';
An entity is deliberately simple — anything with an update() and a draw(). No framework, no scene graph.
Step 1 — a patrolling guardian
Patroller is a hazard that walks a set path. This one bounces back and forth along the floor between two x-positions:
const guard = new Patroller(map, {
x: 120, y: 62,
axis: 'x', mode: 'range', min: 40, max: 200,
speed: 0.8, color: C64.LIGHT_RED,
});
There are three patrol behaviours: mode: 'range' bounces between min and max; mode: 'edge' walks until it reaches a wall or the end of a platform, then turns (the classic Goomba); and axis: 'y' bobs up and down (a piston). Touching any of them is deadly.
Step 2 — coins to collect
Item is a pickup. Give it a position and a colour:
const coins = [
new Item({ x: 92, y: 12, color: C64.YELLOW }),
new Item({ x: 30, y: 28, color: C64.YELLOW }),
new Item({ x: 196, y: 28, color: C64.YELLOW }),
];
let got = 0;
Step 3 — check collisions in the loop
Both entity types expose a hits(player) test. This is the entire game rule, and it lives right in your update step:
update() {
input.tick();
if (input.pressed('reset')) player.respawn();
player.update(input);
guard.update();
if (guard.hits(player)) player.die(); // touched the guardian → respawn
for (let i = coins.length - 1; i >= 0; i--) {
if (coins[i].hits(player)) { coins.splice(i, 1); got++; } // grabbed a coin
}
if (got >= coins.length && !won) win(); // all collected → victory
}
Walking backwards through the coins array is the safe way to remove items mid-loop — splicing forwards would skip the next element. player.die() bumps the death counter and returns the player to its spawn; player.respawn() (bound to the R key here) does the same without the death.
Step 3 — draw them
Entities draw themselves; you just call them, back-to-front, after the map:
render() {
video.clear(C64.BLUE);
map.draw(video, camera.x, camera.y);
for (const c of coins) c.draw(video, camera.x, camera.y, tick);
guard.draw(video, camera.x, camera.y); // guardian over coins
player.draw(video, camera.x, camera.y); // player on top
}
The tick you pass to c.draw is just a frame counter — the coin uses it to twinkle. Draw order is paint order: whatever you draw last sits on top.
What you've learned
That hits()-in-the-loop pattern is the seed of the whole game-rules system. In a bigger game, "touch a guardian → die" and "touch a coin → collect" become declared rules on the player object rather than hand-written ifs — that's the direction the engine is heading, and you can read the plan in the Build Log. But under the hood it's always this: entities with an update, a draw, and a hits.
Next
Time for the tiles that make a platformer feel retro — belts that carry you, floors that crumble, ladders that climb.