webdevpuneet.com

Flap & Dodge Obstacle Game — Free HTML CSS JS Snippet

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

CategoryGames

For supporters

Source Code

Want the source code?

Play the game for free. Become a WebDevPuneet supporter to unlock the complete HTML, CSS and JavaScript source code, the exports (HTML, React, Vue, Angular, Tailwind) and Fork.

Play freeSource code: Patreon

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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.