Dungeon Tile Example

A practical example of dynamic tile selection.

Click in the image above above to toggle where the tops of walls should be.

This demo uses tile graphics created by Runar Heyer and released under the Creative Commons BY-NC-SA license. The Javascript source is available. The original tileset graphics have been modified slightly, with the modified version available at the bottom of this article. In accordance with the Share-Alike terms, the demo is available under the same Creative Commons BY-NC-SA license.

This is a supplement to my past tutorial on automatic tile selection so it's best you read that first.

Practicalities

As is often the case, the neat theory doesn't apply quite so easily to real world situations. The technique presented in my original article works beautifully with a tileset that's designed in a particular way, but takes some adjustment to work with others. Let's go through the process of automatically applying this dungeon tileset from Runar Heyer to a generated or dynamic map.

Wall of two parts

You'll notice the walls in this tileset are made of two separate tiles, one for the side and one for the top. Every wallTop has either another wallTop or a wallSide directly below it. We enforce this in code by defining the map firstly by the position of wallTops, then spawning wallSides in empty tiles below them.

Both wallTop and wallSide are neighbour-aware tiles that appear differently depending on what they're surrounded by.

WallTop

The wallTop tile can connect to other wallTop tiles in any of four directions. This both looks nice and emphasises the contiguity of the wall from the point of view of game mechanics. So we apply the standard techique of assigning powers of two to each direction to calculate the tile index that should be displayed:

function calcTileIndexFull(
  isAboveSame, isLeftSame,
  isBelowSame, isRightSame) {
    var sum = 0;
    if (isAboveSame) sum += 1;
    if (isLeftSame)  sum += 2;
    if (isBelowSame) sum += 4;
    if (isRightSame) sum += 8;
    return sum;
}

This works fine, except for one situation due to how the tiles are designed. If a wallTop has connections in every direction it is drawn as if it's part of a + shaped arrangement, but that may not be the case.

pixel art dungeon walls arranged in a plus sign shape pixel art dungeon walls arranged in a square block

We could detect this situation by adding awareness of diagonal neighbours, but that would mean a massive increase of tile variations from 24 = 16 to 28 = 256. For the sake of this demo I've just designed the map so that walls are never in blocks like that. Other solutions include adding sub-tile graphics that deal with just the corners.

WallSide

Because only one side of the walls are ever visible, wallSide needs far fewer variations than wallTop. Looking closely you can see that the wallSides that are exposed to open air have slightly lightened edges to the bricks. It's a subtle effect but adds a lot to the feeling of the finished dungeon looking like a solid object and not just a series of tiles. Because we only have two neighbours to consider for wallSide, there only need to be 22 = 4 variations.

function calcTileIndexSide(
  isLeftSame,
  isRightSame) {
    var sum = 0;
    if (isLeftSame) sum += 1;
    if (isRightSame) sum += 2;
    return sum;
}

Random Variation

In addition to the neighbour aware variations, this tileset also includes several versions of some tiles that can be used in any situation to give some visual variety. A prime example is the floor tiles that don't have any neighbour aware variations but have many versions of plain rock and grass that can be placed anywhere that calls for a floor tile.

You can pick these random variations using a simple var variation = Math.floor(Math.random() * VARIATION_COUNT); But in the case of a dynamic map you probably don't want them to change every time that tile needs to be calculated. To correct that simply generate a Math.random() value once and store it for each tile position. (You could also do some simple hashing to generate a variation value from the tile's position if you're really keen to save memory.)

Tile Picking

Putting that all together we get the code to generate a tileIndex from our map.

if (typeIndex === WALLTOP) {
  tileIndex = calcTileIndexFull(
    tileMap.isWallTop(x, y - 1),
    tileMap.isWallTop(x - 1, y),
    tileMap.isWallTop(x, y + 1),
    tileMap.isWallTop(x + 1, y)
  );
}
else if (typeIndex == WALLSIDE) {
  tileIndex = calcTileIndexSide(
    tileMap.isAnyWall(x - 1, y),
    tileMap.isAnyWall(x + 1, y)
  );
  if (tileIndex === 3) {
    // we have multiple versions of WALLSIDE 3, pick one
    tileIndex += Math.floor(randomVariation * WALLSIDE_FLAT_VARIATION_COUNT);
  }
}
else {
  // there are multiple versions of FLOOR tiles, pick one
  tileIndex = Math.floor(randomVariation * FLOOR_VARIATION_COUNT);
}

To draw the tile we use the same function as in the original article, but with the y source position decided by the typeIndex which simply selects which row from the tilesheet we want to pick from.

function draw(context, typeIndex, tileIndex, x, y) {
  xStart = tileIndex * TILESIZE;
  yStart = typeIndex * TILESIZE;
  context.drawImage(sheetImage,
    xStart, yStart,
    TILESIZE, TILESIZE,
    x, y,
    TILESIZE, TILESIZE
  );
}
a pixel art tilesheet, giving versions of dungeon walls and floors