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.
How to do it
- 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
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
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.
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 SPEED = 4;22const PLAYER_SIZE = 30;23const FRICTION = 0.18;24const BOUNCE = 0.35;2526const 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; });3233const 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};3940function 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;4647 // normalize diagonal48 if (ax && ay) { ax *= 0.7071; ay *= 0.7071; }4950 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;5657 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; }6162 ctx.fillStyle = '#0b1018';63 ctx.fillRect(0, 0, W, H);6465 // crosshair grid66 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();7172 ctx.fillStyle = '#5eead4';73 ctx.beginPath();74 roundRect(ctx, player.x, player.y, player.w, player.h, 8);75 ctx.fill();7677 const pressed = Object.keys(keys).filter(k => keys[k]).join(' ') || '—';78 hud.textContent = 'Chapter 4 · keys: ' + pressed;7980 requestAnimationFrame(loop);81}8283function 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}9192requestAnimationFrame(loop);9394})();
Click play, then use Arrow keys or WASD to move. · Sliders update the play window after a short pause.