How to Script a 2D Tile Map in Unity3D

Prologue: The Back Story

A little while ago, a friend and mentor and I were talking about the tutorials I write for this site. I was talking about how I feel I should write a tutorial that deals more with scripting and less with components; and how I couldn’t decide what the concept for the tutorial would be. We went back and forth with ideas for a few hours with the pros and cons of each. He then reminded me of a time when he tutored me on a tiling engine and was explaining how algorithms weren’t as scary as I thought.
Suddenly, it hit me like a stampede. I should make a tutorial on making a working tile map system while explaining the editor concepts, the ideas behind the code, and ways you could extend it for a top down adventure game.

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.

Pre-Order The Complete Virtual Reality Game Development with Unity and learn to create immersive games and experiences by building 10 VR games. The course assumes no prior Unity or VR experience – We’ll teach you C#, Unity and 3D programming from the ground-up

Check it out on Zenva Academy and get Early Access!

Tutorial source code

Download link for tutorial
Link for Visio Alternatives here.

Section 1: The Boring Theory Section

If you are a beginner, you may have never heard of a tile map. A tile map is a procedurally generated level map that is created within a given set of rules. This in itself can be confusing and may draw questions, so we should break that down.
What are map generation rules?
map is a 5 x 5 grid
if a tile is marked unset, set a tile in that spot
if a tile would go outside of grid size, do not set a tile.
These are the basic rules you could employ for making a tile map system.

What programming keywords or loops will we use for the tile map system?
Enum, nested for loops, foreach loops, constructors, overloading, and if statements

Can this map system be modified to Isometric, Diametric or Hexagonal?
With this question, I am going to say this. If you can understand the rules, the fundamental math behind each style and what was done with the implementation, you could convert it to either an Isometric, Diametric or Hexagonal grid. To be completely honest, I have not been able to successfully do Isometric, Diametric, or Hexagonal maps to date.

Can this map system be used in an RTS game?
If you are a beginner, I do not recommend you start off with an RTS game. They are extremely difficult to prototype and fully implement. If you have a mentor or are shadowing a team of developers, I would highly suggest you talk to them and see what they think.
That being said, many of the classic RTS games did use a tiling system similar to this. So it can absolutely be used.
Now that these questions are done, I think we should actually talk about the math involved. The math involved isn’t as complicated as you might think, if you are like me and are deathly afraid of math.

What information do we need mathematically?
Size of images, the size of the grid you want to display, and the screen size. You should also have a good understanding of some linear math, multiplication, and positive / negative number counting.

I think this just about covers all of the theory behind making a tile map system. I’ve kept it language agnostic so this portion of the tutorial can be applied to whatever language you are learning. What I mean by language agnostic is this section can be applied to any programming language. If you decide to learn Typescript and Phaser, BabylonJs, LimeJS, Quintus, Unreal Engine, Construct2, HTML5, or any other programming language / Game Engine; You could use this portion of the tutorial and it will work!

Section 2: Planning the Code

In this section, we will be dealing with planning out our source code and then writing it. What I mean by this is, we will first create the UML Diagram of how the code should be structured and then actually write the code. It is important that I stress how imperative it is to plan your code first, and this holds true regardless of whether you design games, apps on any platform, and websites. Proper planning and documentation means that you will spend less time trying to figure out what you are trying to implement and how you want to implement it. It also means that you can much more easily add new features to the code and know exactly where it goes, what it inherits from, and if it conflicts with another portion of your code. For planning this out, I am using Microsoft Visio 2015. If you do not have Visio or the funds to purchase it, you can use Lucid Chart or Libra Office Draw instead.
Basic UML TME
As you can see, we separated everything according to what class it should be in. We made the Enum be in its own class called Tiles, TileSprite class is marked serializable and has no inheritance, and TilingEngine inherits from MonoBehaviour. We also wrote out our properties and methods. We separated the properties from the methods and made sure to add the parentheses to designate methods.

Section 2.5: Writing the Code

In this section, we will be writing the actual code. I  know you guys are elated to finally see some code and how we can apply it to Unity3D. Since this is with the beginner in mind, I will be explaining the code through self documenting code and with remarks after each code segment regarding it. First up, is the Tiles class.

using UnityEngine;
using System.Collections;

public enum Tiles
{
    Unset,
    Grass,
    Water,
    Brick,
    Sandstone
}

Here, we are making use of the enum keyword. If you are unfamiliar with the .Net keywords, I would suggest that you stop reading this tutorial and read this guide on all the C# keywords in the .Net Framework. In the code above, the compiler would see this as Unset = 0, Grass = 1, and so on. You could explicitly set this, however, you don’t need to in most cases.

Next up, is the TileSprite Class.

using System;
using UnityEngine;
using System.Collections;

[Serializable]
public class TileSprite
{
public string Name;
public Sprite TileImage;
public Tiles TileType;

public TileSprite()
{
Name = "Unset";
TileImage = new Sprite();
TileType = Tiles.Unset;
}

public TileSprite(string name, Sprite image, Tiles tile)
{
Name = name;
TileImage = image;
TileType = tile;
}
}

We declared a string as name, sprite as tile image, tiles as tile type. In the default constructor, we set the name to unset, the tile image as new sprite, and tile type as tiles.unset. What we are doing here in the default constructor is giving a string value to associate with the sprite and enum’s value of unset. Next up, we overload the default constructor with a method signature of string name, sprite image, and tiles tile. This is done so later on we can modify the name, image, and tile type later on

Last on the list is our TilingEngine class. I am going to break it down into smaller sections before revealing the full code.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Lean;

public class TilingEngine : MonoBehaviour
{
    public List<TileSprite> TileSprites;
    public Vector2 MapSize;
    public Sprite DefaultImage;
    public GameObject TileContainerPrefab;
    public GameObject TilePrefab;
    public Vector2 CurrentPosition;
    public Vector2 ViewPortSize;    
    
    private TileSprite[,] _map;
    private GameObject controller;
    private GameObject _tileContainer;
    private List<GameObject> _tiles = new List<GameObject>();

    private TileSprite FindTile(Tiles tile)
    {
        foreach (TileSprite tileSprite in TileSprites)
        {
            if (tileSprite.TileType == tile) return tileSprite;
        }
        return null;
    }
}

Alright, the properties on this one is fairly beefy. I’m sure you noticed a new using statement for Lean. That is because we are using an asset called LeanPool. It is a free asset in the asset store and it is designed to be used for object pooling. Object pooling is recycling objects rather than simply deleting them. It uses less memory in the long run and works fantastically for objects that will be reused extremely often. We have a list that has a parameter of tile sprite called tile sprite, a Vector 2 of MapSize, Sprite of default image, two gameobjects for tile container prefab and tile prefab, a vector 2 for current position, vector 2 for view port size. For the private properties we have; Tile sprite as an array called map, list with a parameter of gameobject called tiles, and 2 gameobject respectively called controller and tile controller.
Each of these properties will be discussed as we get to them in the script. On that note, our first method is Find tile with a parameter of tile. Inside we have a foreach loop that goes through each tile sprite and if it is equal to tile, it returns a tile. If it doesn’t equal tile, it returns null. Basic error handling here.
On to the next section of the code. I promise it won’t be as big as the first.

   private void DefaultTiles()
    {
        for (var y = 0; y < MapSize.y - 1; y++)
        {
            for (var x = 0; x < MapSize.x - 1; x++)
            {
                _map[x, y] = new TileSprite("unset", DefaultImage, Tiles.Unset);
            }
        }
    }

    private void SetTiles()
    {
        var index = 0;
        for (var y = 0; y < MapSize.y - 1; y++)
        {
            for (var x = 0; x < MapSize.x - 1; x++)
            {
                _map[x, y] = new TileSprite(TileSprites[index].Name, TileSprites[index].TileImage, TileSprites[index].TileType);
                index++;
                if (index > TileSprites.Count - 1) index = 0;
            }
        }
    }

Our first method is Default tile. Here we see a nested for loop, or a for loop inside of another for loop. Here we see Map size x and y in the loops as well as the var keyword. The var keyword means that you create a generic type that the compiler chooses which specific type it is at compile time. What the for loops are saying is pretty straight forward, however, I think it is always best to put it into plain English for full understanding.
Here is a variable of y and it equals 0. If y is less than the map size’s y minus 1, add to y. Here is a variable of x and it equals 0. If it is less than the map size’s x minus 1, add to x. Each time it loops through, the map’s array with an x and y value is given a new Tile sprite and it’s name should be unset, with the default image, and the enum’s value of Unset.
Next up is the Set tiles method. We declare an index that equals 0. The for loop that says, here is a variable of y and it equals 0. If y is less than the map size’s y minus 1, add to y. Here is a variable of x and it equals 0. If it is less than the map size’s x minus 1, add to x. Each time it loops through, the map’s array with an x and y value is given a new Tile sprite with the parameters of Tile sprites with an array of index that has a name, Tile sprites with an array of index of tile image, and Tile sprites with an array of index of tile type. add to index for each loop. Lastly, we have an if statement. If the index is greater than tile sprites’ counts minus 1, the index is equal to 0. The if statement is a little bit of error handling sprinkled on.

    private void AddTilesToWorld()
    {
        foreach (GameObject o in _tiles)
        {
            LeanPool.Despawn(o);
        }
        _tiles.Clear();
        LeanPool.Despawn(_tileContainer);
        _tileContainer = LeanPool.Spawn(TileContainerPrefab);
        var tileSize = .64f;
        var viewOffsetX = ViewPortSize.x/2f;
        var viewOffsetY = ViewPortSize.y/2f;
        for (var y = -viewOffsetY; y < viewOffsetY; y++)
        {
            for (var x = -viewOffsetX; x < viewOffsetX; x++)
            {
                var tX = x*tileSize;
                var tY = y*tileSize;

                var iX = x + CurrentPosition.x;
                var iY = y + CurrentPosition.y;

                if (iX < 0) continue;
                if (iY < 0) continue;
                if(iX > MapSize.x - 2) continue;
                if (iY > MapSize.y - 2) continue;

                var t = LeanPool.Spawn(TilePrefab);
                t.transform.position = new Vector3(tX, tY, 0);
                t.transform.SetParent(_tileContainer.transform);
                var renderer = t.GetComponent<SpriteRenderer>();
                renderer.sprite = _map[(int)x + (int)CurrentPosition.x, (int)y + (int)CurrentPosition.y].TileImage;
                _tiles.Add(t);
            }
        }
    }

Now we have the Add tiles to world method. This one is the heart of most of the code. We have a foreach loop with the parameter of Game object from within tiles. With each loop, lean pool is to despawn an object. Lean pool will despawn the tile container and the tile container is set to Lean pool to spawn the Tile container prefab.
The variable called tile size is set to 0.64 float.
The variable called view offset x is set to the view port size of x divided by 2 float.
The variable called view offset y is set to the view port size of y divided by 2 float.
The nested for loop starts with y instead of x this time. The reason being is because it is considered industry standard to do so. The variable of y is equal to the negative view offset of y. if y is less than the view offset y, add to y. The variable of x is equal to the negative view offset of x. if x is less than the view offset of x, add to x.
We create a variable called tX and it is equal to x multiplied by the tile size.We create a variable called tY and it is equal to Y multiplied by the tile size.
We create a variable called iX and it is equal to x plus the current position of x.
We create a variable called iY and it is equal to y plus the current position of y.
Now for the if statements. if iX is less than 0, continue on. if iY is less than 0, continue on. if iX is greater than the map size’s x minus 2, continue on. if iY is greater than the map size’s y minus 2, continue on.
We create a variable called t and it is set to Lean pool’s spawn for Tile Prefab. t’s transform position is equal to a new Vector 3 of tX, tY, and 0. We set the parent of the t’s transform to the tile container’s transform. We create a variable called renderer and tell t to get the component of Sprite Renderer. then we tell tiles to add t.

    public void Start()
    {
        controller = GameObject.Find("Controller");
        _map = new TileSprite[(int)MapSize.x, (int)MapSize.y];

        DefaultTiles();
        SetTiles();
    }

    private void Update()
    {
        AddTilesToWorld();
    }

Last 2 methods and then we move on to the next section. We have the start method. We instantiate the controller object by having the Game object’s find method to find an object called controller. Now we set the map to be a new Tile sprite, the parameters of it are explicitly converted map size’s x and y to integer values to coincide with how the array is set up. We now call the Default tiles method and set tiles method. The update method is now up. Inside Update we simply call the Add tiles to world method.
Finally, let’s look at the TilingEngine class as a whole.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Lean;

public class TilingEngine : MonoBehaviour
{
    public List<TileSprite> TileSprites;
    public Vector2 MapSize;
    public Sprite DefaultImage;
    public GameObject TileContainerPrefab;
    public GameObject TilePrefab;
    public Vector2 CurrentPosition;
    public Vector2 ViewPortSize;    
    
    private TileSprite[,] _map;
    private GameObject controller;
    private GameObject _tileContainer;
    private List<GameObject> _tiles = new List<GameObject>();
 
    public void Start()
    {
        controller = GameObject.Find("Controller");
        _map = new TileSprite[(int)MapSize.x, (int)MapSize.y];

        DefaultTiles();
        SetTiles();
    }

    private void DefaultTiles()
    {
        for (var y = 0; y < MapSize.y - 1; y++)
        {
            for (var x = 0; x < MapSize.x - 1; x++)
            {
                _map[x, y] = new TileSprite("unset", DefaultImage, Tiles.Unset);
            }
        }
    }

    private void SetTiles()
    {
        var index = 0;
        for (var y = 0; y < MapSize.y - 1; y++)
        {
            for (var x = 0; x < MapSize.x - 1; x++)
            {
                _map[x, y] = new TileSprite(TileSprites[index].Name, TileSprites[index].TileImage, TileSprites[index].TileType);
                index++;
                if (index > TileSprites.Count - 1) index = 0;
            }
        }
    }

    private void Update()
    {
        AddTilesToWorld();
    }

    private void AddTilesToWorld()
    {
        foreach (GameObject o in _tiles)
        {
            LeanPool.Despawn(o);
        }
        _tiles.Clear();
        LeanPool.Despawn(_tileContainer);
        _tileContainer = LeanPool.Spawn(TileContainerPrefab);
        var tileSize = .64f;
        var viewOffsetX = ViewPortSize.x/2f;
        var viewOffsetY = ViewPortSize.y/2f;
        for (var y = -viewOffsetY; y < viewOffsetY; y++)
        {
            for (var x = -viewOffsetX; x < viewOffsetX; x++)
            {
                var tX = x*tileSize;
                var tY = y*tileSize;

                var iX = x + CurrentPosition.x;
                var iY = y + CurrentPosition.y;

                if (iX < 0) continue;
                if (iY < 0) continue;
                if(iX > MapSize.x - 2) continue;
                if (iY > MapSize.y - 2) continue;

                var t = LeanPool.Spawn(TilePrefab);
                t.transform.position = new Vector3(tX, tY, 0);
                t.transform.SetParent(_tileContainer.transform);
                var renderer = t.GetComponent<SpriteRenderer>();
                renderer.sprite = _map[(int)x + (int)CurrentPosition.x, (int)y + (int)CurrentPosition.y].TileImage;
                _tiles.Add(t);
            }
        }
    }

    private TileSprite FindTile(Tiles tile)
    {
        foreach (TileSprite tileSprite in TileSprites)
        {
            if (tileSprite.TileType == tile) return tileSprite;
        }
        return null;
    }
}

Now that we have created our scripts, we can now go into Unity and set everything up to see it in action.

Section 3: Building the Scene in Unity3D

Open a new project in Unity. Set it to be a 2D project since this was designed with 2D in mind. In the assets folder, create 2 folders; One is Images and the other is scripts.
2016-01-22 (2)
Next up, add your scripts to the scripts folder or create 3 scripts with the same names as the classes spoken about in Section 2. Either write the code by hand if you haven’t done so already, or copy and paste the code in if you wrote it out while reading section 2.
2016-01-22 (3)

Using Microsoft Paint or whatever image editing / creation program make 5 separate images with the colours (blank, grey, green, beige, and blue) and add them to the Images folder.
2016-01-22 (4)

Open the asset store and search for “Lean Pool”, download and import it into the project.
2016-01-22 (5)

Add 3 empty game objects. Name one controller, another TileContainer, and third one Tile.
2016-01-22 (9)

On the game object called tile, add a Sprite Renderer component
2016-01-22 (10)
On the controller, add the Tiling Engine script onto it. make the x and y to be 100. Default image is blank 1, Tile Container prefab attach the tile container object, Tile Prefab attach the tile object, current position x and y is 50. View Port size x is 11 and y is 39.
2016-01-22 (11)

Minus down the Tile Sprites arrow just under the Tiling Engine script.
2016-01-22 (12)

Change the size to 4. Name each one to what it should look like (according to the enums), set the image according to the name, and set the tile type to be what you named it.
2016-01-22 (13)

Run the project and You now have a 2D top down tile map displayed on the game screen.
2016-01-22 (15)