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):
- 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.
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.
isTrigger=false
and I had to put them on special layers with different layer collision settings in Project Settings > Physics 2D to avoid other objects getting stuck on these tiles. This has resulted in OnCollisionEnter2D working, but behaviour is sporadic. On first contact, e.g. with tiles C & D, I get immediate collisions, but it's only when the circle reaches two grid squares lower that I get collisions for all the other tiles. Weird. Any ideas? – Paul Masri-Stone