More Scrolling
The camera from the previous tutorial has one flaw: walk to the edge of the map and the camera keeps going, revealing the empty void beyond. Every great platformer stops scrolling at the map boundary so the world feels solid and complete. Let’s fix it!
Walk to a corner of the map - the camera stops cleanly at the edge and the hero keeps moving. That’s the clamp. Two lines added to updateCamera() and the void disappears.
HOW FAR IS TOO FAR? π
The camera position tells us where the top-left corner of the viewport sits in world space. The camera should never go negative (that would show left of the map) and it should never go so far right that the right edge of the viewport extends beyond the map.
Picture it: if the map is 600px wide and the viewport is 300px wide, the camera’s maximum x position is 600 - 300 = 300. Any further right and you’d be showing 300px of map followed by 300px of nothing.
camera min = 0
camera max = (map width in pixels) - (screen width in pixels)
The same logic applies vertically.
CLAMPING THE CAMERA ποΈ
Add two lines to updateCamera() after the smoothing, before you apply the position:
let camX = 0;
let camY = 0;
const SMOOTH = 0.12;
// Store map dimensions in pixels for easy reuse
const MAP_W = map[0].length * game.tileSize; // e.g. 20 tiles Γ 30px = 600px
const MAP_H = map.length * game.tileSize; // e.g. 14 tiles Γ 30px = 420px
function updateCamera() {
const targetX = player.x + player.width / 2 - SCREEN_W / 2;
const targetY = player.y + player.height / 2 - SCREEN_H / 2;
camX += (targetX - camX) * SMOOTH;
camY += (targetY - camY) * SMOOTH;
// Clamp: never show outside the map
camX = Math.max(0, Math.min(camX, MAP_W - SCREEN_W));
camY = Math.max(0, Math.min(camY, MAP_H - SCREEN_H));
world.x = -Math.round(camX);
world.y = -Math.round(camY);
}
Math.max(0, ...) prevents the camera going too far left or up. Math.min(..., MAP_W - SCREEN_W) prevents it going too far right or down. Together they lock the viewport inside the map at all times.
STARTING NEAR THE EDGE π
There’s one more edge case (pun intended): what if the hero starts near a corner of the map? If the map is only 8 tiles wide and the hero starts at tile 1, the camera would want to center on the hero - but that would put the left side of the viewport outside the map.
The clamp already handles this automatically! Just initialize camX and camY using the same formula you use every frame, and Math.max/min will catch it:
// Initialize camera before the first frame
// The clamp will handle edge cases automatically
camX = Math.max(0, Math.min(
player.x + player.width / 2 - SCREEN_W / 2,
MAP_W - SCREEN_W
));
camY = Math.max(0, Math.min(
player.y + player.height / 2 - SCREEN_H / 2,
MAP_H - SCREEN_H
));
This ensures the very first frame renders correctly, with no jump or pop as the camera corrects itself.
ONE-AXIS SCROLLING π
Some games only scroll horizontally (classic side-scrollers like the original Mario), others only vertically. You can easily restrict the camera to one axis:
function updateCamera() {
// Horizontal scroll only - vertical is fixed
const targetX = player.x + player.width / 2 - SCREEN_W / 2;
camX += (targetX - camX) * SMOOTH;
camX = Math.max(0, Math.min(camX, MAP_W - SCREEN_W));
world.x = -Math.round(camX);
world.y = 0; // Vertical position never changes
}
Or lock the horizontal axis for a vertical-only scroller:
function updateCamera() {
const targetY = player.y + player.height / 2 - SCREEN_H / 2;
camY += (targetY - camY) * SMOOTH;
camY = Math.max(0, Math.min(camY, MAP_H - SCREEN_H));
world.x = 0; // Horizontal position never changes
world.y = -Math.round(camY);
}
That’s the complete scrolling system! The full updateCamera function is only about 8 lines, but it handles worlds of any size, smooth following, and clean edge behavior.
What you’ve built:
- β World Container that decouples game logic from camera position
- β Smooth camera easing that follows the player
- β Clamped edges - no more void beyond the map
- β Correct initial camera position even when the hero starts near an edge
- β Single-axis scrolling for classic-style games
Next up: Your world has depth - now let’s render it that way. Next: Depth