The Full Mini Engine
Compose loop, input, physics, collision, camera into one playable gem hunt
Chapter goal
Ship a tiny complete game: collect gems, avoid hazards, win when you grab them all. This is the architecture you can expand forever.
How to do it
- 1
One engine shell
Engine owns canvas, state, systems. Each frame: input → physics → collisions → camera → render → UI.
- 2
Game-specific data
Gems and spikes are just entities with tags. On overlap: gems remove + score; spikes reset player.
- 3
Keep extending
From here add tilemaps, audio, particles, enemies AI — same loop. You now own a pure HTML game engine.
Live lab
Solution code updates with sliders. Tinker is your sandbox. Play runs whichever document you launch.
Live settings
Every slider injects into the engine constants for this chapter.
The chapter engine script is prewritten below. Type it line by line — each line explains why it belongs there. Matching a line advances automatically; Enter re-checks if you are stuck.
1(function () {2 "use strict";3 // Single boot — if this document is re-parsed, abandon prior work.4 if (window.__engineBooted) return;5 window.__engineBooted = true;6 let alive = true;7 window.addEventListener("pagehide", function () { alive = false; }, { once: true });8 const raf = window.requestAnimationFrame.bind(window);9 window.requestAnimationFrame = function (cb) {10 return raf(function (ts) {11 if (!alive) return;12 try { cb(ts); } catch (err) { console.error(err); alive = false; }13 });14 };1516const canvas = document.getElementById('c');17const ctx = canvas.getContext('2d');18const W = canvas.width, H = canvas.height;19const hud = document.getElementById('hud');2021const GRAVITY = 2100;22const JUMP = 660;23const MOVE = 250;24const GEM_COUNT = 6;25const LERP = 6;26const WORLD_W = 1800;2728const keys = Object.create(null);29let jumpPressed = false;30addEventListener('keydown', e => {31 if (!keys[e.code] && (e.code==='Space'||e.code==='ArrowUp'||e.code==='KeyW')) jumpPressed = true;32 keys[e.code]=true;33 if (e.code.startsWith('Arrow')||e.code==='Space') e.preventDefault();34});35addEventListener('keyup', e => { keys[e.code]=false; });3637const engine = {38 player: { x: 60, y: 80, w: 26, h: 34, vx: 0, vy: 0, grounded: false },39 camera: { x: 0, y: 0 },40 solids: [{ x: 0, y: H-22, w: WORLD_W, h: 22 }],41 gems: [],42 spikes: [],43 score: 0,44 won: false,45 t: 0,46};4748for (let i = 0; i < 16; i++) {49 engine.solids.push({50 x: 140 + i * 100,51 y: H - 80 - (i % 4) * 42,52 w: 72,53 h: 14,54 });55}56for (let i = 0; i < GEM_COUNT; i++) {57 const s = engine.solids[1 + (i * 2) % (engine.solids.length-1)];58 engine.gems.push({ x: s.x + s.w/2 - 7, y: s.y - 28, w: 14, h: 14, taken: false });59}60for (let i = 0; i < 5; i++) {61 engine.spikes.push({ x: 300 + i * 280, y: H - 22 - 16, w: 28, h: 16 });62}6364function aabb(a,b){return a.x<b.x+b.w && a.x+a.w>b.x && a.y<b.y+b.h && a.y+a.h>b.y;}65function resolve(p,s){66 const ox=Math.min(p.x+p.w-s.x,s.x+s.w-p.x);67 const oy=Math.min(p.y+p.h-s.y,s.y+s.h-p.y);68 if(ox<oy){ if(p.x+p.w/2<s.x+s.w/2)p.x-=ox; else p.x+=ox; p.vx=0; }69 else { if(p.y+p.h/2<s.y+s.h/2){ p.y-=oy; p.vy=0; p.grounded=true; } else { p.y+=oy; p.vy=0; } }70}71function resetPlayer(){72 engine.player.x=60; engine.player.y=80; engine.player.vx=0; engine.player.vy=0;73}7475let last = performance.now();76function loop(now){77 const dt = Math.min((now-last)/1000, 0.05); last = now;78 engine.t += dt;79 const p = engine.player;8081 if (!engine.won) {82 let input = 0;83 if (keys['ArrowLeft']||keys['KeyA']) input -= 1;84 if (keys['ArrowRight']||keys['KeyD']) input += 1;85 p.vx = input * MOVE;86 if (jumpPressed && p.grounded) { p.vy = -JUMP; p.grounded = false; }87 jumpPressed = false;88 p.vy += GRAVITY * dt;89 p.grounded = false;90 p.x += p.vx * dt;91 for (const s of engine.solids) if (aabb(p,s)) resolve(p,s);92 p.y += p.vy * dt;93 for (const s of engine.solids) if (aabb(p,s)) resolve(p,s);9495 for (const g of engine.gems) {96 if (!g.taken && aabb(p, g)) { g.taken = true; engine.score++; }97 }98 for (const sp of engine.spikes) if (aabb(p, sp)) resetPlayer();99 if (p.y > H + 80) resetPlayer();100 if (engine.score >= GEM_COUNT) engine.won = true;101 }102103 const targetX = p.x + p.w/2 - W/2;104 engine.camera.x += (targetX - engine.camera.x) * Math.min(1, LERP * dt);105 engine.camera.x = Math.max(0, Math.min(WORLD_W - W, engine.camera.x));106107 // render108 ctx.fillStyle = '#0b1018'; ctx.fillRect(0,0,W,H);109 ctx.fillStyle = 'rgba(125,211,252,0.08)';110 for (let i=0;i<12;i++){111 const x = ((i*160 - engine.camera.x * 0.25) % (W+40) + W+40) % (W+40) - 20;112 ctx.beginPath(); ctx.arc(x, 50 + (i%3)*30, 18, 0, Math.PI*2); ctx.fill();113 }114115 ctx.save();116 ctx.translate(-engine.camera.x, 0);117118 ctx.fillStyle = '#1e2a3a';119 for (const s of engine.solids) ctx.fillRect(s.x,s.y,s.w,s.h);120121 for (const sp of engine.spikes) {122 ctx.fillStyle = '#f87171';123 ctx.beginPath();124 ctx.moveTo(sp.x, sp.y+sp.h);125 ctx.lineTo(sp.x+sp.w/2, sp.y);126 ctx.lineTo(sp.x+sp.w, sp.y+sp.h);127 ctx.closePath(); ctx.fill();128 }129130 for (const g of engine.gems) {131 if (g.taken) continue;132 const bob = Math.sin(engine.t * 4 + g.x) * 3;133 ctx.fillStyle = '#fbbf24';134 ctx.save();135 ctx.translate(g.x + 7, g.y + 7 + bob);136 ctx.rotate(Math.PI/4);137 ctx.fillRect(-6, -6, 12, 12);138 ctx.restore();139 }140141 ctx.fillStyle = engine.won ? '#34d399' : '#5eead4';142 ctx.fillRect(p.x, p.y, p.w, p.h);143 ctx.restore();144145 // UI146 ctx.fillStyle = 'rgba(7,9,13,0.55)';147 ctx.fillRect(12, 10, 150, 28);148 ctx.fillStyle = '#e8edf5';149 ctx.font = '12px ui-monospace,monospace';150 ctx.textAlign = 'left';151 ctx.fillText('Gems ' + engine.score + ' / ' + GEM_COUNT, 22, 28);152153 if (engine.won) {154 ctx.fillStyle = 'rgba(7,9,13,0.72)';155 ctx.fillRect(0,0,W,H);156 ctx.fillStyle = '#5eead4';157 ctx.font = '600 28px system-ui,sans-serif';158 ctx.textAlign = 'center';159 ctx.fillText('Engine Complete', W/2, H/2 - 8);160 ctx.fillStyle = '#8b97ab';161 ctx.font = '13px system-ui,sans-serif';162 ctx.fillText('You built a pure HTML game engine.', W/2, H/2 + 20);163 }164165 hud.textContent = 'Chapter 9 · mini engine · ' + (engine.won ? 'YOU WIN' : 'hunt gems');166 requestAnimationFrame(loop);167}168requestAnimationFrame(loop);169170})();
Collect all gems. Arrows/WASD + Space/W to jump. Spikes reset you. · Sliders update the play window after a short pause.