In Part 1, we built the basic grid with colored lanes and a frog that moves one tile per key press. Now we bring the game to life with moving obstacles — cars and trucks on the roads, and floating logs on the water. The frog must dodge traffic and ride logs to cross safely.
Each obstacle (car or truck) is an object with a pixel x position, a row, a speed, a width (in cells), and a direction. We store them in an array and define them per lane.
// Obstacle: { x, row, speed, width, dir, color }
// x is in pixels, width is in cells
// dir: 1 = moving right, -1 = moving left
const obstacles = [];
function createObstacles() {
// Row 7: cars moving right
obstacles.push(
{ x: 0, row: 7, speed: 1.5, w: 1, dir: 1,
color: '#e74c3c' },
{ x: 200, row: 7, speed: 1.5, w: 1, dir: 1,
color: '#e74c3c' },
{ x: 400, row: 7, speed: 1.5, w: 1, dir: 1,
color: '#e74c3c' }
);
// Row 8: trucks moving left
obstacles.push(
{ x: 80, row: 8, speed: 1.0, w: 2, dir: -1,
color: '#8e44ad' },
{ x: 320, row: 8, speed: 1.0, w: 2, dir: -1,
color: '#8e44ad' }
);
// Row 9: fast cars moving right
obstacles.push(
{ x: 0, row: 9, speed: 2.5, w: 1, dir: 1,
color: '#f39c12' },
{ x: 260, row: 9, speed: 2.5, w: 1, dir: 1,
color: '#f39c12' }
);
// Row 10: trucks moving left
obstacles.push(
{ x: 40, row: 10, speed: 1.2, w: 2, dir: -1,
color: '#2c3e50' },
{ x: 280, row: 10, speed: 1.2, w: 2, dir: -1,
color: '#2c3e50' }
);
// Row 11: cars moving right
obstacles.push(
{ x: 100, row: 11, speed: 1.8, w: 1, dir: 1,
color: '#e67e22' },
{ x: 300, row: 11, speed: 1.8, w: 1, dir: 1,
color: '#e67e22' },
{ x: 480, row: 11, speed: 1.8, w: 1, dir: 1,
color: '#e67e22' }
);
}
createObstacles();
Each frame, update every obstacle's x position by adding its speed multiplied by its direction. This creates smooth, continuous movement across the screen.
function updateObstacles() {
obstacles.forEach(ob => {
ob.x += ob.speed * ob.dir;
});
}
function drawObstacles() {
obstacles.forEach(ob => {
const y = ob.row * CELL;
const w = ob.w * CELL;
// Vehicle body
ctx.fillStyle = ob.color;
ctx.fillRect(ob.x + 2, y + 4, w - 4, CELL - 8);
// Windshield
ctx.fillStyle = 'rgba(255,255,255,0.3)';
if (ob.dir === 1) {
ctx.fillRect(ob.x + w - 14, y + 8, 10, CELL - 16);
} else {
ctx.fillRect(ob.x + 4, y + 8, 10, CELL - 16);
}
});
}
The windshield is drawn on the leading edge of the vehicle based on its direction. This small detail makes it easy to see which way traffic is flowing.
When an obstacle moves completely off one side of the screen, it reappears on the opposite side. This creates an endless stream of traffic without needing to spawn new objects.
function wrapObstacles() {
obstacles.forEach(ob => {
const w = ob.w * CELL;
if (ob.dir === 1 && ob.x > WIDTH) {
// Moving right, went off right edge
ob.x = -w;
} else if (ob.dir === -1 && ob.x + w < 0) {
// Moving left, went off left edge
ob.x = WIDTH;
}
});
}
ob.x > WIDTH (not >= WIDTH) so the obstacle fully disappears before wrapping. The new position is set to -w so it slides in smoothly from the opposite edge.
Logs work exactly like obstacles but on water lanes. The key difference: the frog can ride logs. We store them in a separate array so we can check for log collisions differently than car collisions.
const logs = [];
function createLogs() {
// Row 1: short logs moving left
logs.push(
{ x: 0, row: 1, speed: 1.0, w: 2, dir: -1 },
{ x: 240, row: 1, speed: 1.0, w: 2, dir: -1 },
{ x: 440, row: 1, speed: 1.0, w: 2, dir: -1 }
);
// Row 2: long logs moving right
logs.push(
{ x: 40, row: 2, speed: 0.8, w: 3, dir: 1 },
{ x: 320, row: 2, speed: 0.8, w: 3, dir: 1 }
);
// Row 3: short logs moving left
logs.push(
{ x: 80, row: 3, speed: 1.3, w: 2, dir: -1 },
{ x: 280, row: 3, speed: 1.3, w: 2, dir: -1 },
{ x: 460, row: 3, speed: 1.3, w: 2, dir: -1 }
);
// Row 4: long logs moving right
logs.push(
{ x: 0, row: 4, speed: 0.6, w: 4, dir: 1 },
{ x: 300, row: 4, speed: 0.6, w: 4, dir: 1 }
);
// Row 5: medium logs moving left
logs.push(
{ x: 60, row: 5, speed: 1.1, w: 2, dir: -1 },
{ x: 260, row: 5, speed: 1.1, w: 2, dir: -1 },
{ x: 420, row: 5, speed: 1.1, w: 2, dir: -1 }
);
}
createLogs();
function drawLogs() {
logs.forEach(log => {
const y = log.row * CELL;
const w = log.w * CELL;
// Log body (brown)
ctx.fillStyle = '#8B4513';
ctx.fillRect(log.x + 2, y + 6, w - 4, CELL - 12);
// Wood grain lines
ctx.strokeStyle = '#6b3410';
ctx.lineWidth = 1;
for (let i = 1; i < log.w; i++) {
const lx = log.x + i * CELL;
ctx.beginPath();
ctx.moveTo(lx, y + 8);
ctx.lineTo(lx, y + CELL - 8);
ctx.stroke();
}
});
}
Logs are drawn as brown rectangles with vertical grain lines at each cell boundary. The visual style makes them clearly distinct from the blue water background.
This is the core Frogger mechanic. When the frog is on a water lane and standing on a log, it moves with the log. We convert the frog's grid position to pixels, check for overlap with each log, and shift the frog's pixel position accordingly.
function updateFrogOnLog() {
const lane = lanes[frog.row];
if (lane.type !== 'water') return;
// Frog hitbox in pixels
const fx = frog.col * CELL;
const fy = frog.row * CELL;
let onLog = false;
logs.forEach(log => {
if (log.row !== frog.row) return;
const lx = log.x;
const lw = log.w * CELL;
// Check overlap
if (fx + CELL > lx && fx < lx + lw) {
// Frog is on this log — move with it
frog.col += log.speed * log.dir / CELL;
onLog = true;
}
});
// Clamp frog to screen bounds
if (frog.col < 0) frog.col = 0;
if (frog.col > COLS - 1) frog.col = COLS - 1;
// If on water but not on a log = drown
// (we will handle this in Part 3)
}
The game gets interesting when each lane has a different speed and direction. Alternating directions forces the player to think about timing. Varying speeds create gaps of different sizes.
// Update both obstacles and logs each frame
function update() {
// Move all obstacles
obstacles.forEach(ob => {
ob.x += ob.speed * ob.dir;
});
wrapObstacles();
// Move all logs
logs.forEach(log => {
log.x += log.speed * log.dir;
});
wrapLogs();
// Frog rides log if on water
updateFrogOnLog();
}
function wrapLogs() {
logs.forEach(log => {
const w = log.w * CELL;
if (log.dir === 1 && log.x > WIDTH) {
log.x = -w;
} else if (log.dir === -1 && log.x + w < 0) {
log.x = WIDTH;
}
});
}
The update function handles all movement in one place. Obstacles and logs use the same wrapping logic. The frog-on-log check runs after logs move so the frog position stays in sync.
Here is the complete game loop with obstacles and logs integrated:
function gameLoop(timestamp) {
const delta = timestamp - lastTime;
lastTime = timestamp;
// Update positions
update();
// Draw everything in order
ctx.clearRect(0, 0, WIDTH, HEIGHT);
drawLanes();
drawLaneDetails();
drawLogs(); // logs on water
drawObstacles(); // cars on roads
drawFrog(); // frog on top
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
We now have a Frogger game with moving traffic and floating logs. The frog can ride logs across water lanes. But nothing happens when the frog hits a car or falls in the water — there are no consequences yet.
Continue to Part 3: Collision Detection where we add car collisions, drowning, and goal zone detection.