Lesson 12: Build a Space Shooter
Embark on your journey to building production grade apps.
Congratulations on making it to the final lesson of freshman year. As a reward, you'll build something fun: a browser game.
What We're Building
- A spaceship the player controls with arrow keys.
- Asteroids falling from the top of the screen at increasing speed.
- Space bar fires a laser that destroys asteroids.
- Destroying an asteroid scores points.
- Getting hit ends the run.
- Your best score is saved, so beating it means something.
Skills from earlier lessons doing real work here:
- Performance (optimizing performance): the loop has to hit 60fps, which decides how you draw and how you store state.
- Reading AI-generated code (reading code lesson): a game loop is the easiest place in the world to accumulate slop, because everything is tempting to shove into one file.
- Error handling (error handling lesson): a game that crashes mid-run loses the player's score, and they will not play again.
Set Up the Project
Open your Repl and paste into the AI panel:
I'm building a browser space shooter. Scaffold a Next.js
project with:
- A single page / route. No auth, no database, no API routes.
- A <main> area that will hold the game canvas. Nothing in it
yet.
- Minimal UI. Black background, white text, monospace font.
This is a space game.
Constraints:
- Each file does one thing
- Names describe what they contain or do
- No dead code
- No flexibility for cases I haven't described
Before you say you're done, open the browser and confirm the
page loads with a black background and nothing else on it.
Once the page loads, move on.
Build the Game Loop
This is the whole game in one prompt. Be specific, because every detail you leave out is a detail the agent invents.
> Build the space shooter inside an HTML canvas that fills the
<main> area.
Mechanics:
- Player ship at the bottom, moves left/right with arrow keys.
- Space bar fires a laser that travels straight up.
- Asteroids spawn at random x positions at the top, fall at
increasing speed over time.
- Laser hitting an asteroid destroys both and adds 10 points.
- Asteroid hitting the ship ends the game.
- Render current score in the top-left.
State machine:
- "idle": shows "Press space to start"
- "playing": game loop is running
- "over": shows final score and "Press space to play again"
Keep the canvas at 800x600. Pure canvas 2D, no external game
libraries. Use requestAnimationFrame.
Now play it. Not a quick click, an actual few minutes. If the game isn't fun, nothing downstream matters, and the things that make it unfun are usually small: the ship moves too slowly, the laser fires too fast, asteroids spawn in a pattern you can't dodge. Fix those before adding anything else.
In the game loop prompt, what does the over state show?
Why requestAnimationFrame
A game loop is a function that updates state (ship position, asteroids, collisions), redraws the canvas, and schedules itself to run again. How it schedules itself is the thing to get right.
Two options exist. setInterval(loop, 16) fires every 16ms (roughly 60 times per second). requestAnimationFrame(loop) asks the browser to call your function right before its next paint.
Use requestAnimationFrame. It syncs your updates with the browser's render cycle, pauses automatically in hidden tabs (saving CPU and battery), and matches the display's refresh rate (60Hz, 120Hz) without any code change. setInterval does none of those things.
The pattern:
function loop() {
update()
draw()
requestAnimationFrame(loop)
}
requestAnimationFrame(loop)
Every browser game uses this.
Why use requestAnimationFrame instead of setInterval for the game loop?
Fix the Frame Rate Problem
Here's a bug your agent almost certainly shipped, and you can't see it on your own machine.
If the code moves the ship by a fixed amount every frame, the game speed is tied to the display refresh rate. On a 60Hz laptop it plays as designed. On a 120Hz monitor it runs at double speed and becomes unplayable. On a slow machine dropping to 30fps it crawls. Same code, three different games.
The fix is called delta time. Instead of "move 5 pixels every frame," the code measures how long the last frame actually took and moves the ship proportionally. Movement then depends on elapsed time, not frame count, and the game plays identically everywhere.
> The game currently moves objects by a fixed amount per frame,
which ties game speed to the display refresh rate. Refactor
to use delta time:
- Track the timestamp of the previous frame.
- Calculate the elapsed time since that frame.
- Express all movement as speed per second, multiplied by
elapsed time.
- Cap the delta at around 100ms so that switching back to a
backgrounded tab doesn't teleport everything across the
screen.
Don't change any gameplay values, just how movement is
calculated. Tell me which files you changed.
To confirm it worked: open Chrome DevTools, press Cmd+Shift+P (Ctrl+Shift+P on Windows), type "Show frame rendering stats," and watch the FPS counter while you play. Then throttle your CPU in the Performance tab and play again. The game should get choppier but not slower.
What goes wrong when movement is a fixed number of pixels per frame?
Make It Feel Good
This is the part nobody prompts for and everybody notices. A game that works and a game that feels good are separated by about six small effects.
> Add game feel. Keep every change small and don't touch the
core mechanics:
- Screen shake for about 150ms when an asteroid is destroyed,
stronger when the ship is hit.
- Asteroids break into a few small particles that fade out
instead of just disappearing.
- A short flash on the ship when it takes a hit.
- A thin trail behind the laser.
- A starfield background that scrolls slowly downward.
- Ship movement that accelerates and decelerates rather than
starting and stopping instantly.
Do not add sound yet. Tell me which files you changed.
Play it again. The difference is usually larger than you expect from a list this short.
If you want sound, add it separately, and keep it muted by default with a toggle. A game that makes noise the instant it loads is a game people close.
What is the advice for adding sound to the game?
Save the High Score
Your score currently disappears when the page reloads, which means there's nothing to beat.
> Save the player's best score in the browser's localStorage
under a single key.
- On game over, if the score beats the stored best, replace it.
- Show "Best: N" next to the current score at all times.
- Show "New best!" on the game over screen when the player
beats their record.
- Handle the case where localStorage is unavailable or holds
invalid data. Fall back to a best score of 0 rather than
crashing.
That last line matters. Browsers block localStorage in some privacy modes, and a game that white-screens instead of starting is worse than a game with no high score.
Worth understanding what this is and isn't: localStorage lives in one browser on one machine. Clear your browser data and the score is gone. Open the game on your phone and you start from zero. It's also trivially editable by anyone who opens DevTools, so it works as a personal record and not as a competitive one. A shared leaderboard needs a server, and a server needs a way to tell a real run from a fake POST, which is a much bigger problem than it sounds.
Why should the high-score code handle localStorage being unavailable or holding invalid data?
Audit Before You Call It Done
Games attract slop faster than anything else you'll build, because every new effect is tempting to paste into the file that's already open. You've added six features to this codebase. Check what happened to it:
> Map the architecture of this project.
1. What are the major parts of this game? Name them after
what they do, like input, rendering, or collision.
2. For each part: what is it responsible for, and which
files belong to it?
3. Flag any file that has grown too large or is handling
several unrelated jobs.
Don't change anything yet.
If one file is holding input handling, physics, rendering, particles, and the score, split it before you move on. This is a small enough project that the split takes one prompt, and it's the difference between adding a boss fight next month and giving up.
Why do games attract slop faster than other projects you build?
Try It End-to-End
- Play three full runs. Does the difficulty ramp feel fair, or does it spike?
- Reload the page. Is your best score still there?
- Play in a background tab for ten seconds, then switch back. Does everything continue normally, or does the screen jump?
- Open it on a phone. It won't be playable without touch controls, and that's fine, but confirm it doesn't crash.
- Hold down the space bar. Can you fire fast enough to trivialize the game? If so, add a cooldown.
- Let an asteroid hit you deliberately. Does the game over screen appear cleanly, and does pressing space start a genuinely fresh run rather than a half-reset one?
That last check catches the most common bug in this build: state from the previous run leaking into the next one.
What is the most common bug this build hits when you press space to play again after a game over?
What You Just Built
- A real-time game loop running at 60fps, synced to the browser's render cycle.
- Frame-rate independent movement, so it plays the same on any display.
- A state machine separating idle, playing, and game over.
- Collision detection, a difficulty curve, and enough game feel to make it worth playing.
- Persistent local state that fails safely when the browser won't cooperate.
The techniques here transfer to anything interactive and real-time: a canvas-based visualization, a drawing tool, an animated dashboard. The loop, the delta time, and the state machine are the same three pieces every time.
What's Next
That's freshman year — you can vibecode an app, ship it, harden it against the most common attacks, think like a designer, and build something interactive from scratch. Next, you'll move into the Sophomore track, where you go from shipping simple apps to building real product features: user authentication, payments, performance, notifications, and more.
0/7 correct
0% — get all correct to complete