Tilemaps In Unity 2017.2 - Part 2

Minesweeper

In the previous post we took a brief look at Tilemaps in Unity. In the next couple posts, we will go more in depth and build a complete sample game using Tilemaps. This will give us an opportunity to bo beyond just placing tiles via the Unity Editor, and explore the runtime APIs available for Tilemaps.

The game we’ll be creating is Minesweeper. I assume most people are familiar with minesweeper, so I won’t spend time going over the rules. For sprites I used the tileset from here. The focus and code samples below will be on the Tilemap specific bits, I’ll be glazing over the rest of the game, however the full implementation is available on github here. I’d love to have the final product available online, but at the time of writing, Tilemaps don’t work in WebGL builds.

Designing the tiles

Before we start coding the game there are a couple design decisions we need to make which have impact on the final code structure. Most relevant to our discussion is what tiles we need to create. There are a number of different sprites we need the ability to display:

  • Numbers 1-8 (8 is the maximum number of bombs a given square can touch)
  • Empty square
  • Unrevealed square
  • Flagged square
  • Bomb square

There are then several options for splitting these sprites into tiles:

  • One sprite per tile. This is the simplest approach to get started with
  • One tile per object. i.e., a tile for empty squares, and a tile for bombs
  • A tile per visibility. i.e., a tile for revealed squares, and a tile for unrevealed squares

Any of these approaches could be made to work. However the first one is a bit uninteresting in that it would involve completely static tiles; we wouldn’t have an opportunity to implement our own Tiles.

The second one is more interesting as we would be implement our own tiles. Each tile would first need to check if the square is revealed or not, and display the either the unrevealed square sprite or the flag sprite. The empty square tile would then need to decide how many bombs it is touching. This would work, however the unrevealed square logic would be duplicated in each Tile.

In the last option we have 2 tiles. The unrevealed Tile only needs to decide if it will display the unrevealed square sprite or the flagged sprite. The revealed Tile needs to decide how many bombs it is touching (or if it is a bomb).

For this post we will use the 3rd approach. Like I mentioned, any of them will work, however I feel like this one has the cleanest separation of logic and responsibility. It also aligns cleanest with the actions the player makes. The player action is the act of revealing a square, which results in the Tile type on the map changing.

Project Setup

Now that we’ve decided how to organize our tiles, lets get the project setup. I’m going to glaze over most of this, see the project on github for in-depth details, but at a high level we will

  1. Create a new Unity project
  2. Import our Sprites, and adjust the pixels per unit so they each fit 1 square (the Sprites linked above are 61x61, so I set this to 61)
  3. Create a new Scene and add a Grid and Tilemap

With that done we will now write the Script that will store the game state and contain the majority of the game logic. In the sample I called this script ‘MapInformation’. It needs to do a few different things

  • Store the size and state of the map. Be able to answer which squares are bombs and which are empty squares
  • Handle input from the user; this is a method that takes the coordinates of a square, and reveals that square. If that square is a bomb, it should end the game
  • Handle flagging a given square. This marks the square as a bomb, without actually revealing it
  • Be able to track whether a square is unrevealed, revealed or flagged
  • Initialization method, takes the size of the board and the number of bombs, generates a game board
  • Be able to answer whether a given square is revealed or unrevealed
  • Be able to determine the number of bombs a given square is touching
  • Determine if the game has ended in victory (only bombs are left unrevealed)

Not all of these functions use Tilemaps though. The parts that interact directly with Tilemaps, and thus the parts we are most interested in:

  • When the map is initialized, need to create a unrevealed Tile for each square on the board
  • When the user flags a square, the Tile should be updated to show it is flagged
  • When the user reveals a square, the Tile on the map needs to change to a Revealed Tile

We won’t cover the entire implementation here, but lets take a look at some of the Tilemap related parts. First, here’s the public fields we expose to the Editor:

public class MapInformation : MonoBehaviour
{
    // Map dimensions
    public int SizeX;
    public int SizeY;
    public int NumberOfBombes;

    // Tile information
    public TileBase UnrevealedSquare;
    public TileBase RevealedSquare;

    ....
}

First we expose fields for defining the size of the Map we want to play, and the number of bombs to place on the map.

We also take 2 TileBase instances. These are the tiles we’ll be placing on the tilemap. We’ll cover their implementation later, but the nice part about this setup is MapInformation doesn’t need to know anything about the TileBase implementation; it just needs 2 tiles to work with.

When the game starts we need to initialize the board state by creating the map, placing the required bombs, then configuring the TileMap. While we could try and store game state in the TileMap directly, however I generally try and avoid that. Instead the TileMap will contain the definitive game model, and we will use the TileMap as a view for the game state. With that in mind, lets take a look at the InitializeBoard function:

private byte[,] m_grid;
private RevealedState[,] m_revealedState;
private Tilemap m_tileMap;

public enum RevealedState : byte
{
    Unexplored = 0,
    Flag = 1,
    Guessed = 2
}

private void InitializeBoard()
{
    // Argument checking
    ...

    m_tileMap = this.transform.Find("MinesweeperMap").GetComponent<Tilemap>();

    m_grid = new byte[SizeX, SizeY];
    m_revealedState = new RevealedState[SizeX, SizeY];

    m_tileMap.ClearAllTiles();

    // Place bombs
    System.Random rand = new System.Random();
    for (int i = 0; i < NumberOfBombes; i++)
    {
        // Dumb, brute-force approach to placing bombs
        while (true)
        {
            int x = rand.Next(0, SizeX);
            int y = rand.Next(0, SizeY);

            if (m_grid[x, y] == 0)
            {
                m_grid[x, y] = 1;
                break;
            }
        }
    }

    // Mark all the squares as unexplored
    for (int x = 0; x < SizeX; x++)
    {
        for (int y = 0; y < SizeY; y++)
        {
            m_revealedState[x, y] = RevealedState.Unexplored;
            m_tileMap.SetTile(new Vector3Int(x, y, 0), UnrevealedSquare);
        }
    }

    // Position camera
    ....
}

First we define an enum for storing the revealed state of each square. We then define 2 arrays, one for storing which squares have been revealed, and one for storing where bombs are located. The InitializeBoard function creates these arrays equal to the size of the board. We then do a brute-force approach to place the required bombs on the map.

Once we have our board model generated we are ready to create our Tilemap view. We start by finding the Tilemap component we want to use. We then call the ‘ClearAllTiles’ function on the Tilemap to clear out any existing state it may have.

The, for each tile on the board, we create an UnrevealedSquare tile on the Tilemap. We do this with the ‘SetTile’ method, which takes the (x,y) coordinates of the tile, and the TileBase to set. At the start of the game no tiles are revealed, so we initialize every square to the UnrevealedSquare tile.

Once the Tilemap is initialized, there are only two cases where we need to update it

  1. When a square is revealed, we need to change to to be a ‘RevealedSquare’ tile. This code is very similar to the call above, the only difference is the tile we are setting
m_tileMap.SetTile(new Vector3Int(square.x, square.y, 0), RevealedSquare);
  1. When an Unrevealed square is changed from Unflagged -> Flagged, or from Flagged -> Unflagged, we need to refresh the tile. We’ll discuss this more below, but this tells the Tile to refresh it’s rendering information by calling the GetTileData method
m_tileMap.RefreshTile(new Vector3Int(x, y, 0));

That concludes the Tilemap specific pieces in MapInformation, now lets turn our attention to implementing the actual Tiles

Implementing the Tiles

As we discussed earlier, we will be implementing 2 tiles for this project

  • Unrevealed Tile
  • Revealed Tile

Lets create them one at a time

Unrevealed Tile

This will represent a square the user hasn’t revealed yet. It can have one of two states:

  • Unflagged
  • Flagged

The user can toggle between these two states at will by right licking. Lets go ahead and create the ‘UnrevealedSquare’ class. This class will inherit from ‘TileBase’. As we discussed in the previous post, all tiles in Unity must inherit from TileBase. The responsibility of that class is to determine what the Tile will look like. This is done in the ‘GetTileData’ method, which takes the Tile position, the TileMap, and a TileData struct where we store data on how we want the tile rendered

Lets look at the final implementation, then discuss what each piece does

public class UnrevealedSquare : TileBase
{
    // Sprite for unexplored tiles
    public Sprite Unexplored;

    // Sprite for flagged tiles
    public Sprite Flag;

    public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
    {
        // Query if we are flagged or not
        MapInformation mapInfo = tilemap.GetComponent<Transform>().gameObject.GetComponentInParent<MapInformation>();

        RevealedState state = mapInfo.GetRevealedStateForSquare(position.x, position.y);

        if(state == RevealedState.Unexplored)
        {
            tileData.sprite = Unexplored;
        }
        else if(state == RevealedState.Flag)
        {
            tileData.sprite = Flag;
        }
    }
}

We see that it has 2 public fields of type Sprite; we will fill these in later in the Editor. They will contain the Sprites that we want to use for this Tile.

We then override the GetTileData method. This method is called by Unity in a couple cases

  • When the tile is first created
  • When the tile is refreshed

We are given information about the Tile we are operating on (if you remember from the previous article, a TileBase is not created for each tile. A single TileBase can operate on multiple tiles, so all state has to be passed in the method, we can’t rely on member fields.), and are expected to store rendering information in the provided TileData.

The first thing we do is retrieve the MapInformation behavior. We assume that it is placed on the parent object of the TileMap. Once we have that, we query the MapInformation to see what state the current tile is in. Depending on the answer to that, we set the Sprite to either the flag or the unexplored square.

That’s all we have to do here. When the user right clicks a square to flag or unflag a square, MapInformation calls RefreshTile, which in-turn calls this method. Since we re-query the current state, the Tile will pickup the new state and adjust the sprite accordingly.

RevealedSquare

The RevealedSquare works very in a very similar manner. Lets look at it’s implementation

public class RevealedSquare : TileBase
{
    /// <summary>
    /// Ordered list of sprites.  Indexes 0-9 should be sprites of the respective number
    /// </summary>
    public Sprite[] NumberedSprites;

    public Sprite BombSprite;

    public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
    {
        // Query the map information for how many bombs we are touching
        MapInformation mapInfo = tilemap.GetComponent<Transform>().gameObject.GetComponentInParent<MapInformation>();

        int count = mapInfo.GetBombsTouchingSquare(position.x, position.y);

        // Less than zero means we have a bomb
        if (count < 0)
        {
            tileData.sprite = BombSprite;
        }
        else
        {
            tileData.sprite = NumberedSprites[count];
        }

        m_count++;
    }
}

Once again we inherit from TileBase, and once again we have several public Sprite fields that we will set later from the Unity Editor. Once difference is this time we have an array of Sprites. We will need this array to have 8 elements, each cooresponding to it’s index (so index 0 will have the sprite for a square touching 0 bombs, index 1 will have the sprite for a square touching 1 bomb, etc). We also take a sprite for the bomb.

GetTileData should also look very familiar. First thing we need to do is find our MapInformation component so we can query the state of our current tile. We then do just that, calling a function that will return the number of bombs a given square is touching (or -1 if the square itself is a bomb). We then use that information to set the sprite for the Tile. We either set it to the bomb Sprite, or we use the returned number as an index into our array.

Up next

And that’s it! We now have our 2 Tiles fully implemented. With these Tiles and the MapInformation class we can now render a static board. Next time we’ll look at putting the final touches on the game, including responding to mouse clicks on the Tilemap, creating custom Editors for our Tiles, and actually creating the Tiles in Unity.