Data Tilemaps


Unity Tilemaps

A Tilemap is a special kind of GameObject in Unity. Its size spans the entire 2D plane in Unity. It is an infinite grid, where each cell is by default 1×11 \times 1 Unity units1. You can paint a specific tile image (called a TileBase in code, but a sub-divided image of a Tileset in the project menu) onto the Tilemap. These TileBases contain no data other than the image to be rendered. You cannot store per-tile data in a TileBase.

Here is my current Tileset: Tileset

Tiles are set using tileMap.SetTile(<Vector3Int>, <TileBase>), and can be efficiently set in batches using tileMap.SetTiles(<Vector3Int[]>, <TileBase[]>). There are also functions to clear all tiles and clear certain tiles.

Tilemaps are therefore typically used for static map creation. You can have a whole bunch of neat custom rules, such as when a grass tile is above and to the right of another grass tile, there should be a diagonal grass tile in between the two. This makes map creation faster and smoother.

That’s absolutely not what I want to do with tilemaps.

WIWTDWT (What I Want To Do With Tilemaps)

This is a game about mining in an infinite world. Thus, stone and ore should have health, or at least some measure of richness. A tile must know whether it is stone or ore, so after it is mined, the correct resource can be deposited in the bot’s inventory. And, no, John Coder, I can’t use the tile’s texture to determine its type. Not only does that violate God knows how many design principles (namely, the Single Responsibility Principle (SRP)), it also prevents me from making a single change to a texture, since I risk lobotomizing all of the code that depended on it, and I also wouldn’t be able to have different health/richness values for the same tile type.

The DataTile

So, how do we fix this? The naive implementation would be to throw the Tilemap in the trash and use GameObjects for each and every tile, but Unity has a GameObject limit of 65k, and it hits performance issues well under that. No. What I need is a custom datastructure.

First, I define a TileType:

public enum TileType
{
    Stone,
    Ore
}

Note: an enum is a way to assign semantic meaning to words, without making them variables. Under the hood, Stone = 0 and Ore = 1. They are ints in a trench coat.

public readonly struct DataTile
{
    public readonly TileType type;
    private readonly int durability; // how many times you need to mine it to get rid of it
    private readonly int health;
    private readonly float density; // density of the tile relative to its surroundings,
                                    // used for visual purposes in ore patches

    public DataTile(TileType type, int durability, float density);

    // ...
}

Note: I use a struct because it is inherently readonly (although I enforce it explicitly). Basically, DataTiles should be destroyed and replaced, never modified. They are single use, like cigarettes or Labubus or integers. Another benefit of using a struct is that variables and parameters that have a struct type cannot be null, so we don’t have to worry about weirdness there.

Great. Now we have a way to store data for tiles separately from the tiles themselves. We can make any modifications we like to the underlying representation of a DataTile and the Tilemap will remain the same. We now encounter several other problems. Notably, how should we store the DataTiles so that we know what TileBase in our Unity Tilemap the DataTile corresponds to?

DataTilemaps

We could consider using a 2D array (that is, a list within a list), where the index within the 2D array (e.g., dataTilemap[1, 1]) is the location of the TileBase, since positions on the Tilemap have an associated TileBase. Bzzt! Wrong. This is terribly inefficient in terms of space complexity (O(n2)\mathcal{O}(n^2), where nn is the maximum of the vertical and horizontal distance between any two tiles), and terrible in terms of lookup time (again, O(n2)\mathcal{O}(n^2)).

We want fast lookups, efficient space usage, and to not suck. Enter the humble Dictionary. As you should know, dicts have O(1)\mathcal{O}(1) lookup, and store O(n)\mathcal{O}(n) elements, where nn is now simply the number of tiles. Dicts store key-value pairs, where keys must be unique and values are accessed via the key. The obvious analogue is looking up the definition (value) of a word by looking for the word (key) in a dictionary. Dicts achieve such fast lookup speeds because they encode the keys as hashes, which are unique values for each possible key2, and so when we want to find the value associated with a key, we simply find the key’s hash, and immediately access the correct dictionary entry. It’s a little more complicated than that under the hood, but that should be enough to tide you over for now.

So, we’ll use Vector2Ints (which are vectors of size 2, where both elements of the vector are integers) for the keys, and we’ll use DataTiles for the values. Thus, when we want to find the DataTile associated with a position, we make a Vector2Int out of the position, and look up the corresponding DataTile in the dictionary.

Much better. Now, if you’ve read my previous devlog (or if you know Unity), you’ll know that the best way to store raw data is using a scriptable object. Scriptable objects are scrumptious in this case, since we also would like to be able to save the map later on:

public class DataTilemap : ScriptableObject
{
    private readonly Dictionary<Vector2Int, DataTile> _tiles = new Dictionary();

    public void TryGet(Vector2Int pos, out DataTile tile);
    public void Set(Vector2Int pos, DataTile tile);
    public void RemoveAll();

    // ...
}

Note that from here on out, the “true” tilemap is our DataTilemap. Our Unity Tilemap now exists only for visual representation. Note that this is an incredibly important distinction. Before, the tiles and their positions were managed by Unity’s Tilemap, and we would interact with that directly to set tiles, remove tiles, etc. Since we’ve made our own richer representation, we are switching over to using the DataTilemap for our tilemap representation. Again, recall that each position on a Tilemap used to have an associated TileBase. Now, each position in our DataTilemap has an associated DataTile, and we must now find a way to propagate this setup into the Tilemap.

Connecting DataTilemap and Tilemap

You may see the glaring issue here. We still haven’t connected the visual representation, Unity’s Tilemap, with our own DataTilemap. This is an important design point, and we need to think on this.

DataTile to TileBase

Let’s start with the simplest connection, which is translating a DataTile to a TileBase. Remember that our DataTilemap, with its DataTiles, is the true tilemap. That means that we should be able to infer our TileBase (visual tile) directly from our DataTile (data tile). This is currently pretty straightforward. Choose one TileBase if the DataTile is of TileType ore, and choose another TileBase if the DataTile is of TileType stone. However, this quickly becomes tedious and more complicated if we want to choose a different image (TileBase) based on the durability/density of a DataTile, or if we want to add a different TileType later on, so let’s formalize it.

TileResolver

Let’s make a TileResolver class, which has a resolve(DataTile dTile) function that takes a DataTile as input and outputs a TileBase. TileResolver maintains a list of TileBases that it can use to resolve the DataTile, and it has a single TileType field which lets us know what TileType it can resolve.

Implementing this was kind of a drag, so I’ll explain it briefly here. I use scriptable objects for this, because the resolution logic does not depend on the current game state, and it also does not need to be attached to a GameObject.

Basically, I want to use a single TileResovler to resolve any TileType. So, I’ll make an abstract AbstractTileResolver class which does not limit itself with a TileType, then I’ll extend it with a TileResolver class which does limit itself with a TileType. DensityTileResolver is a subclass of TileResolver, and it chooses a TileBase based on the density of the given DataTile. I can make others, like HealthTileResolver, but I will just make the DensityTileResolver for now. I can instantiate DensityTileResolver to make an oreDensityTileResolver and a stoneDensityTileResolver, which can translate DataTiles of type ore and stone, respectively. Now, to combine these into a single, class that I can use to resolve any DataTile, I’ll make a CompositeTileResolver which extends AbstractTileResolver (so it is not limited by TileType) and it maintains a list of other AbstractTileResovlers. When it is to resolve a DataTile, it iterates over the AbstractTileResolvers in its list, finds the one that can resolve the DataTile, and then resolves using the correct AbstractTileResolver.

DataTilemap to Tilemap

The simplest way to go would be to have the DataTilemap store an instance of a Unity Tilemap, and when the DataTilemap is modified, it modifies the internal Tilemap. There are several problems with this. The first is that this once again violates SRP. Our DataTilemap is supposed to hold data, and hold data only. It should not have the added responsibility to manage the visual representation of said tilemap. The second issue is that we may want to modify the tilemap from other classes, such as map initialization. Now, the map initialization must have an instance of the DataTilemap, and then the DataTilemap must have an instance of the Tilemap. Yuck. Very high coupling. How do we fix this?

Events

Well, again, if you’ve read my previous devlog, you’ll know that unity Events are amazing for this. They invert dependencies. This is a really good example. Let’s extend our DataTilemap to emit some events when it is modified.

public class DataTilemap : ScriptableObject
{
    // ...

    public event Action<Vector2Int, DataTile> TileChanged;
    public event Action<Vector2Int[], DataTile[]> TilesChanged;
    public event Action<Vector2Int> TileRemoved;
    public event Action<Vector2Int[]> TilesRemoved;
    public event Action AllTilesRemoved;
}

Note: Vector2Int is a single Vector2Int, whereas Vector2Int[] is a list of Vector2Ints. Also, the values within the <> denote that parameters that the callback functions must have in order to listen to that event.

MapDisplay

We should make a manager for the Unity Tilemap. Let’s call it MapDisplay. I won’t define it here (I can’t give up all the secrets, sorry!), but it’s not too complicated of a class. To start, let’s make it a wrapper for Unity’s Tilemap (it holds a reference to the TileMap). Through MapDisplay, we can modify the Tilemap’s tiles using TileBases.

Now, we can extend MapDisplay to modify the Tilemap using DataTiles as input, instead of TileBases by using our CompositeTileResolver from earlier.

Then, finally, we can listen to the events from our DataTilemap by holding a reference to our DataTilemap, and adding in our pre-made wrapper functions for the Tilemap (that take in DataTiles) as listeners to the DataTilemap events.

High Level: Model-View-Presenter

3-Tiered Cake

So after so many DataTilemaps and DataTiles and TileResolvers, you may be getting a little confused, especially since I haven’t been tying any of this into the larger picture. Basically, we’ve made the ideal form of a Model-View-Presenter design pattern.

In a Model-View-Presenter design pattern, the Model is the data. It should not have any responsibilities apart from holding data, being modifiable, and emitting events. It should not compute any logic on the data, it should not compute any logic on its output. Then, one layer up, we have the Presenter. The Presenter handles all logic, all modifications to the Model, and all reflections of the model up to the View. The View is the next layer, and it handles only the display of the data. It should not modify the data or hold any of the Model’s data and change it locally, it should just display what it is told to display. Note that there may be multiple files/classes in the Presenter and View sections, but typically not in the Model section.

Although typically this pattern is represented as a 3-layered “cake”, with the Model at the bottom, the Presenter in the middle, and the View at the top, each communicating only with their neighbour, I like to think of the pattern more as a horseshoe.

Horseshoe

In the Horseshoe MVP (HMVP), the presented is further split into two parts, Interal Presenter and External Presenter. They do not communicate or interact with one another. The Internal Presenter only modifies the Model, whereas the External Presenter displays the changes from the Model to the View. Modifications from elsewhere to the Model should go through the Internal Presenter, since the External Presenter, again, only updates the View. So, again, the View and Model should not do any logic, they just exist to be modified and updated. All together, it forms a lopsided horseshoe, with the Internal Presenter connected to the Model, the Model connected to the External Presenter, and the External Presenter connected to the View.

In our example, the Model is the DataTilemap. It adheres to the principles a Model should adhere to. That is, it is modified, it does not handle any logic, and it emits events.

The External Presenter in our example is the MapDisplay and the TileResolver. They both handle updating the view based on changes made to the underlying Model.

The Internal Presenter in our example was not actually discussed, but it would be any map generation classes, which modify the Model and any mining done by bots.

Finally, the View in our example is the Tilemap. It is inert, it does not do any logic, it is simply updated by the External Presenter (the MapDisplay) whenever the Model changes.

A trace

Although all of the above sounds complicated, it’s pretty straightforward. Let’s trace through an example of a modification to the Model.

First, the bot mines an ore tile at position [2, 5]. This is communicated as the removal of a tile through an Internal Presenter, which modifies the Model (the DataTilemap).

The Model is updated with the removed tile, and emits an event that a tile at position [2, 5] has been removed.

The MapDisplay, listening to that event, then updates the Tilemap by telling it to remove the painted tile at position [2, 5]. We do not use a TileResolver since we’re just removing a tile.

Done! It’s as easy as that.

Goodbye.

I’ll be writing a devlog about console commands next. It’s pretty juicy, and it’s also pretty innovative. I haven’t seen anyone do it like I’ve done it. It’s gonna be pretty long, so I’ll try to include more code to keep it interactive.

Footnotes

  1. A Unity “unit” is a special kind of measurement. I might get into this later, but what’s important is that you specify the “pixels per unit” on a sprite, and if you do it correctly, you can make each tile of your sprite tile image one Unity unit. I use 16×1616 \times 16 for this project.

  2. A calculation for the hash of a class must be specified in the class definition for the class to be “hashable”, i.e., for the class to be used as keys in a dictionary. Vector2Int already defines that hashing calculation for us.