← Back to Blog Apr 23, 2026 · 10:00 PM · 7 min read

Frogger Collision Detection: Cars, Water, and Safe Zones

By Wuidi · Tutorial · Part 3

In Part 2, we added moving traffic and floating logs. The frog can ride logs across water, but nothing happens when it hits a car or falls into the river. Now we add collision detection — the rules that make Frogger a real game. Hit a car? Dead. In the water without a log? Drowned. Reach the top row? Score!

What You Will Learn

Step 1: AABB Collision Detection

AABB stands for Axis-Aligned Bounding Box. It is the simplest and most common collision detection method for 2D games. Two rectangles overlap if and only if they overlap on both the X and Y axes simultaneously.

function rectsOverlap(ax, ay, aw, ah,
                      bx, by, bw, bh) {
  return ax < bx + bw &&
         ax + aw > bx &&
         ay < by + bh &&
         ay + ah > by;
}

// Frog hitbox (slightly smaller than cell)
function getFrogRect() {
  return {
    x: frog.col * CELL + 4,
    y: frog.row * CELL + 4,
    w: CELL - 8,
    h: CELL - 8
  };
}
Tip: We shrink the frog's hitbox by 4px on each side. This makes collisions feel fair — the frog can graze past a car without dying. Players hate pixel-perfect collisions that feel unfair, so a smaller hitbox is standard practice.

Step 2: Car Collision (Death by Traffic)

Each frame, check if the frog overlaps any obstacle on road lanes. If it does, the frog dies. We loop through all obstacles and test the frog's rectangle against each one.

function checkCarCollision() {
  const fr = getFrogRect();

  for (let i = 0; i < obstacles.length; i++) {
    const ob = obstacles[i];
    const ox = ob.x + 2;
    const oy = ob.row * CELL + 4;
    const ow = ob.w * CELL - 4;
    const oh = CELL - 8;

    if (rectsOverlap(fr.x, fr.y, fr.w, fr.h,
                     ox, oy, ow, oh)) {
      return true;  // hit a car
    }
  }
  return false;
}

The obstacle hitbox is also slightly smaller than its visual size. This keeps collisions consistent — both the frog and the cars have forgiving hitboxes.

Step 3: Water Detection (Drowning)

Water lanes are the opposite of road lanes. On a road, touching an obstacle kills you. On water, not touching a log kills you. If the frog is on a water row and not overlapping any log, it drowns.

function checkWaterDeath() {
  const lane = lanes[frog.row];
  if (lane.type !== 'water') return false;

  const fr = getFrogRect();
  let onLog = false;

  for (let i = 0; i < logs.length; i++) {
    const log = logs[i];
    if (log.row !== frog.row) continue;

    const lx = log.x + 2;
    const ly = log.row * CELL + 4;
    const lw = log.w * CELL - 4;
    const lh = CELL - 8;

    if (rectsOverlap(fr.x, fr.y, fr.w, fr.h,
                     lx, ly, lw, lh)) {
      onLog = true;
      break;
    }
  }

  return !onLog;  // true = drowning
}
Key insight: Road lanes and water lanes use opposite logic. On roads: overlap with obstacle = death. On water: NO overlap with log = death. This inversion is what makes Frogger's two halves feel different — the bottom half is about dodging, the top half is about catching.

Step 4: Safe Zone Detection

Safe zones (grass lanes) are simple — the frog is always safe there. We use this check to skip collision detection entirely on safe rows, which also serves as a small optimization.

function isSafeZone() {
  const lane = lanes[frog.row];
  return lane.type === 'safe';
}

function checkAllCollisions() {
  // Safe zones — no danger
  if (isSafeZone()) return 'safe';

  // Goal row — frog reached the top
  if (lanes[frog.row].type === 'goal') return 'goal';

  // Road lanes — check car collision
  if (lanes[frog.row].type === 'road') {
    if (checkCarCollision()) return 'hit';
  }

  // Water lanes — check if on a log
  if (lanes[frog.row].type === 'water') {
    if (checkWaterDeath()) return 'drown';
  }

  return 'safe';
}

The checkAllCollisions() function returns a string describing what happened. This makes it easy to handle each case differently in the game loop — play different sounds, show different animations, or award points.

Step 5: Goal Zone Detection

When the frog reaches the top row (the goal), the player scores. We reset the frog to the starting position and track which goal slots have been filled. In classic Frogger, you need to fill all 5 goal slots to complete a level.

const goalSlots = [1, 4, 7, 10]; // columns with lily pads
let filledGoals = [];

function checkGoal() {
  if (lanes[frog.row].type !== 'goal') return false;

  // Check if frog landed on a lily pad slot
  const col = Math.round(frog.col);
  const slotIndex = goalSlots.indexOf(col);

  if (slotIndex !== -1 &&
      !filledGoals.includes(col)) {
    filledGoals.push(col);
    return true;  // goal scored
  }

  // Missed the lily pad — treat as death
  return false;
}

function drawFilledGoals() {
  filledGoals.forEach(col => {
    const x = col * CELL;
    const y = 0;
    ctx.fillStyle = '#2ecc40';
    ctx.fillRect(x + 4, y + 4, CELL - 8, CELL - 8);

    // Small frog icon
    ctx.fillStyle = '#fff';
    ctx.font = '16px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('🐸', x + CELL / 2, y + CELL / 2 + 5);
  });
}
Tip: In classic Frogger, the frog must land precisely on a lily pad in the goal row. Landing between pads counts as a miss. Using Math.round(frog.col) snaps the frog to the nearest column, then we check if that column has a lily pad.

Step 6: Death Animation

When the frog dies, we show a brief animation — a red flash and shrinking effect — before resetting. This gives the player visual feedback that something went wrong.

let deathAnim = null;

function startDeath(type) {
  deathAnim = {
    type: type,       // 'hit' or 'drown'
    x: frog.col * CELL,
    y: frog.row * CELL,
    frame: 0,
    maxFrames: 30     // ~0.5 seconds at 60fps
  };
}

function updateDeathAnim() {
  if (!deathAnim) return false;

  deathAnim.frame++;
  if (deathAnim.frame >= deathAnim.maxFrames) {
    deathAnim = null;
    resetFrog();
    return false;
  }
  return true;  // still animating
}

function drawDeathAnim() {
  if (!deathAnim) return;

  const progress = deathAnim.frame /
                   deathAnim.maxFrames;
  const size = CELL * (1 - progress);
  const offset = (CELL - size) / 2;

  if (deathAnim.type === 'hit') {
    // Red flash for car hit
    ctx.fillStyle = `rgba(231, 76, 60,
      ${1 - progress})`;
  } else {
    // Blue ripple for drowning
    ctx.fillStyle = `rgba(41, 128, 185,
      ${1 - progress})`;
  }

  ctx.fillRect(
    deathAnim.x + offset,
    deathAnim.y + offset,
    size, size
  );
}

function resetFrog() {
  frog.col = Math.floor(COLS / 2);
  frog.row = ROWS - 1;
}

The death animation shrinks a colored square over 30 frames. Red for car hits, blue for drowning. Once the animation finishes, the frog resets to the starting position. During the animation, input is ignored so the player cannot move a dead frog.

Updated Game Loop

Here is the game loop with all collision checks integrated:

function gameLoop(timestamp) {
  const delta = timestamp - lastTime;
  lastTime = timestamp;

  // If death animation is playing, just animate
  if (updateDeathAnim()) {
    ctx.clearRect(0, 0, WIDTH, HEIGHT);
    drawLanes();
    drawLaneDetails();
    drawLogs();
    drawObstacles();
    drawFilledGoals();
    drawDeathAnim();
    requestAnimationFrame(gameLoop);
    return;
  }

  // Update positions
  update();

  // Check collisions
  const result = checkAllCollisions();
  if (result === 'hit' || result === 'drown') {
    startDeath(result);
  } else if (result === 'goal') {
    if (checkGoal()) {
      // Score! Reset frog for next goal
      resetFrog();
    } else {
      startDeath('drown'); // missed lily pad
    }
  }

  // Draw everything
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
  drawLanes();
  drawLaneDetails();
  drawLogs();
  drawObstacles();
  drawFilledGoals();
  drawFrog();

  requestAnimationFrame(gameLoop);
}
Collision check order matters: Check safe zones first (skip everything), then goal (award points), then road (car death), then water (drown). This prevents false positives — a frog on a safe zone should never trigger a water or road check.

What's Next?

The frog can now die from cars and water, and score by reaching goal slots. But there is no score counter, no lives system, and no game over screen. The game just keeps going forever.

Continue to Part 4: Scoring & Lives where we add points, a countdown timer, lives, and a game over screen.

More Articles

❤️
Tutorial Frogger Scoring & Lives: Points, Timer, and Game Over Tutorial · Apr 23, 2026 · 11:00 PM
📈
Tutorial Frogger Levels & Difficulty: Faster Traffic Each Level Tutorial · Apr 23, 2026 · 12:00 AM
🚗
Tutorial Frogger Traffic & Logs: Moving Obstacles and Platforms Tutorial · Apr 23, 2026 · 09:00 PM
← Back to Blog