Tilemaps Tutorial

Introduction

In this tutorial we are going to look at one approach for handling tilemaps.

Tilesheet

For the technique to work, we need a tilesheet with 16 different tiles. Each tile represents a transition depending on its adjacent tiles. For a simple platformer game, each tile has four neighbours: to the left, top, right and bottom. Each map cell could be either a platform or not so the total number of transitions is 16 (2^4 = 16 tiles).

Tilesheet numbered using binary where the first bit represents North, the second bit is East, the third bit is South and the fourth bit is West. For example, a tile that has neighbours to the North and South would have a binary value of 0101.

Maps

Let's say that we have a level map containing either 1's and 0's, where 1 represents a platform and 0 represents an empty cell. We have to process each cell on the map individually and consider its 4 adjacent cells. Using some simple bitwise operations we can figure out which tile from the tilesheet should be drawn in that cell. Unfortunately, Lua does not have bitwise operators so we have to improvise.

function draw(map)
  for x = 1, map.width do
    for y = 1, map.height do
      if map[x][y] > 0 then
        -- todo: check if map[x][y] is out of bounds
        local n = 0
        n = n + map[x][y - 1] -- North
        n = n + 2*map[x + 1][y] -- East
        n = n + 4*map[x][y + 1] -- South
        n = n + 8*map[x - 1][y] -- West
        -- todo: draw tile number n at x,y
      end
    end
  end
end

Once we find the value of "n" for each cell, we can draw the respective tile for that cell. Please note that we also have to check if map[x][y] remains inside the bounds of our map.


Interactive auto-tiling demonstration

What this example shows is how to pack information about the adjacent cells into an integer. The first bit represents North, the second bit represents West and so forth. The resulting "bit" variable has a value between 0 and 15 representing a tile from the tilesheet.

Once we know the "bit" value of each cell, it's not hard to check if its neighbours are non-empty:

local north = bit%2 >= 1
local east = bit%4 >= 2
local south = bit%8 >= 4
local west = bit%16 >= 8

Terrain

In our tutorial, each tile had 2 possible states. Suppose that we want to render more complicated terrain that has 3 possible states (plains, grassland and water). This is fine, however the number of tile transitions increases to 81 (3^4 = 81 tiles).

Example isometric set with 3 possible states for each tile (plains, grassland, water) 3^4 = 81 tiles

What if that we wanted to add two more states: tundra and desert. 5^4 = 625 tiles! As you can see, things can get out of hand quickly as the number of possible states increases. One solution could be to limit the number of transitions. For example, you can make sure that your editor/terrain generation script does not produce maps that contain unlikely transitions like "tundra to desert" or "desert" to "water". Removing unnecessary transitions could greatly reduce the total number of required tiles.

Often, it's necessary to have several layers of tilemaps. We start with the "terrain" layer containing the plains/grassland/water transitions. On top of it, we can render another layer with roads.
So far, we only considered 4 adjacent cells for each tile. If we were to include diagonal neighbour cells, we would need an even bigger tileset.

Example set with 2 possible states for each tile and 8 neighbours 2^8 = 256 tiles

A slightly more generalized version of the previous algorithm:

local dirs = { {0,1}, {1,1}, {1,0}, {1,-1}, {0,-1}, {-1,-1}, {-1,0}, {-1,1} }
local bit = 0
for i, v in ipairs(dirs) do
  local x2 = x + v[1]
  local y2 = y + v[2]
  if map[x2][y2] == 1 then
    bit = bit + 2^(i - 1)
  end
end

Using the above technique makes it possible to produce sloping terrain (like in Sim City 2000). If you are working with a heightmap, one limitation to consider is that each slope variance means another tile state. So, you may have to "smooth" or "blur" your heightmap to avoid very steep transitions that are not contained in your tilesheet.

Simple sloped isometric tilesheet