Writing a generic tilemap class

Reusability and modularity is an important aspect of software design, as it can speed up development of future projects significantly. This also holds in game development, as we will show with the example of a generic tilemap class we are going to implement here.

Generics

Generics is a powerful feature of modern programming languages. It allows us to write code once, but execute it for a number of different cases, or use it in different ways, without having to copy-paste it, or write conversion code between cases.

This is especially true with collections. From typed arrays, to lists, dictionaries and sequence queries, generics help us consider all possible kinds of elements in our collections.

We can consider a tilemap a collection of tiles. In a specific game, each tile may contain any kind of information, from literal tile-drawing information to lists of contained objects and more.

This is a clear case for which using generics provides us with a modular implementation that we can use and reuse for different games with different kinds of tiles.

Tilemap implementation

In the simplest case, we could use a two dimensional array as a tilemap:

Tile[,] tilemap;

While this works, it is not a very flexible solution, and exposing arrays to large parts of our code like that is ugly and potentially unsafe.

Instead, let us create a wrapper class that we can further extend to add more tilemap related logic in the future.

For a simple implementation we need a backing array, properties like width and height, as well as a way to retrieve and change individual tiles. For that we can use indexers.

class Tilemap<T>
{
    private readonly T[,] tilemap;

    public int Width { get; }
    public int Height { get; }

    public Tilemap(int width, int height)
    {
        this.Width = width;
        this.Height = height;
        this.tilemap = new T[width, height];
    }

    public T this[int x, int y]
    {
        get { return this.tilemap[x, y]; }
        set { this.tilemap[x, y] = value; }
    }
}

So far so good.

Tilemap as collection

As we said above, our tilemap really is a collection of tiles. That means we can implement interfaces like IEnumerable<T> to be able iterate over all tiles.

Since we also know the number of elements in the tilemap, we further could fulfil part of the contract for ICollection<T>, however since it makes no sense to speak of Add or Remove operations for a tilemap, it makes more sense to inherit from IReadOnlyCollection<T> instead.

For this interface, we need to add the following members.

public int Count => this.Width * this.Height;

public IEnumerator<T> GetEnumerator()
{
    for (int y = 0; y < this.Height; y++)
        for (int x = 0; x < this.Width; x++)
        {
            yield return this[x, y];
        }
}

IEnumerator IEnumerable.GetEnumerator()
{
    return this.GetEnumerator();
}

As you can see, the enumeration is very straight forward. We simple use a nested loop to iterate the entire array, and return all elements.

Note how the outer loop iterates over rows, while the inner iterates columns. This improves the methods efficiency by increasing memory locality such that we return elements in exactly the same order they are stored in memory in the array.

Using a Tile reference wrapper

A times, I wanted to enumerate a tilemap but next to getting a list of unidentifiable tiles, I also would have liked to know where each of these tiles is located in the tilemap.

Similarly, it can be very helpful to store a reference to a tile. Since the actual tiles of a tilemap may change however, it may be better to store a reference to a position in the tilemap, instead of to the tile itself. With that reference, we can retrieve the current tile at that location whenever needed.

For both of these purposes, we can create a simple wrapper structure representing a reference to a tile in our generic tilemap.

struct Tile<T>
{
    private readonly Tilemap<T> tilemap;

    public int X { get; }
    public int Y { get; }

    public Tile(Tilemap<T> tilemap, int x, int y) : this()
    {
        this.tilemap = tilemap;
        this.X = x;
        this.Y = y;
    }

    public T Value
    {
        get { return this.tilemap[this.X, this.Y]; }
    }
}

With this type in place, we can make our tilemap class inherit IReadOnlyCollection<Tile<T>> instead, and change our enumerating method as follows.

public IEnumerator<Tile<T>> GetEnumerator()
{
    for (int y = 0; y < this.Height; y++)
        for (int x = 0; x < this.Width; x++)
        {
            yield return new Tile<T>(this, x, y);
        }
}

Further, we can create our own values of Tile<T> at any point to refer to tiles in the tilemap, and keep these references in our other types. For example, a game object may want to know which tile of the tilemap it is in. With this wrapper type, it can store a reference to that tile, which is aware of its own position as well.

I use a structure, instead of a class, to implement the Tile<T> wrapper. While this is not necessary for any particular purpose, it enforces thinking of the type as a value: a reference or ‘pointer’ to a location on the tilemap, instead of any object itself.

Once we start using this type more and more to traverse the tilemap, we will see how using a class would mean many unnecessary object creations. Using a value type for such small immutable types can improve performance and comes with no other drawbacks.

Conclusion

Above we implemented a simple tilemap wrapper class, which uses a two dimensional array as storage for its tiles. This gives us much control over the underlying collection and makes sure the raw array is not exposed to the rest of our game.

Further we created a reference-structure to point at particular tile locations in the tilemap. This helps is keep track of positions on the tilemap, even if the tiles themselves might change.

If this has been interesting to you, make sure to share this post on your favourite social media.

Over the coming weeks I will be publishing further posts building on the code here, to highlight some of the many ways in which tilemaps can make our lives easier.

Enjoy the pixels!

Leave a Reply

7 comments

  1. Michael Priest says:

    Very nice tutorial I will share please keep up the tutorials !!

  2. Scott says:

    I’m sorry, but in the end what should the class inheritance look like? Mine looks like this

    class Tilemap: IEnumerable, IReadOnlyCollection<Tile>

    But I am receiving the following error on the IReadOnlyCollection<Tile>:

    Error CS0738 ‘Tilemap’ does not implement interface member ‘IEnumerable<Tile>.GetEnumerator()’. ‘Tilemap.GetEnumerator()’ cannot implement ‘IEnumerable<Tile>.GetEnumerator()’ because it does not have the matching return type of ‘IEnumerator<Tile>’

    • Scott says:

      for some reason after posting the comment it deleted the inside of the ‘s but they are there in my code.

    • Paul Scharf says:

      Hey Scott,

      The class definition would be as follows:

      class Tilemap<T> : IReadOnlyCollection<Tile<T>>
      

      However, while investigating this, I found an error within the post.

      To implement the interface as described above, the method:

      public IEnumerator<T> GetEnumerator()
      

      has to be changed to

      public IEnumerator<Tile<T>> GetEnumerator()
      

      I’ve fixed it also in the post above. Thanks for helping me find the error!

      I hope this helps.

      • Scott says:

        Thank you for the response! That did fix one of the errors that I was having. However I still have the same error as before regarding the class definition. The IReadOnlyCollection<Tile> is still saying that it does not have a matching return type. I changed my website to a link to a text file with my code in it. If you get a chance could you take a look at it? Thank you much. Also here is the link if it allows me to put links in the comments: https://drive.google.com/file/d/14hrC2Zlw4as9-0ant1Wk1C-6M7jSjvnP/view?usp=sharing

        • Paul Scharf says:

          Hey Scott,

          Your code looks pretty much fine, but you currently have the Tile struct inside the Tilemap class. If you take it out (for example into a separate file) it should work.