Generating a Procedural 2D Map in C#: Part 3: The Redo

Taking lessons from Part 1 and Part 2, I'll be taking a dive into creating solid foundations for future parts. We'll be looking at flexibility for generation, saving, and printing.

Generating a Procedural 2D Map in C#: Part 3: The Redo
A snippet of map generation.

Part 1 and Part 2 were great to kick this project off and Part 3 will be consolidating what I've learned to create a solid base.

The Map - Types of Arrays

I'll represent the map as an array - simple x and y coordinates. .NET has a couple of different array types and I didn't know which would better suit my needs, the two I'm interested in are:

Where my key concerns are ease of use and what would be deemed as "best practice" for a simple map representation.

Jagged Arrays

int[][] jaggedArray = new int[3][];

jaggedArray[0] = new int[5];
jaggedArray[1] = new int[4];
jaggedArray[2] = new int[2];

var value = jaggedArray[0][1]

An array of arrays where each element needs to be individually initialized. This allows us to have different sized arrays at each index position. I'm not sure this is a good fit as we could create a map that looks like:

Where each initialized array could be a different length - as represented by the different Y "heights" above.

Multidimensional Arrays

int[,] array = new int[4, 2];

var value = array[0, 1];

I liked an explanation in this answer on StackOverflow:

A multidimensional array, on the other hand, is more of a cohesive grouping, like a box, table, cube, etc.

Considering here I can lock in my map as a rectangle, access it in what I see is a tidier way, and I prefer how the above quote flows; I'm sold on a 2D array.

Interesting point, FxCop will present the following on a 2D array:

This is CA1814 with the key point being, The arrays that make up the elements can be of different sizes, leading to less wasted space for some sets of data. Fair enough but we know we're going to create rectangular maps with no wasted elements, so we're good.

Relationship Between Map and Tile

A map contains tiles, easy. Or really, the Map object will contain a 2D Array of type Tile. Sitting down to write pseudo code, the following went through my mind:

  1. What generates a map? The Map object, or a map generating object?
  2. Does a tile have reference to the parent Map?
  3. Should tiles know about neighbouring tiles?
  4. Does a tile know about its position?

To help me, I'll also be poking at the open source game Mindustry, a top down 2D tower defense game.

To the discussions!

1. What Generates a Map? The Map Object, or a Map Generating Object?

SOLID. Specifically S or Single Responsibility Principle. Map should simply represent the map and allow hooks for reading and writing. The Map object shouldn't "represent the map and generate the map" - it should be simple.

Meaning if we want to generate a map, we should have a MapGenerator. We could inherit from this to create a RandomMapGenerator or some generator with specific constraints. This idea is backed up by the developers of Mindustry with a BasicGenerator inheriting from a RandomGenerator inheriting from a Generator.

This approach of not having a Map god object means if we would need a MapPrinter or similar to display the map. Following the same principle, we could have different printers to represent the map differently.

Answer: A separate object will generate the map.

2. Does a Tile Have Reference to the Parent Map?

Bi-directional navigation is a totally valid relationship type for a lot of scenarios. Though they do introduce complexity, with the most famous being bad cyclic references. (Cyclic references are fine when used correctly). I don't think a tile needs to know about its parent, at least for now, as having the root entity as the Map should be good enough.  

We probably want to keep a Tile object as simple as possible and let parent objects do manipulation and logic. Aka, SRP above.

Answer: No.

3. Should Tiles Know About Neighbouring Tiles?

Similar to above with SRP, the only reason I can think that a tile would want to know about a neighbour is for generation purposes and this can be handled by a specific generator object traversing the Tile array in the Map object.

Answer: No.

4. Does a Tile Know About Its Position?

SRP answers this one too. If a Tile knew about what position it was in, it means it would have knowledge about the parent Map object.

Answer: No.

Feature List

The following will make the solid base feature list of the procedural map generation:

  • Create random maps of n by m size
  • Have three types of terrain: Water, sand and grass
  • Be able to save and load maps
  • Print maps

Pretty much the same as the end of Part 1, as we're going to be skimping on the smooth transitions for now. We'll discuss why after the feature list.

TerrainType, Tile and Map

Our three building blocks. Let's start with the smallest grain and go outward, meaning first up is TerrainType. I later on want these to be defined by the user but, small steps.

public enum TerrainType
{
    None,
    Water,
    Sand,
    Grass
}

Super simple, our three terrain options and a None for default purposes. Going to try and stay away from null.

Onto Tile, as for now a simple class only containing TerrainType. I want the type to be displayed in the debugger rather than have to dig into every Tile and look at the TerrainType property and I do that via [DebuggerDisplay("{TerrainType, nq}")].

[DebuggerDisplay("{TerrainType, nq}")]
public class Tile
{
    public TerrainType TerrainType;

    public Tile() : this(TerrainType.None)
    {
    }

    public Tile(TerrainType terrainType)
    {
        TerrainType = terrainType;
    }
}

Then to Map. For now I'm not too concerned with the accessibility of the Tiles array - I'll come to that later once I better understand what Tile access looks like. This will also probably change the Width and Height variables to be readonly - but again, that's for later.

I didn't want to have any null elements in the array, so a simple initializing SetDefaultMap() method will do.

public class Map
{
    public Tile[,] Tiles;

    public int Width;

    public int Height;

    public Map() : this(15, 15)
    {
    }

    public Map(int width, int height)
    {
        Width = width;
        Height = height;

        Tiles = new Tile[width, height];
        SetDefaultMap();
    }

    private void SetDefaultMap()
    {
        for (int x = 0; x < Width; x++)
        {
            for (int y = 0; y < Height; y++)
            {
                Tiles[x, y] = new Tile();
            }
        }
    }
}

Generating a Random Map

I'll be creating a map generator class. I know I'll want different ways to generate a map in the future so an easy start will be IMapGenerator.

interface IMapGenerator
{
    Map Generate(int width, int height);
}

Then creating RandomMapGenerator that implements the interface. I'll be using the same noise library as Part 1 called SimplexNoise because I can copy my existing code. In the future, there could be different implementations of noise leading to different random generators.

class RandomMapGenerator : IMapGenerator
{
    public Map Generate(int width, int height)
    {
        var map = new Map(width, height);
        var noiseValues = GenerateNoise(width, height);

        for (int x = 0; x < noiseValues.GetLength(0); x++)
        {
            for (int y = 0; y < noiseValues.GetLength(1); y++)
            {
                map.Tiles[x, y].TerrainType = DetermineTerrain(noiseValues[x, y]);
            }
        }

        return map;
    }

    private TerrainType DetermineTerrain(float noiseValue)
    {
        switch (noiseValue)
        {
            case var noise when noise <= 80:
                return TerrainType.Water;
            case var noise when noise <= 100:
                return TerrainType.Sand;
            default:
                return TerrainType.Grass;
        }
    }

    private float[,] GenerateNoise(int width, int height)
    {
        var random = new Random();
        Noise.Seed = random.Next();
        var scale = 0.001f;
        var noiseValues = Noise.Calc2D(width, height, scale);

        return noiseValues;
    }
}

Nothing complex here:

  1. Generate the noise via GenerateNoise(). A 2D float array that matches the dimensions of the map we're wanting to create. Looks something like:
All values here over 100, meaning all grass.

2. Step through each noise element and determine which terrain it should represent it via DetermineTerrain(). I wanted to play with the C#7 switch syntax to see how it feels.
3. Ultimately the end result gives us our set tiles, like this:

All grass as expected.

Easy.

Save and Load Maps

The easiest way I can think of is serializing the Map object via Json.NET. Where should this code go? It doesn't seem right to have the Map object have a dependency on the serializer as that implementation can change meaning we'd want it elsewhere - something like an IMapSerializer where we can have control on the serialization.  For now, I'll assume that I'll always want to serialize to a string.

public interface IMapSerializer
{
    string Serialize(Map map);
    Map Deserialize(string serializedMap);
}
public class JsonMapSerializer : IMapSerializer
{
    public string Serialize(Map map)
    {
        var mapString = JsonConvert.SerializeObject(map);
        return mapString;
    }

    public Map Deserialize(string serializedMap)
    {
        var map = JsonConvert.DeserializeObject<Map>(serializedMap);
        return map;
    }
}

Also easy.

Printing Maps

As is the theme so far, we're going to create more objects to do the printing which will inherit from an interface so we can create all sorts of different printers. Let's start with an IMapPrinter.

public interface IMapPrinter
{
    void Print(Map map);
}

I want to start from a basic print of a map and also to try something I've never needed to do: coloured console output. I'll create a ConsoleMapPrinter that will represent each tile as a coloured character.

public class ConsoleMapPrinter : IMapPrinter
{
    public void Print(Map map)
    {
        Console.BackgroundColor = ConsoleColor.White;
        
        for (int y = 0; y < map.Height; y++)
        {
            for (int x = 0; x < map.Width; x++)
            {
                PrintTerrainCharacter(map.Tiles[x, y].TerrainType);
            }
            Console.WriteLine();
        }

        Console.ResetColor();
    }

    private void PrintTerrainCharacter(TerrainType terrainType)
    {
        switch (terrainType)
        {
            case TerrainType.Water:
                Console.ForegroundColor = ConsoleColor.Blue;
                Console.Write("█");
                break;
            case TerrainType.Sand:
                Console.ForegroundColor = ConsoleColor.DarkYellow;
                Console.Write("▒");
                break;
            case TerrainType.Grass:
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("█");
                break;
            default:
                Console.ForegroundColor = ConsoleColor.White;
                Console.Write(" ");
                break;
        }
    }
}

It loops through each tile and prints according to the TerrainType. But watch out for the x and y axis inversion on the nested for loop. Consoles write left to right then up and down, so we have to address the y axis first then scan through all the x. This tripped me up on original printing.

The console colour is set to white to help make the sand texture pop. Then reset afterwards as cleanup. We get neat ASCII looking maps like this:

Very neat, it's looking similar to the tiled maps of Part 1 and Part 2.

Mission Accomplished

The feature list is fleshed out and maps can be randomly generated, saved and printed at will with flexibility owing to the interfaces written for each of these tasks. Not bad!

But What About the Smooth Pokemon Maps?

The discussion I promised earlier starts here. Creating nice transitions between terrain from Part 2 is only really the concern of a printer - meaning it isn't important at all to the core functionality. I have given it some thought and I'm planning on making Part 4 (future edit: There wasn't one) about an ImagePrinter and thinking about how to decorate the maps.

To End

Lessons from Part 1 and Part 2 went into creating a better, closer to SOLID codebase to extend into the future. Things I'd like to do coming up:

  • ImagePrinter to represent the map in tiles. I'd also like to see what this would look like with other open source game tiles based on popular titles, such as Zelda or Golden Sun.
  • Begin thinking on another layer to a tile so decorations can be placed on them.
  • Use the [] notation directly on Map to access Tile[].
  • Throw this on GitHub.
  • Move around the files into folders e.g. Printer, Generator, Serializer folders.

Onto Part 4.

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