Neighbour Aware Tile Selection

Automatically select the right tile from a tilesheet by examining neighbours.

Creating an attractive scene from tiles typically requires placing the tiles so that they join together with their neighbours. Compare these two simple platforms and see how the bottom version looks generally nicer thanks to tiles that are aware of and blend with their neighbours. This is easy enough to do if you're manually placing tiles, but if you're working with procedurally generated or dynamic map data you're going to need some programming to handle it. Here's a technique for doing that in a neat way.

A Rocky Example

We'll be using parts of a tileset by Agnes Heyer (accessed via this forum post.) I've only used a small part of the full set of tiles and other graphics they have released; the full thing is beautiful. The tileset is available under a Creative Commons BY-NC-SA license.

These 16 tile variations are designed to join to other tiles directly above, below, to the left, and to the right of them. This means each tile will need to check its four direct neighbours to know what version it should appear as. This can be done with a towering if statement, but there is a far neater way.

function giveTileIndex(above, below, left, right):Int { if (!above && !below && !left && !right) { return 0; } else if (above && !below && !left && !right) { return 1; } else if (!above && below && !left && !right) { return 4; } else if (!above && !below && left && !right) { return 2; } else if (!above && !below && !left && right) { return 8; } else if (above && below && !left && !right) { return 5; } // ... and so on. Fortunately there's a better way }

Assigning Numbers

There are 4 directions to consider, each with 2 possible states (either there's a tile to join with, or there isn't). That means there are 24 = 16 combinations. Which is a good thing because that's how many tile variations we have.

We can generate a number for each possible combination of neighbours. Variation 0 will be when there's no neighbours. If the above neighbour is present we add 1, for the left 2, for the below 4, and for the right we add 8. Why these numbers? You'll notice they're powers of two:

This means we can selectively add them to make every integer from 0 to 15. 0 when none are added, 15 when they're all added, and everything in between by adding some combination. This will all be sounding familiar if you've dealt with binary numbers before.

The variation number can be calculated simply by adding up the values assigned to each side that has a matching tile. Try clicking on the example below to change the neighbours of this tile.

tileIndex = 4 + 8 = 12

The function to calculate the the variation number is nice and simple.

function calculateTileIndex(above, below, left, right) { var sum = 0; if (above) sum += 1; if (left) sum += 2; if (below) sum += 4; if (right) sum += 8; return sum; }

Once we know what which tile variation we want to draw, it's a simple case of selecting it from the tilesheet where the variations are arranged in order and drawing it out.

function drawTile(context, tileIndex, x, y) { xStart = tileIndex * xTileSize; context.drawImage(sheetImage, // source rectangle xStart, 0, xTileSize, yTileSize, // destination x, y, xTileSize, yTileSize ); };

Full Demo

Click in the canvas above to toggle each grid cell and see the tile graphics get updated.>

Thanks again to Agnes Heyer for creating the tile images used throughout this article. The Javascript source for both this and the single tile demo is available. In accordance to the Share-Alike part of the Creative Commons BY-NC-SA license, these demos are released under the same license.

Going Further

This is great for maps that are just made up of a single tile type, but what about more complex situations? You can extend this technique directly to handle 3 possibilities for each neighbour but then you need a tileset of 34 = 81 tiles. That is a lot of tiles to hand design, and most games are going to have more than 3 tile types meaning you soon need absurdly large numbers of tile variations. Instead I've found it useful to limit what combinations are accounted for. You might have wall tiles that should join to other nearby walls, but ignore all other types of tile. Or you can compose tiles with a tiles that merge with transparent empty space so they can be placed over any other tile in the game and give a reasonable result. In summary, don't think of this as a magical technique that solves handling tiles in any game but it is a useful building block.

I've often used this technique outside of tiles altogether. Needing to respond to arrangements and neighbours is a fairly common problem throughout many areas of game development, from finite state automata to marching cube algorithms. Whenever you find yourself writing a long sequence of if statements look for the opportunity to generate unique values representing each possibility and often something useful will come up.

You can see how this technique can be applied to something closer to a real game example in this worked example using a dungeon tileset.

Terrible Bonus!

For Javascript (and other languages that have boolean values considered as 0 and 1 for arithmetric) you could make a version of the calculateTileIndex function that skips the conditionals. It'll be harder to read, and any performance gain from avoiding branching is likely lost from whatever Javascript does when casting values. The mildly absurd "+!!" unary operator series casts the parameters to boolean then to a number which will always be either 0 or 1.

function calculateTileIndex(above, below, left, right) { return +!!above * 1 + +!!left * 2 + +!!below * 4 + +!!right * 8; }

Anonymous

Thanks for this tutorial, it was very helpful!

Anonymous

Thank you!

Comments are plaintext only and may be ruthlessly moderated.