A technique for automatically selecting the correct tile from a tilemap.
I originally wrote this as a reply to a question on TIGSource and I figure it’s worthy of expanding and repeating here.
The problem: We have generated a neat platformer level, and want to be able to automatically lay neighbour-sensitive tiles over it so that they fit properly.
Neighbour Sensitive Tiles
Super Mario’s tiles are not neighbour sensitive; the stone block always looks the same if it’s floating alone or if it’s part of a wall.
That’s fine for many games, but can look out of place if you’re trying to create a more organic feel in a level, or just wanting to cut down on repeated patterns. Neighbour sensitive tiles overcome this by matching their appearance to the presence of their neighbours.
A 1-bit Map
Let’s imagine that with some clever techniques we have generated a level layout for a platformer game that consists of either stone or empty air at each grid location. This can be represented as a 1-bit image – that is each pixel’s state will be determined by a single bit, so either on indicating rock or off to indicate open air. Here’s a section of such a map, greatly enlarged and with grid lines added:
A tile set is simply a collection of the graphics that may be used to fill in a map. Mario’s tileset is quite small, consisting of just a few different types of block and scenery, whilst ours will have many graphics for the same type of tile:
To determine which tile should go at each spot on the map, we shall examine that spot’s immediate neighbours (for now we’ll ignore diagonal neighbours.) Rather than writing a huge if/else-if statement to handle all the possible combinations of neighbours, we will use a system that assigns values to each direction.
A value is found for each location by looking at its neighbours and adding up the values for whichever ones have stone present. So if the spot we’re examining has a neighbour above it that is also filled with stone then it is given the value 1. If it has stone-filled neighbours above and below then the value is 1 + 4, so 5.
You may notice that the values assigned to the directions are the same as the values of the positions in a binary number and this is no surprise; both are ways of representing possible combinations of 4 units which can each be in an on or off (stone or air) state.
Here is the map segment with the value of each tile filled in. You might like to manually work out the value for a couple of the tiles yourself so you’re clear on how it works.
Adding the Tiles
The order in which the tileset is laid out above is not arbitrary; it is laid out so that each tile matches what would be expected of a tile in the map that would be assigned that value. Once we have a value assigned for each spot on the map we just need to look up that number in the tileset and place the correct tile there:
Taking it Further
Part One – Removing Air
Whilst the above works very neatly for platforms floating in air, it doesn’t yet fully handle two tile types.
Instead of a platformer imagine we’re working on a top-down 2D RTS with two tile types of grass and water. In such a case we would expect every spot on the map to have a tile graphic present; there’s no open spaces like in our platformer. This means that every spot on the map will need to have a value generated from its neighbours to determine which tile fits there.
We can use exactly the same neighbour valuing system as before, but we now need a way to differentiate between there being grass or water at the spot being examined itself. This is actually very simply done – just add an extra value for the location itself, following the same 2 to the power of n pattern from the other values:
Let’s decide that the presence of water means you do add on that position’s value, whilst the presence of grass means you do not. So a grass tile surrounded on all sides by grass would be numbered at 0. A grass tile with water to the top and left would be 1 + 8 = 9. A water tile surrounded on all sides by grass would be 16. A water tile with water on all sides would be 1 + 2 + 4 + 8 + 16 = 31
Part Two – Greater Variety
How do we handle adding additional types of terrain?
Let’s say we have three types of terrain in our top-down game: water, grass, and forest. As well as water and grass meeting, we must now also deal with water and forest meeting as well as grass and forest meeting.
Previously there was two possibilities for each neighbour position (grass or water) so we used a binary counting system. There’s now three possibilities so we shall use a trinary counting system. Our neighbour valuing system needs to be adjusted to match the new counting system:
Just as the binary counting system followed the pattern of 2 to the power of n, this follows the pattern of 3 to the power of n.
Using a trinary system each position has three possible states: grass, water, forest; or 0, 1, 2. When there is grass in a given position, we ignore the value (multiply it by 0). When there is water in the position we add on the value given (multiply it by 1). When there is forest in the we add on twice the value (multiply it by 2.)
So if we had a tile that was forest with water above it, water to the right, forest below and grass to the left: 81 * 3 + 1 * 2 + 3 * 1 + 9 * 3 + 27 * 0 = 275
You might notice at this point that you’ll need to draw 324 tile graphics to cover all the possible combinations in a three-terrain map. This is a lot of tiles to draw by hand. I would strongly advise looking into at least partially automated methods of producing the multitude of tile combinations.
Of course the system can be extended in just the same way for greater numbers of terrain types, but the number of tile graphics you would need would be even more huge. Instead I would recommend placing some limits on what tiles are allowed to neighbour one-another. For instance if forest and water tiles are never allowed to directly touch then the above example will need several hundred less tile graphics.