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

Input Systems

Keyboard state that the loop can read every frame

Chapter goal

Capture keydown/keyup into a keys map, then move the player from the game loop — never only inside event handlers.

keydown / keyup
keys map
preventDefault
focus & canvas

How to do it

  1. 1

    Track pressed keys

    const keys = {}; on keydown set keys[e.code]=true; on keyup set false. The loop reads keys['ArrowLeft'] etc. each frame.

  2. 2

    Move from the loop

    If left is held, player.x -= speed. Holding a key generates one keydown then repeats — the map approach makes continuous motion reliable.

  3. 3

    Clamp to the world

    After moving, clamp x/y so the entity stays on the canvas. Later collision replaces simple clamps.

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
Input Systems
1 / 78 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;
19const hud = document.getElementById('hud');
20
21const SPEED = 4;
22const PLAYER_SIZE = 30;
23const FRICTION = 0.18;
24const BOUNCE = 0.35;
25
26const keys = Object.create(null);
27window.addEventListener('keydown', e => {
28 keys[e.code] = true;
29 if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(e.code)) e.preventDefault();
30});
31window.addEventListener('keyup', e => { keys[e.code] = false; });
32
33const player = {
34 x: W/2 - PLAYER_SIZE/2,
35 y: H/2 - PLAYER_SIZE/2,
36 w: PLAYER_SIZE, h: PLAYER_SIZE,
37 vx: 0, vy: 0,
38};
39
40function loop() {
41 let ax = 0, ay = 0;
42 if (keys['ArrowLeft'] || keys['KeyA']) ax -= 1;
43 if (keys['ArrowRight'] || keys['KeyD']) ax += 1;
44 if (keys['ArrowUp'] || keys['KeyW']) ay -= 1;
45 if (keys['ArrowDown'] || keys['KeyS']) ay += 1;
46
47 // normalize diagonal
48 if (ax && ay) { ax *= 0.7071; ay *= 0.7071; }
49
50 player.vx += ax * SPEED * 0.55;
51 player.vy += ay * SPEED * 0.55;
52 player.vx *= (1 - FRICTION);
53 player.vy *= (1 - FRICTION);
54 player.x += player.vx;
55 player.y += player.vy;
56
57 if (player.x < 0) { player.x = 0; player.vx = Math.abs(player.vx) * BOUNCE; }
58 if (player.y < 0) { player.y = 0; player.vy = Math.abs(player.vy) * BOUNCE; }
59 if (player.x + player.w > W) { player.x = W - player.w; player.vx = -Math.abs(player.vx) * BOUNCE; }
60 if (player.y + player.h > H) { player.y = H - player.h; player.vy = -Math.abs(player.vy) * BOUNCE; }
61
62 ctx.fillStyle = '#0b1018';
63 ctx.fillRect(0, 0, W, H);
64
65 // crosshair grid
66 ctx.strokeStyle = 'rgba(125,211,252,0.06)';
67 ctx.beginPath();
68 ctx.moveTo(W/2, 0); ctx.lineTo(W/2, H);
69 ctx.moveTo(0, H/2); ctx.lineTo(W, H/2);
70 ctx.stroke();
71
72 ctx.fillStyle = '#5eead4';
73 ctx.beginPath();
74 roundRect(ctx, player.x, player.y, player.w, player.h, 8);
75 ctx.fill();
76
77 const pressed = Object.keys(keys).filter(k => keys[k]).join(' ') || '—';
78 hud.textContent = 'Chapter 4 · keys: ' + pressed;
79
80 requestAnimationFrame(loop);
81}
82
83function roundRect(ctx, x, y, w, h, r) {
84 ctx.moveTo(x+r, y);
85 ctx.arcTo(x+w, y, x+w, y+h, r);
86 ctx.arcTo(x+w, y+h, x, y+h, r);
87 ctx.arcTo(x, y+h, x, y, r);
88 ctx.arcTo(x, y, x+w, y, r);
89 ctx.closePath();
90}
91
92requestAnimationFrame(loop);
93
94})();
Your lineline 1
(function () {

Click play, then use Arrow keys or WASD to move. · Sliders update the play window after a short pause.

Chapter solution
Reference
Live-bound HTML
Play window
Live