Neighbour Aware Tile Selection

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

Two rows of pixel art tiles, one with simple repeating tiles, the other with tiles adjusted to match their 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

Example of pixel art tiles connected together

We'll be using parts of a tileset by Runar Heyer (accessed via this forum post.) I've only used a small part of the full set of tiles and other graphics he has 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.

a tileset giving 16 versions of a rock tile, numbered 0 to 15

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

Bitwise

Earlier I mentioned that we're using powers of two and that relationship to binary numbers. We've effectively been doing bitwise operations this whole time. This version of calculateTileIndex does the same as before but makes it more clear that we're messing around with bits rather than just adding up some numbers. I consider this bitwise version to be better code, but have used the addition version for the demo as I think it's friendlier to a broad audience.

// bitwise version
function calculateTileIndex(above, left, below, right) {
   var index = 0;
   if (above) index |= 1 << 0;
   if (left)  index |= 1 << 1;
   if (below) index |= 1 << 2;
   if (right) index |= 1 << 3;
   return index;
}

// using binary literals for best clarity, but doesn't work in older/bad browsers
function calculateTileIndex(above, left, below, right) {
   var index = 0;
   if (above) index |= 0b0001;
   if (left)  index |= 0b0010;
   if (below) index |= 0b0100;
   if (right) index |= 0b1000;
   return index;
}

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.