Unity 2D Tile Map Tutorial – destructible tiles

The unity 2D tile map system is a very efficient and powerful tool that enables you to quickly prototype and build quality games. Because it’s a relatively new system, developpers may don’t know how to deals with it. Probably beginners only use this system to buid maps but they forgot that this system can be « easely » customizable.

As this system is free and directly available into the Unity editor, its pertinent to invest time to build extra functionalities around this system.

In this tutorial, we’ll be focusing on how to make a destructible tile because this feature is commonly asked. I’ve seen a lot of topic dealing with it whitout giving a clear answer. So,You’ll find it there.

Prerequisites

This tutorial assume that you have a good knowledge of the basic unity tilemap system. If not, there is a lot of good tutorials, here is some links :

Download the project

You can work directly within the project right here : Github

Getting Started

As you can see, destructible tiles have a broken sprite wich is rendered when their life is under 50%. This properties are not available within the built-in system. We’ll need to create our customizable tile with these 2 properties.

Let’s start by creating a simple tile map like this. You can use my empty project. I’ll Assume that the palette is already prepared. If you need some assets, i higly recommands you to download this one

Create the map

Let’s make a simple map like below but if you want to make it more complex, you can.

Basic tile map for the tutorial

Now you have designed the map, you have to add colliders. We’ll use the dedicated tile map collider. In addition of it, we’ll use a composite collider wich will merge all single colliders into one.

Don’t forget to set Rigidbody to Kinematic

Create the player

Now, we have to create a player who’ll shoot some projectiles. Let’s just make a basic character with rigidbody and collider like that

Basic player

The scene is set up and we’ll write our first line of code. The 2 followings scripts are only for the player. That’s the shooter and the projectil itself.

Projectile.cs

using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class Projectile : MonoBehaviour
{
    #region Properties

    private Rigidbody2D rigidbodyReference;
    private CircleCollider2D colliderReference;

    [SerializeField]
    private float velocity;
    [SerializeField]
    private LayerMask whatIsDestructible;

    private RaycastHit2D hit;
    private Vector3 originalVelocity;

    #endregion

    #region Implementation

    public void ShootTo(Vector2 pDirection)
    {
        rigidbodyReference.velocity = pDirection * velocity;
        originalVelocity = rigidbodyReference.velocity;
    }

    #endregion

    #region Unity callbacks

    public void Awake()
    {
        rigidbodyReference = GetComponent<Rigidbody2D>();
        colliderReference = GetComponent<CircleCollider2D>();
    }

    public void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag != "Player") {
            Destroy(this.gameObject);
        }
    }

    #endregion
}

ProjectileShooter.cs

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

public class ProjectileShooter : MonoBehaviour
{
    #region Properties

    public GameObject projectilePrefab;

    #endregion

    #region Implementation

    private Projectile InstantiateNewProjectile()
    {
        GameObject _newProjectile = Instantiate(projectilePrefab) as GameObject;
        _newProjectile.transform.position = transform.position;
        return _newProjectile.GetComponent<Projectile>();
    }

    public void ShootTo(Vector2 pDirection)
    {
        Projectile _newProjectile = InstantiateNewProjectile();
        _newProjectile.ShootTo(pDirection);
    }

    #endregion

    #region Unity callbacks

    public void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Vector2 worldMousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            ShootTo((worldMousePosition - (Vector2)transform.position).normalized);
        }
    }

    #endregion

}

Now script are ready, let’s create a prefab for the projectile like this

Don’t forget to set Gravity scale to 0

Let’s add the projectil shooter to the player and try it

Don’t forget to set tag to “Player”

Create the destructible Tile

Tiles are scriptable object that can be customized, for that, we’ll need to inherits from Tile class base. We’ll just override the StartUp() function to initialize some additional data :

  • tilePosition : Position of the tile in the grid
  • startLife : Life a the creation of the tile

We’ll create a specific function which will be called when the tile receive damage. It will be in charge of changing the sprite or removing it if the tile has been destructed.

Create the scriptable tile

Let’s create a function in the script to be able create a scriptable tile in the editor.

using UnityEditor;
using UnityEngine;
using UnityEngine.Tilemaps;

public class DestructibleTile : Tile
{
    #region Properties

    [Space(20)]
    [Header("Destructible Tile")]
    /// <summary>
    /// Life remaining before destroy tile
    /// </summary>
    public float life;
    private float StartLife;

    /// <summary>
    /// Sprite to display when life is below 50%
    /// </summary>
    public Sprite brokenSprite;

    public ITilemap tileMap;
    public Vector3Int tilePosition;

    #endregion

    #region Tile Overriding

    public override bool StartUp(Vector3Int position, ITilemap tilemap, GameObject go)
    {
        StartLife = life;

        //Store some data
        this.tileMap = tilemap;
        this.tilePosition = position;

        return base.StartUp(position, tilemap, go);
    }
    #endregion

    #region Implementation

    /// <summary>
    /// Apply damage to the tile
    /// </summary>
    /// <param name="pDamage"></param>
    public void ApplyDamage(float pDamage)
    {
        life -= pDamage;
        if (life < StartLife / 2 && base.sprite != brokenSprite)
        {
            base.sprite = brokenSprite;
        }
        if (life < 0)
        {
            base.sprite = null;
        }
    }
    #endregion

    #region Asset DataBase

    [MenuItem("Assets/MateriaGame/DestructibleTile")]
    public static void CreateDestructibleTile()
    {
        string path = EditorUtility.SaveFilePanelInProject("Save Destructible Tile", "DestructibleTile_", "Asset", "Save Destructible Tile", "Assets");

        if (path == "")
            return;

        AssetDatabase.CreateAsset(ScriptableObject.CreateInstance<DestructibleTile>(), path);
    }
    #endregion
}

Go now in the editor, and create a new instance of a destructible tile. Fill all information. For the destructible environnement, i recommend to create a new palette wich will host all destructible tiles. Once it’s done, place some destructible tile into the scene :

Collide with the tile map

As said earlier in the tutorial, the collider is attached to the tilemap and not to the tile, so we need a script that will handle collisions. Here is the code of the script.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class DestructibleTileMap : MonoBehaviour
{
    #region Properties
    private Tilemap tileMap;
    private GridLayout grid;
    private Vector3Int tilePosition;
    #endregion

    #region Unity callbacks
    public void Start()
    {
        tileMap = GetComponent<Tilemap>();
        grid = GetComponentInParent<GridLayout>();
    }
    #endregion

    public void Damage(Projectile projectile, Vector3 pContactPoint)
    {
        TileBase tileToDamage = tileMap.GetTile(grid.WorldToCell(pContactPoint));
        if (!Equals(tileToDamage, null))
        {
            if (tileToDamage is DestructibleTile)
            {
                ((DestructibleTile)tileToDamage).ApplyDamage(10);
                tileMap.RefreshTile(((DestructibleTile)tileToDamage).tilePosition);
            } 
        }
    } 
}

Get tile coordinate on collision

You can see that we can get a tile using a world position. This feature is natively provided by the unity tilemap built-in system. That’s by this way that we retrieve the tile on wich we’ll apply damage and this is this tile that we’ll refresh.

So, add this component to the tile map, and create a new layer called “Destructible” Assign it to the tile map

Send the position on the tilemap

It may be the most difficult part of this tutorial. The collider is not attached to the tile, it’s attached to the tilemap. When you hit the tile map collider, it actually doesn’t tell you wich tile you have hitten. You need to calculate the tile position when collision occurs.

This is how projectile will hit the tile map, we need to get the position (0,0)

So, how to determine which tile has been hitten. We’ll use the normal of the hitten vertice.

Normal and vertice explanation

Now we know that, we easely can get the tile by inversing the normal to get the tile, but there is something i didn’t mentionned about.

The difficulty is that when we use a trigger, we don’t have the collision point. Of course, you can use collider without trigger, like that, you can have simply the contact point. On this tutorial, we assume that our game is in the most unfavorable position, we have to use Trigger.

For this,  we’ll cast a circle when OnTriggerEnter2D is called by the unity engine. So, we’ll be able to get the contact point.

Modify the projectile to get the collision point

Update the projectile.cs script as below

    public void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag != "Player")
        {
            hit = Physics2D.CircleCast(transform.position, colliderReference.radius, rigidbodyReference.velocity.normalized, 0.1f, whatIsDestructible);
            if (hit)
            {
                DestructibleTileMap damageable = hit.collider.gameObject.GetComponent<DestructibleTileMap>();
                if (!Equals(damageable, null))
                {
                    Vector2 directionHit = (hit.point - (Vector2)transform.position).normalized;
                    damageable.Damage(this, hit.point + (directionHit.normalized * 0.5f));
                }                
            }
            Destroy(this.gameObject);
        }
    }

Update the prefab of the projectile to select the destructible layer

If you tested the game right now, be aware when you hit a destructible tile, you are modifying directly the scriptable tile. It means that all modifications will be persistent !

Create a copy of scriptable tiles

As you may know, Scriptable Objects in unity, when is modified apply its modification to all reference to this scriptable object and keep them persistent. In addition of placing destructible tile into the map, we need to create an instance, wich will be a copy of this tile when the map is created. By this way, modification will be applied to the single instance, not to the entire destructible tiles.

We need to update the DestructibleTile component as following

public void Start()
{
    tileMap = GetComponent<Tilemap>();
    grid = GetComponentInParent<GridLayout>();

    foreach (Vector3Int position in tileMap.cellBounds.allPositionsWithin)
    {
        TileBase t = tileMap.GetTile(position);
        if (!Equals(t, null))
        {
            if (t is DestructibleTile)
            {
                DestructibleTile dt = Instantiate(t) as DestructibleTile;
                dt.StartUp(position, dt.tileMap, dt.gameObject);
                tileMap.SetTile(position, dt);
            }
        }
    }
}

That’s all. You can launch the game right now and notice that all goes well ! We have a destructible tilemap totally functional

To conclude, the unity 2D tile map system is very powerful but it’s a little complicated to find complete example. That’s is why i write this article. I hope you find what you were looking for, and that’s will improve your game.

If you liked it, please share this article and follow me on Twitter !
Thanks you for reading, feel free to ask me questions.

Posted in Featured, Tutorials, Unity 2D Tile Map and tagged , .

Leave a Reply

Your email address will not be published. Required fields are marked *