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:
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.
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.
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 theStart
method to prevent unexpectedNullReferenceExceptions
. There is a mechanism calledScript 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.
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:
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