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!
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
};
}
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.
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
}
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.
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);
});
}
Math.round(frog.col) snaps the frog to the nearest column, then we check if that column has a lily pad.
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.
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);
}
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.