Skip to main content

Command Palette

Search for a command to run...

Flappy Bird in the Browser: Game Loop Architecture Every Developer Should Understand

Updated
5 min read
O
I’m a Python coder and Django coder based in Maharashtra, India, with over 6 years of hands-on experience building scalable web applications, APIs, and backend systems. I’m available for freelance or full-time remote roles worldwide. I specialize in crafting clean, efficient Django backends, REST and GraphQL APIs, and interactive dashboards using Python, PostgreSQL, and modern frontend stacks. Browse below to explore real-world tools I’ve built— from JSON formatters and SHA generators to real-time chat apps and SaaS platforms.

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.