Let's Build a Game Engine in HTML
Chapter 3
3 / 9

Entities & Drawing

Data first — objects that know how to paint themselves

Chapter goal

Stop hard-coding rectangles. Represent the player and platforms as data objects, then write draw functions that read that data.

entity objects
x/y/w/h
draw helpers
layering

How to do it

  1. 1

    Model the world as data

    A player is { x, y, w, h, color }. Platforms are an array of similar shapes. The engine state is just JavaScript objects.

  2. 2

    One draw path per type

    drawEntity(ctx, e) keeps rendering out of game logic. Later you can swap rectangles for sprites without touching physics.

  3. 3

    Order matters

    Draw background → platforms → player → HUD. Later layers cover earlier ones.

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.

Type-along practice
Photo layout
Entities & Drawing
1 / 72 lines

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.

Prewritten engine scriptmistakes 0
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 };
15
16const canvas = document.getElementById('c');
17const ctx = canvas.getContext('2d');
18const W = canvas.width, H = canvas.height;
19
20const PLAYER_W = 28;
21const PLAYER_H = 36;
22const PLATFORM_H = 14;
23const SPACING = 100;
24const HUE = 168;
25
26const player = {
27 x: 80, y: H - 80 - PLAYER_H,
28 w: PLAYER_W, h: PLAYER_H,
29 color: `hsl(${HUE} 80% 55%)`
30};
31
32const platforms = [
33 { x: 0, y: H - 28, w: W, h: 28, color: '#1a2535' },
34 { x: 140, y: H - 28 - SPACING * 0.7, w: 110, h: PLATFORM_H, color: '#243246' },
35 { x: 320, y: H - 28 - SPACING, w: 90, h: PLATFORM_H, color: '#243246' },
36 { x: 480, y: H - 28 - SPACING * 0.55, w: 120, h: PLATFORM_H, color: '#243246' },
37];
38
39function drawEntity(e, radius = 6) {
40 ctx.fillStyle = e.color;
41 roundRect(ctx, e.x, e.y, e.w, e.h, radius);
42 ctx.fill();
43}
44
45function roundRect(ctx, x, y, w, h, r) {
46 const rr = Math.min(r, w/2, h/2);
47 ctx.beginPath();
48 ctx.moveTo(x+rr, y);
49 ctx.arcTo(x+w, y, x+w, y+h, rr);
50 ctx.arcTo(x+w, y+h, x, y+h, rr);
51 ctx.arcTo(x, y+h, x, y, rr);
52 ctx.arcTo(x, y, x+w, y, rr);
53 ctx.closePath();
54}
55
56function draw() {
57 ctx.fillStyle = '#0b1018';
58 ctx.fillRect(0, 0, W, H);
59
60 // parallax bands
61 for (let i = 0; i < 4; i++) {
62 ctx.fillStyle = `rgba(94,234,212,${0.02 + i * 0.01})`;
63 ctx.fillRect(0, 40 + i * 50, W, 24);
64 }
65
66 for (const p of platforms) drawEntity(p, 4);
67
68 // player glow
69 ctx.fillStyle = 'rgba(94,234,212,0.15)';
70 ctx.beginPath();
71 ctx.ellipse(player.x + player.w/2, player.y + player.h, player.w * 0.7, 6, 0, 0, Math.PI*2);
72 ctx.fill();
73 drawEntity(player, 8);
74
75 // face
76 ctx.fillStyle = '#042f2e';
77 ctx.fillRect(player.x + player.w * 0.25, player.y + player.h * 0.3, 4, 4);
78 ctx.fillRect(player.x + player.w * 0.65, player.y + player.h * 0.3, 4, 4);
79
80 document.getElementById('hud').textContent =
81 'Chapter 3 · entities: 1 player + ' + platforms.length + ' platforms';
82}
83
84draw();
85
86})();
Your lineline 1
(function () {

Static composition — reshape entities with the sliders. · Sliders update the play window after a short pause.

Chapter solution
Reference
Live-bound HTML
Play window
Live