Game Design & Development · Lesson 2

From Scratch: Build a Browser Game

HTML5 Canvas Game Loop Input Handling Collision Vanilla JavaScript + Scratch Path Beginner Friendly

Before engines and libraries, every game was written from scratch. Understanding the foundations — the game loop, canvas drawing, input, and collision — makes you a better developer regardless of which tools you use later. Every section below shows the idea two ways: in typed JavaScript, and in Scratch's drag-and-drop blocks — so build it in whichever one fits you, or both.

🧭 Before You Dive In

This lesson has more code on the page than Lesson 1 did — and that can feel like a lot at once. You don't need to memorize any of it. Read a section once, then type the code yourself (or drag the matching blocks in Scratch) and watch it run. Getting something on screen is most of the learning; the rest comes from tinkering with it afterward.

If something breaks the first time, that's completely normal — it happens to professional developers every day. Reading the error message and fixing one small thing at a time is the skill, not a sign you're doing it wrong.

Two meanings of "Scratch" in this lesson: building a game "from scratch" means writing every line yourself, with no libraries or engines — that's the JavaScript path below. Scratch (capital S) is a specific, free tool from MIT where you build games by snapping together colored blocks instead of typing. They're two different things that happen to share a name — this lesson teaches the same ideas in both.
What you need: for the JavaScript path, a code editor (VS Code works great) and a web browser. For the Scratch path, just a free account at scratch.mit.edu — no installs required either way. Budget 45–60 minutes.

🧱 Anatomy of a Browser Game

A browser game is just three files working together:

📄 index.html Creates the canvas element and loads your scripts. The stage on which the game lives.
🎨 style.css Optional. Makes the page look good around the canvas — background color, no scrollbars, centered layout.
⚡ game.js Where all the logic lives: the game loop, player, enemies, score, and collision detection.
<!-- index.html: the bare minimum -->
<canvas id="gameCanvas" width="480" height="320"></canvas>
<script src="game.js"></script>

🧩 The Same Idea in Scratch

Scratch skips the file setup entirely. Open a new project and you already have a Stage (the canvas), a default Sprite (the player), and a script area next to it — the equivalent of index.html and game.js already wired together for you.

🔁 The Game Loop

Think of a flipbook: each page looks a little different from the last, and flipping through them fast tricks your eye into seeing motion. Every game — from Pong to Cyberpunk 2077 — uses the exact same trick, just automated by the browser. That repeating cycle is called a game loop, and it fires about 60 times per second, doing two jobs each time:

Update Move objects
Check input
Resolve physics
Render Clear canvas
Draw everything
Show to player
Wait ~16ms
(60 fps)
repeat…
function gameLoop() {
  update();  // move everything, check input, collisions
  render();  // draw the current frame to the canvas
  requestAnimationFrame(gameLoop); // call again next frame
}
requestAnimationFrame(gameLoop); // start the loop

requestAnimationFrame is the browser's built-in game loop. It syncs to your monitor's refresh rate (usually 60 fps) and pauses automatically when you switch tabs — saving battery and CPU.

🧩 The Same Idea in Scratch

Live blocks — drag them, click to select, or reveal the JS they translate to.

"Green flag clicked" is Scratch's requestAnimationFrame(gameLoop) — the signal that starts everything. "Forever" is the loop itself: everything snapped inside it runs again and again, exactly like update() and render() firing every frame in the JS version. Scratch just hides the ~16ms timing math from you.

🖌️ Drawing with Canvas

Picture a whiteboard that gets wiped clean and redrawn from scratch every single frame — that's the Canvas API. It hands JavaScript a blank surface and lets you control every pixel. The one rule that trips people up at first: clear it, then redraw everything, every single frame.

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

function render() {
  // 1. Clear the canvas each frame
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 2. Draw the player (a blue square)
  ctx.fillStyle = '#3b82f6';
  ctx.fillRect(player.x, player.y, player.w, player.h);

  // 3. Draw score text
  ctx.fillStyle = '#fff';
  ctx.font = '16px Inter, sans-serif';
  ctx.fillText('Score: ' + score, 8, 22);
}
Important: Never draw without clearing first! If you skip clearRect, you'll see a smear trail of every position the object has ever been.

🧩 The Same Idea in Scratch

Scratch has no equivalent code to write here at all — that's the trade-off. Every sprite just carries its own x/y position and current costume, and Scratch's engine automatically clears and redraws the whole Stage every frame for you. The "clear, then redraw" rule still happens underneath — you just never have to remember it yourself.

⌨️ Keyboard Input

Think of each key like a light switch: pressing it flips the switch on, releasing it flips it back off. Instead of reacting to every single press and release, we just keep a little switchboard — a keys object — and check which switches are on during each update.

const keys = {};
const scrollKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Space'];

document.addEventListener('keydown', e => {
  keys[e.code] = true;
  if (scrollKeys.includes(e.code)) e.preventDefault(); // stop the page from scrolling
});
document.addEventListener('keyup',   e => keys[e.code] = false);

function update() {
  if (keys['ArrowLeft']  || keys['KeyA']) player.x -= 4;
  if (keys['ArrowRight'] || keys['KeyD']) player.x += 4;
  if (keys['ArrowUp']    || keys['KeyW']) player.y -= 4;
  if (keys['ArrowDown']  || keys['KeyS']) player.y += 4;
}
Common glitch: by default, the browser uses arrow keys and space to scroll the page. If you skip e.preventDefault() on those keys, the page will scroll up and down while someone is trying to play — breaking the controls. This one line is the fix for almost every "my arrow keys aren't working right" bug you'll hit.

That's it — the same switchboard pattern runs every keyboard-controlled game you've ever played, from Mario's jump button to a rhythm game's tap keys.

🧩 The Same Idea in Scratch

Live blocks — try swapping the dropdown or the number, then reveal the JS.

The hexagonal piece is a Sensing block called "key (_) pressed?" — a Boolean, just like the true/false values sitting inside our keys object. Snap it inside an "if … then" block, and it checks the exact same switch every single frame, since it lives inside the "forever" loop from before.

💥 Collision Detection

Picture two picture frames hanging on a wall. They only overlap if they overlap left-to-right and top-to-bottom at the same time — lining up on just one direction isn't enough. That simple idea has a formal name — AABB, short for Axis-Aligned Bounding Box — and it's the collision check almost every simple 2D game relies on.

function collides(a, b) {
  return a.x < b.x + b.w &&  // a's left is left of b's right
         a.x + a.w > b.x &&  // a's right is right of b's left
         a.y < b.y + b.h &&  // a's top is above b's bottom
         a.y + a.h > b.y;    // a's bottom is below b's top
}

This exact check is how Pac-Man knows he touched a ghost, how Mario knows he's landed on a platform, and how the dodge game below knows you got hit.

🧩 The Same Idea in Scratch

Live blocks — edit the sprite name or the number, then reveal the JS.

Scratch's "touching (_)?" block — another Sensing Boolean — is the whole collides() function above, already written for you. It's even more precise than AABB: it checks actual sprite shapes, not just rectangles. Same job, zero formula to type.

See the Scratch Wiki's guide to collision blocks →

🎮 Play It — Dodge Game

This complete dodge game was built using only the techniques above: Canvas, game loop, keyboard input, and AABB collision. Move with arrow keys or WASD. Avoid the red enemies. Score rises as you survive.

Score: 0 Arrow keys / WASD ♥ ♥ ♥
What you see in the code: the game loop runs every frame via requestAnimationFrame. Enemies spawn at random X positions and fall from the top. AABB collision checks each enemy against the player every frame. The score ticks up with Date.now().
Which tiny-loop archetype is this? This is the Dodging Game archetype from What Makes a Game? — one mechanic (move), one lose condition (collision), one climbing variable (score). Flip the collision logic — good items add score instead of costing a life — and you've rebuilt the Catch Game archetype using the exact same loop.

🧩 Build This Exact Game in Scratch

Scratch's own team built an official tutorial for this exact archetype — move a sprite, avoid (or chase) another one, and keep score. It walks you through the same green-flag → forever → touching → score pattern you just read above, one block at a time.

Open the official Scratch Chase Game tutorial →

🛠️ Your Turn — Extend the Game

Now that you've seen the pattern, try one of these small, safe edits to code that already works. Pick whichever sounds most fun — you don't need to do all six, and it's completely fine to keep the code above open while you work.

Change player speed — find the 4 in the movement code and try 6 or 2. How does this change the difficulty feel?

Add a score multiplier — make enemies worth more points the longer you survive (multiply score by a factor that increases over time).

Add a power-up — spawn a green square at random intervals. When the player collides with it, restore a life.

Add sound — use the Web Audio API to play a beep when the player is hit: new AudioContext() → create an oscillator → start() and stop(0.1).

Add a high score — use localStorage.setItem('highscore', score) to save the best score between sessions.

Turn Dodge into Catch — in the collision check, instead of always subtracting a life, make half the falling squares "good" (green, +1 score) and half "bad" (red, −1 life). You've just built a second tiny-loop archetype from the same code.

🧩 The Same Six Ideas — in Scratch

Working in Scratch instead? Every edit above has a direct block equivalent: change the number inside "change x by (4)" for speed · multiply the "change score by" amount using a timer variable for a multiplier · add a power-up sprite with its own "if touching (_)? then change lives by (1)" script · drop in a "play sound" block inside your collision check · add a "highscore" variable and an "if score > highscore then" block to save the best run · and swap "change lives by (−1)" for "change score by (1)" on your good sprites to turn Dodge into Catch.