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.
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 :
- Loop over all the available textures for the given type
- Compare the Tile.TileContext to the Texture.TileContext
- 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 */
Now let me add a Land (l) tile to its right.
/* * w | w | w * w | w | l * w | w | w */
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 */
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 !