Search code examples
unity-game-engine2dcollision-detection

Detect which Tilemap cells have collided with a Collider2D in Unity


I have a Tilemap. This has a TilemapCollider2D component. Onto this are painted several tiles that each have their own sprite collider shape. However they are sprite tiles not prefabs. (They were not painted using the Prefab Brush.)

I also have a game object with a Collider2D (a CircleCollider2D in my case) with isTrigger set to true and no Rigidbody2D attached as this game object stays at a fixed position relative to its parent.

[EDIT: I discovered that this collider is actually using the Rigidbody2D of the parent game object. Without any rigid body, collisions wouldn't be detected at all.]

When the Collider2D enters/exits a tile, how can I identify the grid coordinates (Vector3Int) of that tile?

To be clear, I want to detect this from the Tilemap script. i.e. TilemapCollider2D.OnTriggerEnter2D() or TilemapCollider2D.OnCollisionEnter2D().

For example, in the picture below, I would want to have received OnTriggerEnter2D() for tiles B, C, D & E and to know their positions in the grid.

Circle collider overlapping with tiles B, C, D & E


Solution

  • As this question covers not just the collision between the Collider2D and the TilemapCollider2D, but between the Collider2D and each tile, it is not as simple as detecting the collision between the colliders.

    (Simple detection of collision between the colliders is covered in this question on answers.unity.com.)

    For the tilemap script to detect the entry and exit of each tile, it needs to respond to OnTriggerEnter2D(), OnTriggerStay2D() & OnTriggerExit2D().

    Here is my solution based on detecting when a CircleCollider2D intersects with each tile, not taking into account the geometry of any collider within the tiles. The intersections are an approximation (for efficiency) and may need adapting for other types of Collider2D.

    Overview

    Within OnTriggerEnter2D(), get the bounding box of the CircleCollider2D and from that identify which tiles it intersects with.

    For each of those tiles, get the world position of CircleCollider2D that is closest to that tile's centre. If that world position is within the tile, then there is an intersection. As well as handling this as desired, also add this tile's coordinates to a tracking list.

    Within OnTriggerStay2D(), do the same as OnTriggerEnter2D() but remove from the tracking list those tiles that no longer intersect and handle their intersection exits.

    Within OnTriggerExit2D() the two colliders have separated, so handle intersection exits for all tiles in the tracking list and clear the tracking list.

    Code example

    using System.Collections;
    using System.Collections.Generic;
    using System.Linq; // needed for cloning the list with .ToList()
    using UnityEngine;
    using UnityEngine.Tilemaps; // needed for Tilemap
    
    public class MyTilemapScript : MonoBehaviour
    {
        List<Vector3Int> trackedCells;
        Tilemap tilemap;
        GridLayout gridLayout;
    
        void Awake()
        {
            trackedCells = new List<Vector3Int>();
            tilemap = GetComponent<Tilemap>();
            gridLayout = GetComponentInParent<GridLayout>();
        }
    
        void OnTriggerEnter2D(Collider2D other)
        {
            // NB: Bounds cannot have zero width in any dimension, including z
            var cellBounds = new BoundsInt(
                gridLayout.WorldToCell(other.bounds.min),
                gridLayout.WorldToCell(other.bounds.size) + new Vector3Int(0, 0, 1));
    
            IdentifyIntersections(other, cellBounds);
        }
    
        void OnTriggerStay2D(Collider2D other)
        {
            // Same as OnTriggerEnter2D()
            var cellBounds = new BoundsInt(
                gridLayout.WorldToCell(other.bounds.min),
                gridLayout.WorldToCell(other.bounds.size) + new Vector3Int(0, 0, 1));
    
            IdentifyIntersections(other, cellBounds);
        }
    
        void OnTriggerExit2D(Collider2D other)
        {
            // Intentionally pass zero size bounds
            IdentifyIntersections(other, new BoundsInt(Vector3Int.zero, Vector3Int.zero));
        }
    
        void IdentifyIntersections(Collider2D other, BoundsInt cellBounds)
        {
            // Take a copy of the tracked cells
            var exitedCells = trackedCells.ToList();
    
            // Find intersections within cellBounds
            foreach (var cell in cellBounds.allPositionsWithin)
            {
                // First check if there's a tile in this cell
                if (tilemap.HasTile(cell))
                {
                    // Find closest world point to this cell's center within other collider
                    var cellWorldCenter = gridLayout.CellToWorld(cell);
                    var otherClosestPoint = other.ClosestPoint(cellWorldCenter);
                    var otherClosestCell = gridLayout.WorldToCell(otherClosestPoint);
    
                    // Check if intersection point is within this cell
                    if (otherClosestCell == cell)
                    {
                        if (!trackedCells.Contains(cell))
                        {
                            // other collider just entered this cell
                            trackedCells.Add(cell);
    
                            // Do actions based on other collider entered this cell
                        }
                        else
                        {
                            // other collider remains in this cell, so remove it from the list of exited cells
                            exitedCells.Remove(cell);
                        }
                    }
                }
            }
    
            // Remove cells that are no longer intersected with
            foreach (var cell in exitedCells)
            {
                trackedCells.Remove(cell);
    
                // Do actions based on other collider exited this cell
            }
        }
    }
    

    FYI

    From a separate discussion on forum.unity.com, I learned a couple of important points (that weren't obvious to me):

    1. For a TilemapCollider2D, OnTriggerEnter2D() & OnTriggerExit2D() are invoked at the level of the whole TilemapCollider2D, not at a per-tile level. i.e. just like any other collider type.
    2. Collision2D.contacts is an array of ContactPoint2D. ContactPoint2D.point is described as "The point of contact between the two colliders in world space." which implies to me that it is the point of intersection. However, it is actually the location at which the physics model wants to apply force. As a result, my solution uses trigger colliders and OnTriggerXxxx instead of OnCollisionXxxx and I work out the intersections myself.