Lesson 02
Flying Bird: build a one-button arcade game that actually feels playable.
A Flappy-style game is a great first JavaScript project because it has only one input, but still teaches physics, obstacle spawning, collision, scoring and restart states.
Play the reference first
Before reading the code, play the live game for a minute: Flying Bird. Notice the whole design is built around three feelings: the bird drops quickly enough to demand attention, the flap is strong enough to recover, and the pipe gap is wide enough that failure feels fair.
1. Keep the game state small
The real game starts with a short set of constants and variables. This is the best possible shape for a small arcade game: tuneable constants at the top, mutable state below.
const gravity = 0.5;
const jump = -8;
const pipeW = 50;
const pipeGap = 150;
const pipeSpeed = 3;
let bird = { x: 80, y: H / 2, vy: 0, r: 15 };
let pipes = [];
let score = 0;
let best = parseInt(localStorage.getItem("flappyBest") || "0");
let gameRunning = true;
let started = false;
let frameCount = 0;
`bird` has position, vertical speed and radius. Pipes live in an array because new pipes keep entering from the right. The score is separate from `best`, because current run and best run have different lifetimes.
If a game is hard to tune, its constants are probably hidden inside update logic. Put numbers like gravity, jump strength and speed near the top so you can balance them without hunting through the file.
2. Gravity is just velocity changing over time
Gravity in this game is intentionally simple. Every frame adds a little downward speed, then the bird's position changes by that speed.
bird.vy += gravity;
bird.y += bird.vy;
When the player taps, we do not move the bird directly. We set vertical velocity to a negative number, so the bird launches upward and gravity slowly pulls it down again.
function flap() {
if (!gameRunning) return;
if (!started) started = true;
bird.vy = jump;
}
This creates a natural arc with only two lines of physics. The trick is balancing the values. If `jump` is too strong, the bird feels floaty. If `gravity` is too high, the game becomes stressful instead of skillful.
3. Spawning pipes with a frame counter
The game uses `frameCount % 90 === 0` to spawn a pipe every 90 ticks. Each pipe stores its x position and gap. Moving obstacles is just subtracting speed from x.
if (frameCount % 90 === 0) addPipe();
for (let i = pipes.length - 1; i >= 0; i--) {
pipes[i].x -= pipeSpeed;
if (pipes[i].x + pipeW < 0) {
pipes.splice(i, 1);
continue;
}
}
The loop runs backward because it removes old pipes with `splice`. If you remove items while looping forward, it is easy to skip the next pipe by accident.
4. Scoring only once per pipe
A pipe is scored when its right edge passes the bird. The `scored` flag prevents the same pipe from giving points every frame.
if (!pipes[i].scored && pipes[i].x + pipeW < bird.x) {
pipes[i].scored = true;
score++;
scoreEl.textContent = score;
if (score > best) {
best = score;
bestEl.textContent = best;
localStorage.setItem("flappyBest", best);
}
}
Using `localStorage` is a lightweight way to keep a best score without user accounts. It is not secure or global, but for a quick browser arcade game it gives the player a reason to retry.
5. Collision with pipes and screen edges
The bird ends the run if it hits the top, bottom or a pipe. The pipe check first confirms the bird overlaps horizontally with the pipe. Only then does it test whether the bird is outside the safe vertical gap.
if (bird.y + bird.r > H || bird.y - bird.r < 0) {
gameRunning = false;
draw();
return;
}
if (bird.x + bird.r > pipes[i].x && bird.x - bird.r < pipes[i].x + pipeW) {
if (bird.y - bird.r < pipes[i].top || bird.y + bird.r > pipes[i].bottom) {
gameRunning = false;
}
}
This is not pixel-perfect, but it is readable and fair. For arcade games, readable hitboxes usually matter more than mathematically perfect ones.
6. Input: click and Space
The game supports mouse/touch through click and keyboard through Space. The important detail is `preventDefault()` on Space, which prevents the page from scrolling while the game is being played.
document.addEventListener("click", flap);
document.addEventListener("keydown", e => {
if (e.key === " ") {
e.preventDefault();
flap();
}
});
That tiny `preventDefault` line fixed a real category of bugs in Supagames: games could work locally, but on the website the browser would scroll down when the player tried to jump.
What to change for your own version
- Replace the bird with a balloon, drone, bat or paper plane, but keep the physics simple.
- Add collectible coins inside safe gaps, but do not put them so high or low that the route becomes unfair.
- Increase speed slowly after every 5 points instead of starting too hard.
- Add a short start screen so the first pipe does not punish a player before they understand the input.
- Keep one-button input. The charm of this genre is clarity, not complicated controls.