Tutorial 1 · Hello, Platform
Here's the finished article — run it. Arrow keys or A/D to move, Z / Space / ↑ to jump. Everything below builds exactly this.
Thirty lines of code produced that. Let's build it piece by piece.
The six modules
The engine is a folder of plain ES modules. This game imports six of them:
import { createLoop } from '../../engine/loop.js'; // the 50Hz heartbeat
import { Video } from '../../engine/video.js'; // the 320×200 screen
import { Input } from '../../engine/input.js'; // the joystick
import { Camera } from '../../engine/camera.js'; // follows the player
import { Player } from '../../engine/player.js'; // the character
import { TileMap, SOLID, PLATFORM } from '../../engine/tilemap.js';
import { C64 } from '../../engine/palette.js'; // the 16 colours
No npm, no bundler. The browser loads them directly.
Step 1 — describe the room
A room is an array of strings plus a legend that says what each character means:
const map = new TileMap({
rows: [
'..............................',
'................=====.........',
'.......=====..................',
'....................===.......',
'##############################',
],
legend: {
'#': { flags: SOLID, kind: 'brick', base: C64.RED, top: C64.LIGHT_RED },
'=': { flags: PLATFORM, kind: 'platform', base: C64.YELLOW, top: C64.WHITE },
},
tileSize: 8,
});
. is empty air. # is solid brick — it blocks you from every side. = is a platform — you can jump up through it but you land on top. Those behaviours come entirely from the flags field; the engine reads them, you never write collision code.
Step 2 — describe the character
A player is a physics profile — ten numbers that define how it feels to move — plus a spawn point:
const player = new Player(map, {
inertia: true, // momentum: speed builds up and bleeds away
maxSpeed: 1.6, accel: 0.12, friction: 0.09, airControl: 0.7,
gravity: 0.14, jumpVel: 3.2, minJumpVel: 1.2, variableJump: true,
fixedJump: false, maxFall: 4, color: C64.YELLOW,
}, { x: 24, y: 78 });
Every value is in pixels per tick (the engine runs at a fixed 50 frames a second, like a PAL C64). Want a floatier jump? Lower gravity. Want instant, momentum-free movement like Jet Set Willy? Set inertia: false. The Movement Lab lets you feel each number on a slider.
Step 3 — the screen, joystick and camera
const video = new Video(stage, { border: C64.LIGHT_BLUE });
const input = new Input().attach(window);
const camera = new Camera(video.width, video.height, map.pixelWidth, map.pixelHeight);
Video makes the 320×200 canvas (integer-scaled, pixel-perfect, with the C64's light-blue border). Input is a one-button joystick — four directions and jump, keyboard-mapped. Camera will keep the player in view and stop at the room's edges.
Step 4 — the loop
This is the heart of it. update() runs 50 times a second in fixed steps; render() draws once per animation frame:
createLoop({
update() {
input.tick(); // sample the joystick
player.update(input); // move & collide
camera.follow(player.x + player.w/2, player.y + player.h/2);
},
render() {
video.clear(C64.BLUE); // VIC-II blue screen
map.draw(video, camera.x, camera.y); // the room
player.draw(video, camera.x, camera.y); // the character
},
}).start();
That's the whole game. player.update(input) does all the hard work — acceleration, gravity, jumping, and tile collision against the map — driven by the profile you handed it.
The one rule to remember
The update step is a fixed 50Hz. That's why every physics number is "per tick" and why a jump looks identical on a slow laptop and a 144Hz monitor — the simulation never speeds up or slows down, only the drawing does. Get comfortable with that and everything else in the engine follows.
Next
You have a character in a room. Now let's give the room a point — something to collect and something to avoid.