Source Code

<div class="fd-wrap">
  <div class="fd-stage">
    <canvas id="fd-canvas" width="360" height="520"></canvas>
    <div class="fd-hud">
      <span id="fd-score">0</span>
    </div>
    <div class="fd-screen fd-start-screen" id="start-screen">
      <p class="fd-logo">Flap &amp; Dodge</p>
      <p class="fd-sub">Tap, click, or press Space to flap</p>
      <button class="fd-btn" id="start-btn">Start</button>
      <p class="fd-best">Best: <span id="start-best">0</span></p>
    </div>
    <div class="fd-screen fd-over-screen" id="over-screen">
      <p class="fd-logo">Game Over</p>
      <p class="fd-final-score">Score: <span id="final-score">0</span></p>
      <p class="fd-best">Best: <span id="over-best">0</span></p>
      <button class="fd-btn" id="retry-btn">Try Again</button>
    </div>
  </div>
</div>

Flap & Dodge Obstacle Game — Free HTML CSS JS Snippet

Flap & Dodge Obstacle Game · Games · Plain HTML, CSS & JS · Live preview

What's included

Features

Delta-time physics integration: gravity and obstacle speed scale by real elapsed seconds, not frame count
requestAnimationFrame game loop with a clamped dt (max ~33ms) to prevent physics jumps on dropped frames
Randomised obstacle gap position per pipe pair, generated with a safe margin from the canvas edges
Two-axis AABB-vs-gap collision test: horizontal overlap gates a vertical gap-boundary check
Per-pipe passed flag ensures the score increments exactly once per obstacle regardless of frame rate
Canvas-drawn character with velocity-driven tilt rotation for a readable falling/flapping visual cue
Persistent best score via localStorage, shown on both the start screen and the game-over screen
Unified input handling across mousedown, touchstart (with preventDefault), and Space keydown

About this UI Snippet

Flap & Dodge Obstacle Game — Delta-Time Physics, Canvas Rendering & Gap Collision Detection

Screenshot of the Flap & Dodge Obstacle Game snippet rendered live

Side-scrolling flap-and-dodge games look simple on the surface but hinge entirely on one detail most quick clones get wrong: frame-rate independence. If gravity and obstacle speed are applied as a fixed number of pixels per rendered frame, the game runs at wildly different speeds on a 60Hz versus a 144Hz display, or stutters unpredictably when the browser tab is throttled. This snippet uses genuine delta-time physics — every update multiplies velocity and position changes by the elapsed time since the last frame, in seconds, so the character falls, accelerates, and the obstacles scroll at a truly constant real-world speed regardless of the device's refresh rate.

The game loop and delta time

The core loop runs through requestAnimationFrame(loop), and each call computes dt as the milliseconds since the previous frame converted to seconds and clamped to a maximum of 0.033 (roughly 30fps) so a dropped frame or tab-switch stall never produces a huge physics jump that teleports the character through an obstacle. Gravity is defined as GRAVITY = 1400 pixels per second squared, applied each frame as bird.vy += GRAVITY * dt, and the resulting velocity moves the character as bird.y += bird.vy * dt. A click, tap, or spacebar press calls flap(), which simply overwrites the vertical velocity to a fixed upward value (FLAP_VELOCITY = -420) — the same physics integration then naturally arcs the character back down under gravity afterward.

Obstacle spawning and gap collision

New obstacle pairs are pushed onto a pipes array on a fixed real-time interval (PIPE_INTERVAL seconds, not frame count), each with a randomly chosen gapCenter kept away from the very top and bottom of the canvas via a margin. Every frame, each pipe's x decreases by PIPE_SPEED * dt, and pipes are filtered out of the array once they scroll fully off the left edge, keeping the array small and collision checks cheap. Collision detection is a straightforward two-axis check: a pipe is only a collision candidate while the character's horizontal extent overlaps the pipe's horizontal extent, and within that window the character loses if its vertical extent extends above the gap's top edge or below the gap's bottom edge — exactly the classic "AABB vs gap" test used by every implementation of this genre.

Scoring and canvas rendering

A pipe is marked passed and the score increments the instant the pipe's right edge scrolls behind the character's left edge, so scoring happens exactly once per obstacle regardless of frame rate. Rendering is done entirely on a single 2D canvas context: a vertical sky gradient, filled rectangles for the top and bottom pipe segments (derived directly from each pipe's gapCenter and PIPE_GAP), and a hand-drawn circular character with a rotating tilt driven by the current vertical velocity (Math.atan-free — a simple clamped ratio of vy) so the character visibly noses down while falling and up while flapping, a small but important readability cue borrowed from the genre's classic feel. Best score persists via localStorage and is shown on both the start screen and the game-over screen.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the dt clamp in the game loop protects against a physics-breaking jump if the browser tab is backgrounded and then resumed — that single line is doing more work than it looks like. It's also a great snippet to extend with AI assistance: ask it to add a gradual difficulty ramp that increases obstacle speed or narrows the gap as the score rises, add a simple particle burst or screen-shake effect on collision for more satisfying game feedback, or refactor the character's tilt-rotation math into its own small function with comments explaining the velocity-to-angle mapping. Use the assistant to sanity-check the collision math too — walk through a near-miss scenario by hand and confirm the AABB-vs-gap logic agrees with your intuition.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a canvas-based side-scrolling flap-and-dodge obstacle game in plain HTML, CSS, and JavaScript — no frameworks, no libraries, no external assets. Do not name it after any existing commercial game.

Requirements:
- A character falls under constant gravity and receives an upward velocity boost on each click, tap, or spacebar press.
- A steady stream of gapped obstacle pairs scroll from right to left at a constant speed, each with a randomly chosen vertical gap position, spawned on a fixed real-world time interval.
- Use requestAnimationFrame for the game loop and compute all movement (gravity integration and obstacle scrolling) using delta-time in seconds since the previous frame, not a fixed per-frame pixel amount, so speed stays consistent across different frame rates. Clamp the delta-time value to avoid large physics jumps after a dropped frame or backgrounded tab.
- Colliding with any obstacle, the ceiling, or the ground immediately ends the run.
- The score increases by exactly one each time the character successfully passes through an obstacle gap, counted once per obstacle regardless of frame rate.
- Include a Start screen shown before the first run, and a Game Over screen after a collision showing the final score and a "Try again" button that fully resets the game state.
- Persist the best-ever score across page reloads using localStorage and display it on both the start and game-over screens.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Start the gameClick "Start" on the opening screen, or simply click/tap the stage or press Space — any flap input while idle begins the run immediately via startGame().
  2. 2
    Flap to gain heightEach click, tap, or spacebar press sets the character's vertical velocity to a fixed upward value (FLAP_VELOCITY), instantly interrupting its fall. Gravity then pulls it back down every frame until you flap again.
  3. 3
    Dodge the gapsObstacle pairs scroll in from the right at a constant real-world speed (PIPE_SPEED, not frame-dependent) with a randomly positioned vertical gap. Keep the character's circle within the gap as each pair passes.
  4. 4
    Watch your score climbThe score increments by one the instant a pipe pair is fully passed, tracked per-pipe with a passed flag so each obstacle only scores once no matter the frame rate.
  5. 5
    See your final score on Game OverColliding with a pipe, the ceiling, or the ground ends the run immediately, freezes the loop, and shows the Game Over screen with your final score and your all-time best.
  6. 6
    Beat your best scoreYour highest score persists across sessions via localStorage. Click "Try Again" to instantly reset the character, obstacles, and score and start a fresh delta-time-driven run.

Real-world uses

Common Use Cases

Teaching delta-time game physics and canvas fundamentals
This snippet is a compact, complete example of the single most important concept in real-time game programming: decoupling simulation speed from rendering frame rate. Students can compare the update() function's use of dt against a naive fixed-step version to directly see why frame-rate-dependent physics breaks on variable-refresh-rate hardware.
Portfolio piece demonstrating canvas game-loop architecture
A working obstacle-dodging game with correct delta-time physics, collision detection, and persisted high scores is a strong, self-contained portfolio artifact that goes well beyond a static canvas drawing — it demonstrates real game-loop architecture, state machines (idle/playing/over), and input handling across mouse, touch, and keyboard.
Engaging waiting-screen or loading-state distraction
A quick, replayable arcade game like this is a natural fit for a loading screen, an offline/error page, or a "while you wait" panel in a checkout or onboarding flow, giving users something genuinely fun to do rather than staring at a spinner.
Canvas HUD and overlay-screen pattern reference
The layered structure — a canvas for the simulation, an absolutely positioned score HUD, and semi-transparent start/game-over overlay screens that fade in and out — is a reusable pattern for any canvas-based mini-game or interactive demo that needs UI chrome layered on top of a rendering surface.
Base for difficulty ramps or alternate obstacle patterns
Because GRAVITY, PIPE_SPEED, PIPE_GAP, and PIPE_INTERVAL are top-level constants, this snippet is a practical starting point for a difficulty curve that gradually increases pipe speed or narrows the gap as the score rises, similar in spirit to how the Rhythm Tap Game snippet could scale its note frequency over time.
One-thumb mobile arcade game
The single-tap-to-flap control scheme, combined with preventDefault on touchstart to stop the page from scrolling or zooming during play, makes this comfortable to play one-handed on a phone without any on-screen buttons taking up screen space.
Related: Dot Muncher Maze Game
See the Dot Muncher Maze Game for a related games pattern worth pairing with this one.
Related: Flexbox Alignment Game
See the Flexbox Alignment Game for a related games pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

requestAnimationFrame does not guarantee a fixed interval between calls — it can fire at 60fps, 120fps, or drop frames under load. If gravity and obstacle speed were applied as constant pixel amounts per frame, the game would run faster on high-refresh-rate displays and stutter unpredictably under load. By multiplying every physics change by dt (seconds elapsed since the last frame), this snippet keeps gravity acceleration and obstacle scroll speed constant in real-world time regardless of how often the frame callback fires.

For each pipe, the code first checks horizontal overlap: does the character's circle extent intersect the pipe's x-range? Only if that is true does it check the vertical condition — whether the character's top edge is above the gap's top boundary or its bottom edge is below the gap's bottom boundary. If either is true while horizontally overlapping, it is a collision. This two-stage check is cheap because most pipes are off-screen or far away and get skipped by the horizontal test immediately.

Yes. Add a scaling factor to PIPE_SPEED and/or shrink PIPE_GAP based on the current score inside update(), for example recalculating an effective speed as PIPE_SPEED + score * 3 each frame. Keep the change gradual and test that the gap never shrinks below roughly 2.5x the character's diameter, or the game becomes unfairly difficult to react to.

best is read from localStorage once when the script first loads and kept in memory for the whole session, only being overwritten (in memory and in localStorage) when a completed run's score exceeds it. Starting a new game resets the current score to 0 but intentionally leaves best untouched so it always reflects your highest-ever run, shown on both the start screen and the game-over screen.

Create Audio objects (e.g. const flapSound = new Audio("flap.mp3")) and call .play() inside flap(), inside the score-increment branch in update(), and inside endGame() respectively. Since many browsers block audio playback before any user interaction, the very first flap() call (which also starts the game from idle) is a safe place to "unlock" audio playback for the rest of the session.