Unity: How to create 2D Tilemap of any size programmatically

In one of my previous articles I showed a way to create a 2D Tilemap programmatically. Recently my new game called Hashi Flow has been released and there the Tilemap component has been used heavily again. In previous tutorial the board had a constant size of 11x11 cells, but it didn’t fit for the new game because there I needed to create boards of multiple sizes:

Example of 4x4, 5x5 and 7x7 boards

Approach described in the given tutorial will help you to build a Tilemap of any size purely programmatically!

As always, the source code is available at GitHub, please, find the link at the end of this tutorial.

Prerequisites

Unity version in use is 2019.4.3, for the rest of gotchas please refer to the above mentioned tutorial.

There are 3 separate parts: scene setup, data preparation and coding. If you are already familiar with the main concepts feel free to skip to the last part directly.

Part 1. Scene Setup

I will use a very simple rectangular tile:

To create a tile from that image first we should have a Tile Palette. To open one press Window -> 2D -> Tile Palette

Once Tile Palette view is open, click “Create new Palette” → fill the name and press “Create” and save it in the Tiles folder:

Then drag and drop the base tile image onto Tile Palette to create a tile.

Tiles setup

Lastly to set up a Tilemap, click right mouse button under Hierarchy view → 2D Object → Tilemap. A new game object Grid will appear.

Now in general, scene setup is done. But as a suggestion, paint a board of your basic size by hand — it will help you to organize the scene in a better way and much faster.

Mine base one is 4x4

Part 2. Data preparation

I’ll go through the Data preparation part very quickly as it has been already nicely explained in my two other tutorials here and here.

In short, we need to obtain the board setup from somewhere instead of having it hard-coded. I prefer to have a separate json file that is accessed in a runtime. But for simplicity sake this time I’ll keep the data in code.

Let’s create our first class GameData. You can think of this class as a holder that manages all the data required to create a given level. Usually I keep one instance of such an object loaded during the Splash screen (hence DontDestroyOnLoad method invocation on Awake)

public class GameData : MonoBehaviour
{
public string Size = "4x4";
private void Awake()
{
DontDestroyOnLoad(gameObject);
}
}

Note! In production the public Size field shouldn’t be exposed but it significantly eases our life during the development. To show different board sizes this field will be changed directly in Unity.

Last step is to create a new GameObject and attach the GameData script onto it. By now you should have the following objects on the scene:

Part 3. Tilemap creation

Moving to the most interesting part! Let’s make the board created via a script.

Make a new script — GameZone— and attach it to the Tilemap object (the child of the Grid object).

The given problem we are solving can be easily broken down into a few small steps to solve. Basically, we need to:

  • have access to the tile which will be drawn
  • draw the board, respecting the given size
  • make sure that the resulting board is correctly displayed on the scene

Let’s implement those steps one by one.

To have access to the tile which will be drawn I’ll use the Resources folder created under the Tiles folder where the base tile created previously will be stored.

Following a good practice of separation of concerns, we will make the TilesHolder class responsible for reading and providing the base tile (and any other possible tiles) to GameZone:

public class TilesHolder : MonoBehaviour
{
private Tile _baseTile;
private void Awake()
{
_baseTile = (Tile) Resources.Load("base", typeof(Tile));
}
public Tile GetBaseTile()
{
return _baseTile;
}
}

In order to access it from GameZone let’s attach this script to the Tilemap object. Now we can use it in the GameObject script. But there is no much use of it yet, so moving forward we will draw the board while respecting the given size.

First of all we should obtain Tilemap, TilesHolder and GameData objects in the GameZone script:

public class GameZone : MonoBehaviour
{
private Tilemap _gameZoneTilemap;
private TilesHolder _tilesHolder;
private GameData _gameData;
private void Awake()
{
_gameZoneTilemap = GetComponent<Tilemap>();
_tilesHolder = GetComponent<TilesHolder>();
_gameData = FindObjectOfType<GameData>();
}
}

Note! From my experience it is better to keep the initialization of fields in the Awake method and any required modifications in the Start method to prevent unexpected NullReferenceExceptions. There is a mechanism called Script Execution Order to control the order but I personally consider it as a workaround rather than a solution because it is far away from the scripts themselves and can be easily overlooked.

Breaking down the drawing part the algorithm will be as following:

  • get the given board size
  • find where to start the drawing
  • clear the current board (in case it was drawn already)
  • draw the board, respecting the size
  • clean the Tilemap (by compressing its bounds)
public class GameZone : MonoBehaviour
{
.....
private void Start()
{
var sizes = _gameData.Size.Split('x');
var origin = _gameZoneTilemap.origin;
var cellSize = _gameZoneTilemap.cellSize;
_gameZoneTilemap.ClearAllTiles();
var currentCellPosition = origin;
var width = int.Parse(sizes[0]);
var height = int.Parse(sizes[1]);
for (var h = 0; h < height; h++)
{
for (var w = 0; w < width; w++)
{
_gameZoneTilemap.SetTile(currentCellPosition, _tilesHolder.GetBaseTile());
currentCellPosition = new Vector3Int(
(int) (cellSize.x + currentCellPosition.x),
currentCellPosition.y, origin.z);
}
currentCellPosition = new Vector3Int(origin.x, (int) (cellSize.y + currentCellPosition.y), origin.z);
}
_gameZoneTilemap.CompressBounds();
}
}

To change the board size set the desired value of the GameData::Size public field.

2x2, 4x4 and 6x6 boards examples

Looks good already! But there is an obvious problem here. The smaller board doesn’t occupy the whole screen whereas the bigger one is rendered partially outside of the Camera view. But there are different possible solutions to fix this issue. I prefer to modify the Camera itself by altering its position and orthographicSize.

public class GameZone : MonoBehaviour
{
private const float CameraPositionModifier = 0.5f;
private const float CameraSizeModifier = 1.2f;
.....
private Camera _camera;
private void Awake()
{
.....
_camera = Camera.main;
}
private void Start()
{
.....
ModifyCamera(width);
}
private void ModifyCamera(int width)
{
var modifier = (width - 4) * CameraPositionModifier;
_camera.transform.position = new Vector3(
_camera.transform.position.x + modifier,
_camera.transform.position.y + modifier,
_camera.transform.position.z
);
_camera.orthographicSize = Mathf.Pow(CameraSizeModifier, (width - 4)) * _camera.orthographicSize;
}
}

You might be wondering how did I come up with those numbers? It is entirely empirical approach, you would need to play with it and see what works best for your game. In my case the minimum board size is 4x4 and the current maximum is 7x7.

The results after camera modifications look like this:

Final results

Afterwards

Well done finishing the tutorial! Should you have any questions please leave them in the comments section below.

The project source files can be found at this GitHub repository

Check it out in action in Hashi Flow.

Support

If you like the content you read and want to support the author — thank you very much!
Here is my Ethereum wallet for tips:
0xB34C2BcE674104a7ca1ECEbF76d21fE1099132F0

--

--

--

Serious software engineer with everlasting passion for GameDev. Dreaming of next big project. https://pudding.pro

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

How Can I Use MacAfee Framework Services?

CompArts Week 4

[HTB] Blocky — Writeup (OSWE-Prep)

6 steps to set up linux server with Nginx Docker & SSL

Motion Detection using OpenCV & Python

A Premiere to File Capabilities in Linux

Leetcode: Valid Anagram (Kotlin)

What Is API?

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Pudding Entertainment

Pudding Entertainment

Serious software engineer with everlasting passion for GameDev. Dreaming of next big project. https://pudding.pro

More from Medium

How to parse int and double value in Unity C#

Unity: Unit testing

Making Tic Tac Toe Using Unity 2D and C#

Login Form InWindows Form App. Using C#