Square tilemaps implementation: Scrolling maps

This article covers how to implement scrolling square tilemaps using the Canvas API.

Note: When writing this article, we assumed previous reader knowledge of canvas basics such as how get a 2D canvas context, load images, etc., which is all explained in the Canvas API tutorial, as well as the basic information included in our Tilemaps introduction article. This article also builds upon implementing static square tilemaps — you should read that too if you've not done so already.

The camera

The camera is an object that holds information about which section of the game world or level is currently being shown. Cameras can either be free-form, controlled by the player (such as in strategy games) or follow an object (such as the main character in platform games.)

Regardless of the type of camera, we would always need information regarding its current position, viewport size, etc. In the demo provided along with this article, these are the parameters the camera has:

Rendering the map

There are two main differences between rendering scrolling maps vs. static maps:

To handle these issues, we need to slightly modify the rendering algorithm. Let's imagine that we have the camera pointing at (5,10). That means that the first tile would be 0x0. In the demo code, the starting point is stored at startCol and startRow. It's convenient to also pre-calculate the last tile to be rendered.

js
const startCol = Math.floor(this.camera.x / map.tsize);
const endCol = startCol + this.camera.width / map.tsize;
const startRow = Math.floor(this.camera.y / map.tsize);
const endRow = startRow + this.camera.height / map.tsize;

Once we have the first tile, we need to calculate how much its rendering (and therefore the rendering of the other tiles) is offset by. Since the camera is pointing at (5, 10), we know that the first tile should be shifted by (-5,-10) pixels. In our demo the shifting amount is stored in the offsetX and offsetY variables.

js
const offsetX = -this.camera.x + startCol * map.tsize;
const offsetY = -this.camera.y + startRow * map.tsize;

With these values in place, the loop that renders the map is quite similar to the one used for rendering static tilemaps. The main difference is that we are adding the offsetX and offsetY values to the target x and y coordinates, and these values are rounded, to avoid artifacts that would result from the camera pointing at positions with floating point numbers.

js
for (let c = startCol; c <= endCol; c++) {
  for (let r = startRow; r <= endRow; r++) {
    const tile = map.getTile(c, r);
    const x = (c - startCol) * map.tsize + offsetX;
    const y = (r - startRow) * map.tsize + offsetY;
    if (tile !== 0) {
      // 0 => empty tile
      this.ctx.drawImage(
        this.tileAtlas, // image
        (tile - 1) * map.tsize, // source x
        0, // source y
        map.tsize, // source width
        map.tsize, // source height
        Math.round(x), // target x
        Math.round(y), // target y
        map.tsize, // target width
        map.tsize, // target height
      );
    }
  }
}

Demo

Our scrolling tilemap implementation demo pulls the above code together to show what an implementation of this map looks like. You can take a look at a live demo, and see its source code.

Animated gif of a section grass, dirt areas, and trees made from repeated sections of a tilemap showing how you see different sections of the area when you scroll.

There's another demo available, that shows how to make the camera follow a character.