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.