Flappy Bird in the Browser: Game Loop Architecture Every Developer Should Understand
Flappy Bird is the "Hello, World" of browser game development. It's been implemented in every language, on every platform, by millions of developers. But most write it without understanding why the physics constants are what they are, or why requestAnimationFrame matters over setInterval.
This post goes deeper. And if you just want to play — here's the live game.
Why requestAnimationFrame and Not setInterval
A naive game loop uses setInterval(gameLoop, 16) — "run every 16ms to get ~60fps." The problem is setInterval doesn't know about the browser's rendering pipeline. It can fire mid-frame, causing visual tears, or fire when the tab is hidden and waste CPU.
requestAnimationFrame solves both:
javascript
function gameLoop(timestamp) {
const delta = timestamp - lastTime;
lastTime = timestamp;
update(delta);
draw();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
Key benefits:
Syncs to display refresh rate — 60fps on 60Hz screens, 120fps on 120Hz screens
Pauses when tab is hidden — no wasted computation
Passes a high-precision timestamp — you can make physics frame-rate independent using
delta
Frame-rate independent physics is important. If your game runs the same physics update regardless of how much time has passed, it will run at different speeds on different machines. The fix: multiply velocity and gravity by delta / targetFrameTime.
The Physics Constants — Why These Numbers
The feel of Flappy Bird is entirely in two numbers: gravity and jumpForce. Everything else is tuning.
For a canvas of height 600px:
| Constant | Value | Why |
|---|---|---|
| gravity | 0.45 | ~1.5px increase in downward vel per frame |
| jumpForce | -8.5 | Enough to clear ~130px of height per jump |
| pipeSpeed | 3 | ~180px/sec at 60fps — readable, reactive |
| gapHeight | 150 | Challenging but achievable at low pipe speeds |
These aren't arbitrary. The ratio of jumpForce to gravity determines how many frames the bird rises vs falls. At these values:
The bird rises for roughly 18 frames after a jump (8.5 / 0.45)
It falls back to jump height in another 18 frames
Total air time:
36 frames (0.6 seconds at 60fps)
That 0.6s window is the entire skill expression of the game. Players learn to time taps within that window. Change either constant significantly and the game feels broken.
Collision Detection — Keep It Simple
Flappy Bird uses AABB collision. Despite being a "circle" visually, the hitbox is rectangular — and intentionally slightly smaller than the sprite. This is called hitbox forgiveness and every great action game does it.
javascript
const BIRD = { x: 80, y: birdY, w: 34, h: 34 };
const HITBOX = { x: 88, y: birdY + 6, w: 22, h: 22 }; // 6px inset on all sides
function rectCollide(a, b) {
return (
a.x < b.x + b.w &&
a.x + a.w > b.x &&
a.y < b.y + b.h &&
a.y + a.h > b.y
);
}
The inset hitbox means a near-miss feels fair. Without it, players perceive the game as unfair even when they technically hit the pipe — because the visual edge of the bird appeared to clear it.
State Machine for Game Screens
A production-quality Flappy Bird has three states: IDLE, PLAYING, DEAD. Managing transitions explicitly prevents bugs:
javascript
const STATE = { IDLE: 0, PLAYING: 1, DEAD: 2 };
let state = STATE.IDLE;
function handleInput() {
if (state === STATE.IDLE) {
state = STATE.PLAYING;
return;
}
if (state === STATE.PLAYING) {
birdVel = JUMP_FORCE;
return;
}
if (state === STATE.DEAD) {
resetGame();
state = STATE.IDLE;
}
}
This pattern — explicit state machines for UI/game flow — applies well beyond games. I use similar patterns in Django views for multi-step workflows and in Celery tasks for job states (PENDING → RUNNING → COMPLETE → FAILED).
Pipe Scrolling Without Object Pooling
The simplest approach creates new pipe objects and removes them when off-screen:
javascript
function updatePipes() {
pipes.forEach(p => p.x -= pipeSpeed);
pipes = pipes.filter(p => p.x + PIPE_WIDTH > 0);
}
At any moment you have 2–4 pipes on screen. The GC overhead of creating/destroying ~1 object per second is negligible. Object pooling is the right optimisation for particle systems (hundreds of objects/second), not for Flappy Bird.
Play It Live
The version I've deployed runs entirely client-side, no server round-trips:
rohanyeole.com/flappy-bird — free, no account, works on mobile and desktop.
It's part of the same infrastructure as my developer tools collection — all static, all fast. Tools like the diff checker, cron generator, password strength checker, and markdown to HTML converter follow the same principle: zero friction, immediate utility.
If You Want to Build the Django-Served Version
Hosting a static game on Django is straightforward. The game files live in static/, the view returns a template, and the template loads the JS. No API calls, no database hits. This is also how I serve all 50+ tools — Django handles routing and templating, the logic is entirely client-side JS.
If you want to discuss Django architecture, deployment, or Python backend work, my site is here and I'm available for projects.
