The solution I have for you implies that you change the way clicks are detected (meaning removing the code you use and using the one I suggest instead). If you and your code are open to that change, you can create a simple component using the IPointerDownHandler
interface. This will force the use of a public void OnPointerDown(PointerEventData e)
method in your component, which will then be called at runtime whenever a click is detected.
using UnityEngine;
using UnityEngine.EventSystems; // <-- Important line.
public class Clickable : MonoBehaviour, IPointerDownHandler // <-- Interface.
{
public void OnPointerDown(PointerEventData data) // <-- Automatically called.
{
Debug.LogFormat("Click detected at {0}.", data.position);
}
}
You will need a PhysicsRaycaster
or Physics2DRaycaster
on your camera gameobject, depending on if you use 2d physics or 3d physics. No raycast calculation needed, it’s all done for you. You will also need an EventSystem
component in your scene, but this one is added automatically when you add a Canvas
.
I tried this solution myself and it worked: the OnPointerDown
method was not called when I pressed a button that was over the sprite having my custom Clickable
component.
For more information on all the interfaces you can use, [click here][1] and use the navigation menu on the left.
[EDIT]
Here is a second possibility, one using your custom detector (and tweaking it a bit). It’s a bit hacky and needs refactoring in my opinon. I will leave you the pleasure of doing that if you use it. Even then, it will still be hacky and it will be a hassle to maintain.
You will first need to place and manually adjust colliders on UI elements that should block clicks.
Then, you can use RaycastAll
instead of Raycast
to detect every game object in the way. By checking if a game object’s layer corresponds to the UI layer, you can determine if any UI is in the way. Here is the updates code:
using UnityEngine;
public class ClickDetector : MonoBehaviour
{
int uiLayer;
void Start()
{
// taking note of UI layer's id for posterity.
uiLayer = LayerMask.NameToLayer("UI");
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector2 mousePos2D = new Vector2(mousePos.x, mousePos.y);
// Raycast all the things.
RaycastHit2D[] hits = Physics2D.RaycastAll(mousePos2D, Vector2.zero);
GameObject hitObject = null;
for (int i = 0; i < hits.Length; i++)
{
if (hits*.transform.gameObject.layer == uiLayer)*
{
// We get out of here if UI is detected.
return;
}
if (hitObject == null && hits*.collider != null)*
{
// We take note, but do nothing yet.
// There may still be some UI laying around…
hitObject = hits*.collider.gameObject;*
}
}
Debug.Log(hitObject.name);
}
}
}
Why I prefer the first solution
Less code, obvious bugs, less hassle.
The first solution will do nothing if something (event system, physics raycaster, custom Clickable
component) is missing. It will outright bug and you will see it immediatly. Also, a friend once told me: “The best code is the code that doesn’t exist.” Less code is needed for the first solution. Is is clearly the best to me since it only needs an initial setup in the scene and every UI element will block raycasts until the end of time.
Why I don’t like the second solution
More code, subtle bugs, more hassle.
The second solution will have hidden bugs if something (UI layer, collider) is missing. Meaning everything will work nicely unless the bug’s conditions are met: clicking a button on top of a clickable sprite. It also needs to go through the trouble of putting and adjusting colliders on your UI. I haven’t fully tested collisions, but I know you can check the IsTrigger
box and your button will still block inputs to world objects.
_*[1]: Redirect to... title of new-page