0
votes

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

2
@derHugo Yes I'd looked at that but it was doing things from the perspective of the circle object detecting, rather than the tilemap so I wasn't sure it was applicable, and I wanted to avoid the raycasting route if possible.I've done more experimenting and... I discovered that my circle collider was automatically making use of the parent's Rigidbody2D, which I wasn't expecting, but it explains why collision/trigger detecting has been happening.Paul Masri-Stone
I followed those instructions and made both colliders 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
Weird behaviour related to this bug perhaps? stackoverflow.com/questions/53462798/… Or is there a more sane explanation and solution?Paul Masri-Stone
Just to wrap up these comments, I got an excellent explanation of the weird behaviour from a couple of the folks at Unity. forum.unity.com/threads/…Paul Masri-Stone

2 Answers

1
votes

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.
0
votes

not sure about the set up you have, but I would suggest trying the colliders on the letter objects, they are children of the tile map right?

I have set up like this:

Tilemap

-A - has colider2d and rigidbody2d

-B - has colider2d and rigidbody2d

-C - has colider2d and rigidbody2d

-D - has colider2d and rigidbody2d

Circle - has circle collider2d and a circle script

the following is in the circle script and gives the exact trigger object

private void OnTriggerEnter2D(Collider2D collider)
{
    Debug.Log($"touched {collider.name}");
}

just note that you need at least one rigidbody to get a trigger but you can swap from the letters to the circle if you need.