Hello and welcome back to the Blightmare dev blog! Today's post will wrap up the ongoing deep dive into terrain optimizations with a look at how the decorative outline tiles work.
We introduced the outline tiles in our original terrain post but some of the details were glossed over a bit. Careful readers would also have noticed that the original implementation of terrain outlines didn't work in the editor. The new method not only makes level loading a bit faster by pre-computing where the outline is and which image goes where, but it also shows up live in the editor which makes the level building process more similar to what the in-game experience will be like.
As the name implies, outline tiles are placed around the entire perimeter of the terrain tiles. These tiles to not have any collision associated with them, instead they are purely aesthetic and provide a bit of visual depth to the terrain. Other than the lack of collision, there's nothing special about the outline tiles so we could have built them as a distinct tile palette in the editor and let the level designer manually place the outline. This sounds like an annoying and tedious process to me, which is why we automated it from the start.
The previous implementation of tiles used a rule based tile setup to integrate with Unity's Tilemap API. It would have been possible to unify the rules for terrain and outline tiles within a single rule set, but that would have been a very large rule set so we opted for a simpler method. One of the side effects of the new tile editing method is that we no longer rely on any of the advanced functionality of the Tilemap API. Instead, we track tile locations ourselves, and as I went over last week, we have our own rule evaluation code. This puts us in a good position to add outline support without a lot of extra work.
One of the challenges when I originally thought about this problem was trying to wrap my head around the idea that a click on a single tile would modify several tiles. Of course, this is just the rectangular "brush" functionality that we already had, which seems obvious now, but took me a little while to grasp. Basically, every hover or draw operation needed to be expanded by a tile in every direction. Once that was figured out, the tiles that needed to be inspected were being inspected so it was just a matter of determining which tiles to place.
All of these tile posts have been full of bitmasks and this one will be no different! The thing that distinguishes an outline tile from a terrain tile is the outline tiles are adjacent to terrain. This means that if we know we're always inspecting tiles that are either terrain or 1 away from terrain, then a tile is an outline if there's no terrain at its location. The only other thing we need to know then is which image to use. The mask will tell us almost everything we need to know in that respect.
The hard part is trying to determine which outline tile set we should use. Since the outline tiles are completely generated, we can't rely on the user to tell us which tile set to use. The first part of this problem is then to configure an outline tileset for each terrain tileset. The last part is determining which terrain tileset should "own" a given outline location. We need this concept to resolve cases where there are different terrain tiles surrounding a single outline tile. In our case, we support a fairly minimal outline set which has all the cardinal direction edges (N, E, S, W), along with "interior" and "exterior" corners. Here's Edge S, Interior SW, and Exterior SW:
With this reduced set of tiles, the rules to determine which tile goes where collapses down into a single rules for each tile - the terrain rules were much more complicated! The order of evaluation for these rules is important as we go from most specific to least, but as long as we do that, the rules are very simple:
// Interior Corners { 0b00001011, new OutlineRule(OutlineType.Corner_Interior_SE, new Vector2Int(1, -1)) }, { 0b00010110, new OutlineRule(OutlineType.Corner_Interior_SW, new Vector2Int(-1, -1)) }, { 0b01101000, new OutlineRule(OutlineType.Corner_Interior_NE, new Vector2Int(1, 1)) }, { 0b11010000, new OutlineRule(OutlineType.Corner_Interior_NW, new Vector2Int(-1, 1)) }, // Edges { 0b00000010, new OutlineRule(OutlineType.Edge_S, Vector2Int.down) }, { 0b01000000, new OutlineRule(OutlineType.Edge_N, Vector2Int.up) }, { 0b00001000, new OutlineRule(OutlineType.Edge_E, Vector2Int.right) }, { 0b00010000, new OutlineRule(OutlineType.Edge_W, Vector2Int.left) }, // Exterior Corners { 0b00100000, new OutlineRule(OutlineType.Corner_Exterior_NE, new Vector2Int(1, 1)) }, { 0b10000000, new OutlineRule(OutlineType.Corner_Exterior_NW, new Vector2Int(-1, 1)) }, { 0b00000100, new OutlineRule(OutlineType.Corner_Exterior_SW, new Vector2Int(-1, -1)) }, { 0b00000001, new OutlineRule(OutlineType.Corner_Exterior_SE, new Vector2Int(1, -1)) },
Serialization adds a little extra information by essentially adding the outline tile types on the end of the 256 possible terrain tile masks. We can easily determine if a tile id is an outline by just checking if the value is at least 256. If it is, we subtract 256 and map the remainder into the outline tileset.
Putting it all together then, we can finally draw terrain in the editor like it will look in the game:
Thanks for stopping by! As always, please add Blightmare to your wishlist and follow us on Twitter to help support the game. Have a great week!