Build a Game from First Principles

Twelve steps. Every one of them runs. At each step you have a working thing that is one idea bigger than the last — that's the whole method, and it's how every game on this site was actually built.

You can follow along in any editor and a browser — no build step, no install. Get the engine three ways; the one-line hot-link import is the fastest start.

Step 1 · A loop and a screen

A game is a fixed heartbeat (50 ticks a second, like the machines this engine remembers) and something to draw on.

import { createLoop, Video, C64 } from './retro64/engine/index.js';
const video = new Video(document.querySelector('#game'));
const loop = createLoop({
  update() {},                                  // nothing thinks yet
  render() { video.clear(C64.BLUE); },          // but the sky exists
});
loop.start();

A blue screen at a steady 50Hz. That's a game engine; everything else is furniture. (Why fixed-tick matters — determinism, replays, feel — is the build log's war story.)

Step 2 · A place

The world is a tilemap: rows of glyphs, and a legend that says what each glyph is. One line in the legend gives a tile solidity, a look, or a behaviour.

import { TileMap, SOLID } from './retro64/engine/index.js';
const map = new TileMap({
  rows: ['........................................', /* …more rows… */
         '########################################'],
  legend: { '#': { flags: SOLID, kind: 'brick', base: C64.RED, top: C64.LIGHT_RED } },
});
// render(): map.draw(video, 0, 0, tick);

Draw it after the clear. You have a floor. (Maps in full — every kind, every flag.)

Step 3 · A body that feels right

The engine's core bet: how moving feels is a list of numbers, not code. Make a Player with a profile — or steal a named one and tune it later.

import { Player, Input } from './retro64/engine/index.js';
const input = new Input().attach(window);
const player = new Player(map, {
  inertia: true, maxSpeed: 1.5, accel: 0.1, friction: 0.09, airControl: 0.7,
  gravity: 0.16, jumpVel: 3.3, variableJump: true, minJumpVel: 1.3, maxFall: 4,
}, { x: 24, y: 150 });
// update(): input.tick(); player.update(input);
// render(): player.draw(video, 0, 0);

Run and jump. Spend real time here — this is where your game's personality lives. The Movement Lab is these numbers on sliders; the player tutorial explains every knob.

Step 4 · A look

The engine already knows what your body is doingplayer.pose names it every tick (idle, walk, run, skid, turn, jump, fall, climb). Hand the drawing to any of the fifty-three roster characters, or author frames as pure JSON:

import { CHARACTERS, poseOf } from './retro64/playground/characters.js';
player.sprite = (v, camX, camY, p) =>
  CHARACTERS.knight.paint(v, Math.round(p.x - camX), Math.round(p.y - camY),
    { facing: p.facing, pose: poseOf(p), tick, poseT: p.poseT, phase: p.stridePhase });

Look and feel are independent choices — a Ghost with wall-jump physics is nobody's business but yours. (Frames-as-JSON, the fallback chains, Dash and Myrtle: the player tutorial.)

Step 5 · Something that minds

A game needs an other. Every engine enemy is three methods — update(player), hits(player), draw() — and the shelf is long: patrollers, chasers, hoppers, flyers, stalkers, turrets, generators that pour stalkers. (The bestiary.)

import { Patroller } from './retro64/engine/index.js';
const mobs = [new Patroller(map, { x: 200, y: 164, min: 160, max: 280, speed: 0.5 })];
// update(): for (const m of mobs) { m.update(player); if (m.hits(player)) player.hurt({ type: 'guardian', source: m }); }

hurt() is a negotiation — the contact event can be cancelled (armour, mercy frames), and with energy in the profile a hit costs a point instead of a life. (Getting hurt.)

Step 6 · Deaths that play

Nothing should vanish silently. The Outcomes layer plays a deterministic, box-scaled departure for any body — puff, burst, pop, zap, ghost, spark:

import { Outcomes } from './retro64/engine/index.js';
const outcomes = new Outcomes();
// when something dies:  outcomes.spawn('burst', mob, { color: mob.color });
// update(): outcomes.update();   render(): outcomes.draw(video, camX, camY);

(Skip ahead: once you adopt a World in step 9, it runs this layer for you — dead mobs play their outcome automatically.)

Step 7 · A fight that is data

Combat is a moveset — a table in your profile. Stances pick the move on the fire press, the engine runs envelopes, cooldowns, hit shapes and committed lunges, and landing on an enemy is the stomp:

profile.attacks = {
  kick:  { stance: 'moving', active: 14, hit: { shape: 'front', reach: 14, oy: 8, oh: 6 },
           effect: { kill: true }, motion: { lunge: 2.2, hop: 1.7, committed: true } },
  stomp: { stance: 'stomp', effect: { kill: true }, motion: { bounce: 2.4 } },
};
player.on('attack-hit', ({ target, effect }) => { /* what a hit MEANS is yours */ });

The Sparring Yard is this step alone, live. Four ready-made fighting presets ship in profiles.js.

Step 8 · A reason

Rules is the bookkeeper: score, clocks, goals. Tell it what happened; it tells you when you've won.

import { Rules } from './retro64/engine/index.js';
const rules = new Rules({ scoring: { collect: 100, kill: 150 },
  targets: [{ id: 'all', type: 'collect-all' }] });
// rules.event('kill'); rules.tick();

A clock whose expiry costs a life (onExpire: 'die') is one field. (Rules in full.)

Step 9 · A world, not a screen

Rooms + exits = a World: flick-screens (Jet Set Willy), stitched scrollers, or a top-down maze — same rooms, one dropdown of difference. The World also runs most of what you hand-wired above: it spawns mobs from data, judges every touch, collects items, opens gates on demands, sweeps the dead through Outcomes, and lands your moveset's strikes.

import { World } from './retro64/engine/index.js';
const world = new World({ rooms: ROOMS, start: 'first', legend: LEGEND });
world.attach(player, { x: 24, y: 150 });
// update(): player.update(input); world.update();   render(): world.draw(…); player.draw(…);
rules.bind(world, player);

Steps 5, 6 and 8 collapse into data the moment this arrives — that's the point of it. (Worlds · scrollers · co-op: world.addPlayer(p2) seats a second hero, and mobs hunt whoever's nearest.)

Step 10 · Dress it

A Backdrop is parallax layers as data; scenery tiles (flags 0) dress rooms without touching collision; dust, emotions and death styles come free with the player. The difference between a demo and a place is this step — budget real time for it.

Step 11 · Prove it

Before anyone else plays it: simulate the promises. Every jump reachable (hold-jump, from the lip, with the real profile), every corridor wide enough for the body, every key outside its gate, every goal completable. Every shipped game on this site has a harness that walks it; the leap arithmetic and the hard-won rules live in the tutorials — and in the source of every rooms.js here.

Step 12 · Ship it

Three roads: keep it a page with the hot-link import; download the engine bundle and go anywhere; or skip code entirely — the Game Designer builds all of the above visually and publishes to the Arcade with one button, remixable by anyone who presses Stop.


The compressed version of this page is every game's source, live in its SOURCE panel. The two-hour guided version with one game built end-to-end is Path of the Fist. This page is the map; those are the territory.