Game Design & Development · Lesson 2
From Scratch: Build a Browser Game
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.
🧱 Anatomy of a Browser Game
A browser game is just three files working together:
<!-- 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:
Check input
Resolve physics
Draw everything
Show to player
(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); }
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; }
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.
🎮 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.
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(). 🧩 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.