Moving on from the basic groundwork of creating the map, let's get better transitioning between terrain types. I've continued using the same three types of terrain: grass, sand and water.

Here's a look at where the last post left off compared to where this post will leave off.

Basic Transitions

First up, I got together the tiles needed to make the simple transitions. Being witty I thought I could just rotate pieces, turns out due to the tessellating nature of this tileset at least, I couldn't do it as such the following tiles were created.

A 4x2 grid of basic transitional tiles.

Trying a small sample size, I picked the tile that represented sand at the top and water for the bottom. The steps are:

  1. A pass would be made to create the jagged base map.
  2. Then another pass to smoothen the lines.  

This was achieved by looking at the top, right, bottom and left neighbouring tiles then replacing the original tile with the new transitional tile. Which tile was going to give way to the other tile? I decided to go with the edge tile that is the "highest" terrain type. Going back to the original post and using Perlin Noise, each tile has a height that then gets translated into what terrain it is. For example, if sand is next to water, then the sand tile is swapped to the transitional tile. That's why the second picture above has the water line encroaching on the original sand line.

Or in short, "highest terrain gives way to lower terrain". Below is a visual representation (numbers for grid reference, not "height") where square 4 represents the jagged tile look, while 5 and 6 show the "give way" rule where the terrain of 8 and 9 will encroach.

Slightly Less Basic Transitions

Now moving on to something harder, the pieces that are flanked by more than one transition. Something like the following highlighted piece:

Created with these:

Seems simple enough with existing code looking at the four sides - except it turned into this:

Turns out, only matching the four sides allowed unexpected matching to happen, see the following: (As a side note, the second image has a "sand" tile in the middle bottom but says water because the comparison only considered the original map, not the map after transitional tiles were added on the second pass):

When only comparing top, right, bottom, left both scenarios are identical as indicated by the text. This meant there had to be a self check too which would prevent the middle sand tile from overwriting what is originally a water tile.

Some of the maps while working that out looked like:

After solving that, we get an image that looks very nearly ready:

More Slightly Less Basic Transitions

Except being a total novice to how 2D tilesets work, I didn't realise those pointy sand bits were going to be an issue. It means yet another tile type where a bottom corner could be a different terrain. So off I went cutting those from the original tileset.

Only the corners are another terrain.

Here I ran into a problem, with my 4 sides model, I couldn't represent the neighbours properly. An 8 neighbour model was created to try to take the diagonals into account. From now, each type of tile available tile to use would know what neighbours it fits with and each placed tile would know about it's neighbours.

It also got tiring having a huge block of code to create the types of each tile available. It could be hidden in a setup method, but I wanted to have this as a configuration and the tile configuration JSON file was created:

[
    {
        "TerrainType": 1,
        "ImageFilePath": "C:\\Users\\Niko Uusitalo\\Pictures\\tiles\\gggg.png",
        "NeighbourRequirements": {
            "TopLeft": 1,
            "Top": 1,
            "TopRight": 1,
            "Right": 1,
            "BottomRight": 1,
            "Bottom": 1,
            "BottomLeft": 1,
            "Left": 1
        }
    },
    {
        "TerrainType": 2,
        "ImageFilePath": "C:\\Users\\Niko Uusitalo\\Pictures\\tiles\\wwww.png",
        "NeighbourRequirements": {
            "TopLeft": 2,
            "Top": 2,
            "TopRight": 2,
            "Right": 2,
            "BottomRight": 2,
            "Bottom": 2,
            "BottomLeft": 2,
            "Left": 2
        }
    }
]
A non-complete TileConfig.json file.

The numbers represented a value of the TerrainType enum below.

public enum TerrainType
{
    Any,
    Grass,
    Water,
    Sand,
    OutOfBounds
}

The flow to make a map at this point:

  1. Create the map grid.
  2. Create the Perlin Noise and apply to map.
  3. Define the solid terrain types based on the Perlin Noise - grass, sand and water.
  4. Apply above to map.
  5. Create a blank map of the same size as the original to use as the transitional layer.
  6. Go through each tile on the original map and if it needs to be a transitional tile, add it to the transitional layer.
  7. Flatten transitional layer and original map to create the end result.

An Interesting .NET Aside

Step 6 is the crux of this post. And it's where I learned something about .Equals() in C#. I needed to compare the neighbours of the current map tile to the list of available tiles (from the config above) so I simply checked if any of the current tile's neighbours matched any from the list. Meaning I needed to write a compare and thought "why not just override .Equals()?".

Taking the object idea I already had called NeighbourRequirements which kept track of each neighbouring tile for a tile.

public class NeighbourRequirements
{
    public TerrainType TopLeft { get; set; }
    public TerrainType Top { get; set; }
    public TerrainType TopRight { get; set; }
    public TerrainType Right { get; set; }
    public TerrainType BottomRight { get; set; }
    public TerrainType Bottom { get; set; }
    public TerrainType BottomLeft { get; set; }
    public TerrainType Left { get; set; }

    // other code excluded for brevity
}

I wrote the equality check:

public override bool Equals(object obj)
{
    var compareToNeighbours = obj as NeighbourRequirements;

    if (compareToNeighbours == null)
    {
        return false;
    }            

    if (TopLeft != TerrainType.Any && compareToNeighbours.TopLeft != TerrainType.Any && TopLeft != compareToNeighbours.TopLeft)
    {
        return false;
    }

    if (Top != TerrainType.Any && compareToNeighbours.Top != TerrainType.Any && Top != compareToNeighbours.Top)
    {
        return false;
    }

    if (TopRight != TerrainType.Any && compareToNeighbours.TopRight != TerrainType.Any && TopRight != compareToNeighbours.TopRight)
    {
        return false;
    }

    if (Right != TerrainType.Any && compareToNeighbours.Right != TerrainType.Any && Right != compareToNeighbours.Right)
    {
        return false;
    }

    if (BottomRight != TerrainType.Any && compareToNeighbours.BottomRight != TerrainType.Any && BottomRight != compareToNeighbours.BottomRight)
    {
        return false;
    }

    if (Bottom != TerrainType.Any && compareToNeighbours.Bottom != TerrainType.Any && Bottom != compareToNeighbours.Bottom)
    {
        return false;
    }

    if (BottomLeft != TerrainType.Any && compareToNeighbours.BottomLeft != TerrainType.Any && BottomLeft != compareToNeighbours.BottomLeft)
    {
        return false;
    }

    if (Left != TerrainType.Any && compareToNeighbours.Left != TerrainType.Any && Left != compareToNeighbours.Left)
    {
        return false;
    }

    return true;
}
A crude check to compare eligible neighbours.

It turns out, I got a warning:

Which made sense when I remembered a Pluralsight course module about hugely speeding up .Equals() with Structs (author's blog post here). In short overriding .Equals() means taking some underlying .NET into your own hands in respect to hashing and comparing. It also didn't feel right and general opinion sometimes went that way, as seen here and here.

I had some options I knew about and I'll be up front, I went with a simple method to compare because I wanted to move forward with making neat maps. But on reflection these will be looked into in the future:

  • IComparer<T>
  • IComparable<T>
  • EqualityComparer<T>

I'm happy to come back to in my code cleanup steps later on.

The Last Hurdle

Back to the maps. The edge tiles would have OutOfBounds neighbours as their TerrainType type which came back to bite me because nothing matches out of bounds and the outer most perimeter of a map doesn't transition because of this.

Non-transitioning top and bottom most sand tiles.

As it was late at night by this point, the easiest way to get around this is to create a map, but skip the visual output of the one tile width perimeter. Meaning for a 15x15 map, the innermost 13x13 gets printed. Easy.

This means the generated maps now looked proper enough:

And we can make them huge:

This wraps up the prototype of how I'm looking to make the transitional pieces smoother. From here the goals would be:

  1. Tidy up code.
  2. Have a pass to maybe change up the tiles such as swapping a grass tile for another grass tile for variation.
  3. Decoration like trees and small interesting bits on some tiles.

This is a series of producing procedural maps in C#. All parts: Part 1, Part 2, Part 3.