Skip to content

Collisions

This page shows how to find out which tilemap, and which tile, a physics contact belongs to, for 2D and 3D colliders.

Concept

A tilemap's colliders do not live on the tilemap's GameObject. Each chunk, a hidden child of the tilemap covering 60 by 60 cells, carries its own colliders: EdgeCollider2D or PolygonCollider2D components for 2D, one MeshCollider for 3D. When your character touches a wall, the collider it reports belongs to a chunk.

Chunks copy the tilemap's layer and tag at every rebuild. Collision layer settings apply to them, and collision.gameObject.CompareTag("Ground") works if the tilemap is tagged Ground.

A contact tells you where the two colliders touched, which is on the tile's edge, not inside it. Converting that point straight to a cell lands on the tile half of the time and on the empty cell next to it the other half. The fix is to push the point a little along the contact normal before converting it.

Finding the tilemap

Walk up from the collider:

void OnCollisionEnter2D(Collision2D collision)
{
    STETilemap tilemap = collision.collider.GetComponentInParent<STETilemap>();
    if (tilemap != null)
    {
        // We hit a tilemap.
    }
}

In 3D, Collision.collider works the same way.

The tilemap also forwards the collision and trigger messages its chunks receive, so a script on the tilemap's own GameObject can implement OnCollisionEnter2D, OnCollisionStay2D, OnCollisionExit2D, OnTriggerEnter2D, OnTriggerStay2D, OnTriggerExit2D and their 3D counterparts. The argument is what the chunk received: the Collision2D or Collision seen from the chunk's side, so collision.gameObject is the object that hit the tilemap, or for a trigger the other object's collider.

Finding the tile

This helper takes a contact point and normal in world space and returns the cell holding a tile with a collider. The normal's direction depends on which of the two objects reports the contact, so it tries both sides.

using UnityEngine;
using CreativeSpore.SuperTilemapEditor;

public static class TileContactUtil
{
    public static bool TryGetHitCell(STETilemap tilemap, Vector3 worldPoint, Vector3 worldNormal, out Vector2Int cell)
    {
        Vector2 local = tilemap.transform.InverseTransformPoint(worldPoint);
        Vector2 localNormal = ((Vector2)tilemap.transform.InverseTransformDirection(worldNormal)).normalized;
        // A tenth of a cell is far enough to leave the edge and short enough to stay in the tile.
        float nudge = 0.1f * Mathf.Min(tilemap.CellSize.x, tilemap.CellSize.y);

        cell = TilemapUtils.GetGridPositionInt(tilemap, local - localNormal * nudge);
        if (HasCollider(tilemap, cell))
            return true;

        cell = TilemapUtils.GetGridPositionInt(tilemap, local + localNormal * nudge);
        return HasCollider(tilemap, cell);
    }

    public static bool HasCollider(STETilemap tilemap, Vector2Int cell)
    {
        Tile tile = tilemap.GetTile(cell.x, cell.y);
        return tile != null && tile.collData.type != eTileCollider.None;
    }
}

Use it from a 2D character:

using UnityEngine;
using CreativeSpore.SuperTilemapEditor;

public class TileTouchReporter2D : MonoBehaviour
{
    void OnCollisionEnter2D(Collision2D collision)
    {
        STETilemap tilemap = collision.collider.GetComponentInParent<STETilemap>();
        if (tilemap == null)
            return;

        for (int i = 0; i < collision.contactCount; ++i)
        {
            ContactPoint2D contact = collision.GetContact(i);
            if (TileContactUtil.TryGetHitCell(tilemap, contact.point, contact.normal, out Vector2Int cell))
            {
                Tile tile = tilemap.GetTile(cell.x, cell.y);
                float damage = TilemapUtils.GetTileParameter(tilemap, cell.x, cell.y, "damage", 0f);
                Debug.Log("Touched tile at " + cell + ", damage " + damage);
            }
        }
    }
}

And from a 3D one, with the tilemap's Collider Type set to 3D:

using UnityEngine;
using CreativeSpore.SuperTilemapEditor;

public class TileTouchReporter3D : MonoBehaviour
{
    void OnCollisionEnter(Collision collision)
    {
        STETilemap tilemap = collision.collider.GetComponentInParent<STETilemap>();
        if (tilemap == null)
            return;

        for (int i = 0; i < collision.contactCount; ++i)
        {
            ContactPoint contact = collision.GetContact(i);
            if (TileContactUtil.TryGetHitCell(tilemap, contact.point, contact.normal, out Vector2Int cell))
                Debug.Log("Touched tile at " + cell);
        }
    }
}

Each contact is converted on its own. A character standing across two tiles reports two contacts and gets two cells.

Tip

For a trigger there is no contact point. Take the other collider's bounds, convert its corners to cells, and check the cells in between with TileContactUtil.HasCollider, or pass the bounds as a local Rect to TilemapUtils.OverlapRect.

Tile collider data

Each tile in the tileset has a collider shape, edited in the Tile Properties window. From code it is Tile.collData, a TileColliderData:

Member What it is
type eTileCollider.None, Full or Polygon.
vertices The polygon, in tile space from (0, 0) at the bottom left to (1, 1) at the top right. Only meaningful for Polygon.
GetVertices() The shape for any type: null for None, the unit square for Full, vertices for Polygon.

The shape stored in the tile is the unrotated one. A flipped or rotated cell collides with the shape turned the same way as the tile; to get that shape yourself, clone the data and apply the cell's flags:

uint data = tilemap.GetTileData(cell.x, cell.y);
Tile tile = tilemap.Tileset.GetTile(Tileset.GetTileIdFromTileData(data));
if (tile != null && tile.collData.type == eTileCollider.Polygon)
{
    TileColliderData shape = tile.collData.Clone();
    shape.ApplyFlippingFlags(data);
    // shape.vertices is now oriented like the tile on screen.
}

Call Clone() first: ApplyFlippingFlags changes the array in place, and without the clone you would be editing the tileset.