Tiling tutorial

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

The 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.

Bitwise operations in Lua

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:
local bit = 0
-- North
if map[x][y + 1] == 1 then
  bit = bit + 1 -- 2^0
-- East
if map[x + 1][y] == 1 then
  bit = bit + 2 -- 2^1
-- South
if map[x][y - 1] == 1 then
  bit = bit + 4 -- 2^2
-- West
if map[x - 1][y] == 1 then
  bit = bit + 8 -- 2^3

Considering that our map contains either 0's and 1's it is possible to simplify the code above to:

local bit = 0
bit = bit + 1 * map[x][y + 1] -- North
bit = bit + 2 * map[x + 1][y] -- East
bit = bit + 4 * map[x][y - 1] -- South
bit = bit + 8 * map[x - 1][y] -- West

What these code examples show 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

Limiting the number of transitions

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.

More than four neighbours

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)

Sloping terrain

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