An alternative to tile bitmasking, a way to adapt an undetermined number of types

I recently read this excellent article on Tile Bitmasks, a simple and efficient way to adapt your tiles to their surroundings. For instance, having a “shore” tile as a transition between land and sea.

While this is a very good approach, I chose to make a small variation on that to allow me to to work with an undermined number of terrain types.

 

 

map1
This is the system I use since my previous article on level-editor

The reason behind this system is the way I develop. I often use assets that are free, from different sources. As a consequence, I often en up having inconsistent spritesheets. Some have tiles for each angles some are a single tile, and I want to support has many terrains as I want without modifying the system.

 

Advantages :

  • Allows different sizes of spritesheets
  • Can handle N types of terrain without changing the size of the bitmask

Drawbacks

  • Heavier on computation
  • O(2) implementation by default (ouch)

Enough talking now, let’s see the code !

Best match approach

To build up the system I used 2 main objects : Tile and TileContext.

Tile is the actual final object containing the image, the collision box and all. Implementation is very specific to one’s game.

TileContext is actually a simple array of neighbors TileTypes.

public class TileContext {
    /**
     * Adjacent types in clockwise order starting topleft
     * Index 4 is the current tile's type
     * 0 | 1 | 2
     * 3 | 4 | 5
     * 6 | 7 | 8
     */
    public TileDef.TileType[] adjacentTilesType = new TileDef.TileType[9];

    /**
     * Compares to TileDef contexts and score them. Returns adjacentTilesType.length if identical
     */
    public int compareScore(TileContext other) {
        int score = 0;

        for (int i = 0; i < adjacentTilesType.length; i++) {
            if (adjacentTilesType[i] == null// Null is a wildcard
                    || other.adjacentTilesType[i] == null) {
                continue;
            }

            if (adjacentTilesType[i] == other.adjacentTilesType[i]) {
                score++;
            } else {
                score--;
            }
        }
        return score;
    }
}

An enum called TileType stores all the available types of terrain I want to use is also needed.

public enum TileType {
        WATER,
        GRASS,
        HILL,
        HOUSE
    }

Finally a small public object to bind a Texture (or in my case, a TextureRegion) to a TileContext.

public class TextureRegionContext {
    public final TextureRegion region;
    public final TileContext context;

    public TextureRegionContext(TileContext context, TextureRegion region) {
        this.region = region;
        this.context = context;
    }
}

 

How does it work ?

If you took the time to read the code above you may have a clue of the way the system works, but let me explain it anyway.

Overview

Since we know have a context for each tile and each texture, it’s just a matter of matching them together. For any given tile :

  1. Loop over all the available textures for the given type
  2. Compare the Tile.TileContext to the Texture.TileContext
  3. Apply the texture being the best match

Scoring function

To compare them, the TileContext class has a  compareScore function, which returns a matching score. It contains a little trick that allows it to deal with the unknown types or else.

The rule of the score function are simple. For any neighbor :

  • If the type matches : earn one point.
  • If it’s not matching, loose a point.
  • If it’s null, skip it

And you as you may have guessed, the last rule is the most important. It serves as a wildcard.

Example

Let’s start with a simple one, a tile surrounded by water (w). It sounds reasonable enough

/* 
 * w | w | w
 * w | w | w
 * w | w | w
 */
water_single
Boooriiinnng

Now let me add a Land (l) tile to its right.

/* 
 * w | w | w
 * w | w | l
 * w | w | w
 */
water_sided
Now my center tile adapts to the land tile with a nice shore

Now I’m a bit of a messy person, and I add a burned-land (b) and a hill (h) tile, right around it, without letting times for my artists to draw all the nice transitions it would need.

/* 
 * w | b | b
 * w | w | l
 * w | w | h
 */

full
Messy, but that’s the best I can have

Thanks to the best-match solution, my tile will look like the previous one. While not being perfect, it still blends in better than the default one.

To make the previous example work, I declared the water tile with the following context (n being the ‘null’ used as a joker):

/* Water tile            shore tile
 * n | n | n                   n | n | n
 * n | w | n                   n | w | l
 * n | n | n                   n | n | n
 */

Side note : All assets in the examples are from Kenney’s website. And I’m very thankful for that !

Leave a comment