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

Frogger Traffic & Logs: Moving Obstacles and Platforms

By Wuidi · Tutorial · Part 2

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.

What You Will Learn

Step 1: Obstacle Objects

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();
Tip: Using pixel-based x positions (not grid columns) allows obstacles to move smoothly between cells. The width in cells determines how many grid squares the obstacle spans — 1 for cars, 2 for trucks.

Step 2: Moving Obstacles Each Frame

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.

Step 3: Screen Wrapping

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;
    }
  });
}
Tip: We check 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.

Step 4: Log Objects on Water

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.

Step 5: Frog Riding Logs

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)
}
Key insight: The frog rides the log by adding the log's speed to the frog's position each frame. This creates the illusion that the frog is standing on a moving platform. The fractional column value means the frog can be between grid cells while riding — this is intentional and makes the movement feel smooth.

Step 6: Multiple Lanes with Different Speeds

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.

Updated Game Loop

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);
Draw order matters: Lanes first (background), then logs (on water), then obstacles (on roads), then the frog on top. This ensures the frog is always visible and logs appear to float on the water surface.

What's Next?

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.

More Articles

💥
Tutorial Frogger Collision Detection: Cars, Water, and Safe Zones Tutorial · Apr 23, 2026 · 10:00 PM
❤️
Tutorial Frogger Scoring & Lives: Points, Timer, and Game Over Tutorial · Apr 23, 2026 · 11:00 PM
🐸
Tutorial How to Make a Frogger Game: Fundamentals for Beginners Tutorial · Apr 23, 2026 · 08:00 PM
← Back to Blog