Monday, December 12, 2016

A Look Under the Hood: Piecing Tiles Together

It took me a while of trying out different things before I found a way to create a retro map that was procedurally generated and still looked great. Specifically, getting the tiles to render in such a way that they looked correct and seamless, no matter how many terrain types were joining up. I thought I'd share my method for others looking to do the same.

Photoshop View
Transparent .png
(These images are courtesy of Lanea Zimmerman (aka Sharm) and the CC-by-SA 3.0 license; you can find more where that came from here).

You'll need at least two types of terrain for the technique we're working with, sliced into square blocks (these are 32 x 32 pixels). Notice that the graphics are on a transparent background, and that they tile seamlessly. This is very important. The row of solid blocks (6th down) are for variation, and while they're not necessary, they greatly improve the feel of the terrain. The two loners on the top left won't be used at all-- but they make nice decoration.

So, say that you're dealing with a checkerboard of terrain sprites on your 2d game. You have three types of terrain: water, dirt and grass. Pretty simple stuff, all things considered. Just rendering the base, flat tile doesn't look very good, though. In fact, it looks pretty terrible. Hard, unnatural angles. Even the variation blocks don't help that much. (This overview is assuming that your land masses are in semi-random blobs. I can cover how I handled my map generation some day... but not now.)

8 x 8 tiles in three terrains, using only solid blocks.
So, we have our transparent png, we can easily cover up those raw edges. But then you run into another problem. Our 'transitions' take up about half a block's width-- meaning we'll be drawing our grass' edge out onto another tile terrain type. So-- what block takes precedence? Does grass get to creep onto the dirt and tile? Is dirt or water more important? Are we even asking ourselves the right questions?

This was my eventual solution:


Rather than trying to tell what types of tiles should get to keep their land, keep it as simple as possible. We only care about each of these four corners: North, Diagonal, East, and itself (Center). Name the tiles of your terrain like that: Grass_ + whatever corners your grass covers. So for instant, that piece of grass in the upper right corner of our initial graphic would be Grass_NDE, because the lower left corner is where the transparency sits. Just be consistent in your naming conventions, because it'll be important later.

And there it is. The fifteen tiles you really will need. (I just named my block of solid grass Grass.)

Next, I configured my tile game object by making four children, each with its own Sprite Renderer component, and I named them Base, Neighbor1, Neighbor2, Neighbor3.

Then for the code. The short version:

Step 1. I made a dictionary to hold all four corner variables that we were working with. If I'm at the top or the right edge of the map, I use the tile's own terrain for the non-existent neighbor.
Step 2. I made a list of all the different types of terrain present, and then I sorted them in a custom order. Nothing too complicated here, but this step is important. That said, feel free to try out different rendering orders.

List<string> sortedTerrains = new List<string>();
if (unsortedTerrains.Contains("Sand")) { sortedTerrains.Add("Sand"); }
if (unsortedTerrains.Contains("Tilled")) { sortedTerrains.Add("Tilled"); }
if (unsortedTerrains.Contains("Dirt")) { sortedTerrains.Add("Dirt"); }
if (unsortedTerrains.Contains("Stone")) { sortedTerrains.Add("Stone"); }
if (unsortedTerrains.Contains("Water")) { sortedTerrains.Add("Water"); }
if (unsortedTerrains.Contains("Grass")) { sortedTerrains.Add("Grass"); }
return sortedTerrains;

Now we have a list of terrains in the order we're going to render them. Stick that sucker in a foreach loop. Solid base, each further terrain type will take a corner.

List<Sprite> spriteList = new List<Sprite>();
foreach (string terrain in terrains) {
// The bottom layer will have a solid tile.
if (spriteList.Count == 0) {
spriteList.Add(terrainSprites[terrain]);
continue;
}

// tileTypes is our dictionary with our corner data from step one
spriteName = terrain + "_";
if (tileTypes["N"] == terrain) { spriteName += "N"; }
if (tileTypes["D"] == terrain) { spriteName += "D"; }
if (tileTypes["E"] == terrain) { spriteName += "E"; }
if (tileTypes["C"] == terrain) { spriteName += "C"; }


if (IsTerrainValid(spriteName)) {
spriteList.Add(terrainSprites[spriteName]);
}
}

this.AssignTerrainBackgrounds(spriteList);

And... that's pretty much it, with some error checking and such. The additive naming convention does all the work for us. Stick each sprite into the appropriate SpriteRenderer component of our tile game object, and you have a layered tile without much work.


No comments:

Post a Comment