1
\$\begingroup\$

I'm using code like the following with a tilemap and tilemap collider 2d:

var ray = Physics2D.RaycastAll(worldCoordinate, Vector2.zero);

Unfortunately, when I click where I have a sprite in my tilemap, the array value that comes back in ray only ever contains one entry which ends up being for the entire tilemap, rather than the sprite I clicked on in the tilemap.

Is there a special technique I should be using to cast a ray from a world coordinate, into the tilemap to determine which tile has been hit?

Please note: I'm using "z as y", so something like WorldToCell seems to not be enough as I also wish to match based on elevation and based on objects that are partially occluded by things in front of them.

Update, here's an image I can reference in discussion:

enter image description here

\$\endgroup\$
2
  • \$\begingroup\$ Raycast only focuses on the physical world. Actually it can't "see" the sprites. Maybe you should separate the moving objects out. \$\endgroup\$
    – Mangata
    Apr 25, 2022 at 3:50
  • \$\begingroup\$ I'm looking to identify the tiles in the map. But the issue is that I'm using elevation. So I need a way to translate world coordinates to actual tiles clicked. Not just the cell at the base level. \$\endgroup\$ Apr 25, 2022 at 11:57

1 Answer 1

2
\$\begingroup\$

Allow me to clarify more in this answer.

Raycast inspection(or any other physical inspection) only focuses on the physical world. They can not recognize one sprite on top of another, or something like that. They only care about the geometry of the colliders.

When you edit the scene with tilemaps, all the sprites in the tilemap share a single collider (TilemapCollider2D). So raycast can not tell which sprite you actually clicked. it can only tell you at what point does the collision occur.

So then the solution is clear: Let the sprite have its own gameobject and collider.

I found two ways to achieve it:

Option1:

Use TilemapExtrasPackage, Directly use ray inspection and sort the results.

Give each sprite a physical shape, and use gameobject brush to edit them into the scene. And you will get a lot of overlapping colliders like:

enter image description here

Use RaycastAll to find all colliders that the current screen point passes through, and sort them. Pick the first one.

Code:

void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        var hits = new List<RaycastHit2D>(Physics2D.RaycastAll(ray.origin, ray.direction));
        if (hits.Count > 0)
        {
            hits.Sort((l, r) =>
            {
                var p1 = l.transform.position;
                var p2 = r.transform.position;
                return (int)(((p1.y - p2.y) * 10 + (p2.z - p1.z)) * 10);
            });
            var target = hits[0].transform.GetComponent<SpriteRenderer>();
            target.color = target.color == Color.red ? Color.green : Color.red;
        }
    }
}

enter image description here

Precautions:

  1. Set sort axis as (0,1,-0.49) enter image description here
  2. The tilemap of interactive objects needs to set a smaller y value to facilitate sorting.

Option2:

If there are no additional interactable objects on the tile, the tile click area can be mapped algorithmically.

Core idea: split the grid into smaller-grained areas, record which grid covers each small area, traverse the grid from back to front, and refresh the record.

enter image description here

Assuming the grids are "half-height", That is to say a plot of 2 heights with the bottom side overlapping with a diamond behind it.(Most of the games I've seen are like this, and so are the Unity presets). Now divide a grid into 6 areas as follows,This is the smallest partition case:

enter image description here

To find out which areas a grid covers, we can do this:

For the grid of integer layers(Here is a multiple of 2), we can divide it into two parts:

  1. The plot where it is located(red)

  2. The area covered by each layer(blue and yellow):

enter image description here

For non-integer grids, just add the area occupied by the extra half-tile(pink):

enter image description here

When click hapens, Find out which area was clicked, and find the real grid that covered it last time. That's all.

Code:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class NewBehaviourScript : MonoBehaviour
{
    public GridLayout gridLayout;
    public Tilemap tilemap;
    private Dictionary<Tuple<int, int>, Vector3Int[]> tileInfoDict = new Dictionary<Tuple<int, int>, Vector3Int[]>();
    void Start()
    {
        var bounds = tilemap.cellBounds;
        var allTilePos = new List<Vector3Int>();
        for (int x = bounds.xMin; x < bounds.xMax; x++)
        {
            for (int y = bounds.yMin; y < bounds.yMax; y++)
            {
                for (int z = bounds.zMin; z < bounds.zMax; z++)
                {
                    var pos = new Vector3Int(x, y, z);
                    if (tilemap.HasTile(pos))
                    {
                        allTilePos.Add(pos);
                    }
                }
            }
        }
        allTilePos.Sort((left, right) =>
        {
            return (right.x + right.y) - (left.x + left.y);
        });
        foreach (var item in allTilePos)
        {
            DealInfo(item.x, item.y, item.z);
        }
    }
    private void DealInfo(int x, int y, int z)
    {
        var fullLayerCount = z / 2;
        var isHalfLayer = (z % 2) == 1;

        var posNow = new Vector3Int(x, y, z);
        var selfInfo = EnsureGetInfo(x, y);
        for (int index = 0; index < 6; index++)
        {
            selfInfo[index] = posNow;
        }
        for (int i = 1; i <= fullLayerCount; i++)
        {
            var midX = x + i;
            var midY = y + i;
            //mid
            var midInfo = EnsureGetInfo(midX, midY);
            for (int index = 0; index < 6; index++)
            {
                midInfo[index] = posNow;
            }
            //left
            var leftInfo = EnsureGetInfo(midX - 1, midY);
            for (int index = 3; index < 6; index++)
            {
                leftInfo[index] = posNow;
            }
            //right
            var rightInfo = EnsureGetInfo(midX, midY - 1);
            for (int index = 0; index < 3; index++)
            {
                rightInfo[index] = posNow;
            }
        }
        if (isHalfLayer)
        {
            var midX = x + fullLayerCount + 1;
            var midY = y + fullLayerCount + 1;
            //mid
            var midInfo = EnsureGetInfo(midX, midY);
            midInfo[2] = posNow;
            midInfo[5] = posNow;
            //left
            var leftInfo = EnsureGetInfo(midX - 1, midY);
            leftInfo[4] = posNow;
            leftInfo[5] = posNow;
            //right
            var rightInfo = EnsureGetInfo(midX, midY - 1);
            rightInfo[1] = posNow;
            rightInfo[2] = posNow;
        }
    }
    private Vector3Int[] EnsureGetInfo(int x, int y)
    {
        var key = new Tuple<int, int>(x, y);
        if (!tileInfoDict.ContainsKey(key))
        {
            tileInfoDict.Add(key, new Vector3Int[6]);
        }
        return tileInfoDict[key];
    }
    private int GetAreaIndex(Vector3 p1, Vector3 p2)
    {
        var v = p2 - p1;
        var gridX = gridLayout.cellSize.x;
        var gridY = gridLayout.cellSize.y;
        v = new Vector3(v.x * gridY / gridY, v.y, 0);
        var v1 = new Vector3(-1, 1);//left
        var v2 = new Vector3(1, 1);//right
        var len = Mathf.Sqrt(2) * gridY / 4;
        var m1 = Vector3.Project(v, v1).magnitude;
        var m2 = Vector3.Project(v, v2).magnitude;
        if (m1 > len)
        {
            if (m2 < len)
                return 1;
            else if (v.x > 0)
                return 3;
            else
                return 0;
        }
        else
        {
            if (m2 > len)
                return 4;
            else if (v.x > 0)
                return 5;
            else
                return 2;
        }
    }
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction);
            if (hit.collider != null)
            {
                Vector3Int cellPosition = gridLayout.WorldToCell(hit.point);
                var key = new Tuple<int, int>(cellPosition.x, cellPosition.y);
                if (tileInfoDict.ContainsKey(key))
                {
                    var center = gridLayout.CellToWorld(cellPosition);
                    var index = GetAreaIndex(center, hit.point);
                    var targetPos = tileInfoDict[key][index];
                    if (targetPos != null)
                    {
                        tilemap.SetTileFlags(targetPos, TileFlags.None);
                        if (tilemap.GetColor(targetPos) == Color.red)
                        {
                            tilemap.SetColor(targetPos, Color.green);
                        }
                        else
                        {
                            tilemap.SetColor(targetPos, Color.red);
                        }
                    }
                }
            }
        }
    }
}
\$\endgroup\$
13
  • 1
    \$\begingroup\$ I seem to understand , Do you mean a map with height difference like Ogre Battle? It may be calculated algorithmically. Later I will edit the answer to show it. : ) \$\endgroup\$
    – Mangata
    Apr 25, 2022 at 16:11
  • 1
    \$\begingroup\$ Hello, I checked your update, this method does not work for your grey pyramids. But I think it should be classified as interactive object maybe? I have a hunch that TilemapExtras will be the final solution. You can use multiple tilemaps, one for the ground and the others for interactive objects. Using gameobject brush in TilemapExtras you can draw independent gameobject with its own collider on tilemap. :-) \$\endgroup\$
    – Mangata
    Apr 27, 2022 at 7:03
  • 1
    \$\begingroup\$ @AlexanderTrauzzi Hello, I updated the answer to make it more readable, I think using the object brush would be better for your needs. Hope that helps :) \$\endgroup\$
    – Mangata
    May 9, 2022 at 15:50
  • 1
    \$\begingroup\$ Hello, you may check CustomPhysicsShape \$\endgroup\$
    – Mangata
    Jun 26, 2022 at 18:36
  • 1
    \$\begingroup\$ 1. When adding a PolygonCollider2D to gameobject, the sprite's physical shape is automatically obtained. You can drag a sprite into the scene and add a collider to it. 2. "object brush" is an editor tool, It is used when editing the scene. If you want to dynamically change the scene at runtime, you can simply load some prefabs to the specified location. \$\endgroup\$
    – Mangata
    Jun 26, 2022 at 20:57

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .