Unity3D New Input System: Is it really so hard to stop UI clickthroughs (or figure out if cursor is over a UI object)?

Viewed 2586

Even the official documentation has borderline insane recommendations to solve what is probably one of the most common UI/3D interaction issues:

If I click while the cursor is over a UI button, both the button (via the graphics raycaster) and the 3D world (via the physics raycaster) will receive the event.

The official manual: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.2/manual/UISupport.html#handling-ambiguities-for-pointer-type-input essentially says "how about you design your game so you don't need 3D and UI at the same time?".

I cannot believe this is not a solved problem. But everything I've tried failed. EventSystem.current.currentSelectedGameObject is sticky, not hover. PointerData is protected and thus not accessible (and one guy offered a workaround via deriving your own class from Standalone Input Module to get around that, but that workaround apparently doesn't work anymore). The old IsPointerOverGameObject throws a warning if you query it in the callback and is always true if you query it in Update().

That's all just mental. Please someone tell me there's a simple, obvious solution to this common, trivial problem that I'm just missing. The graphics raycaster certainly stores somewhere if it's over a UI element, right? Please?

4 Answers

I've looked into this a fair bit and in the end, the easiest solution seems to be to do what the manual says and put it in the Update function.

bool pointerOverUI = false;

void Update()
{
    pointerOverUI = EventSystem.current.IsPointerOverGameObject();
}

Reply to @Milad Qasemi's answer

From the docs you have attached in your answer, I have tried the following to check if the user clicked on a UI element or not.

// gets called in the Update method
if(Input.GetMouseButton(0) {
    int layerMask = 1 << 5;

    // raycast in the UI layer
    RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero, Mathf.Infinity, layerMask);

    // if the ray hit any UI element, return
    // don't handle player movement
    if (hit.collider) { return; }

    Debug.Log("Touched not on UI");
    playerController.HandlePlayerMovement(x);
}

The raycast doesn't seem to detect collisions on UI elements. Below is a picture of the Graphics Raycaster component of the Canvas:

enter image description here

Unity documentation for this issue with regard to Unity.InputSystem can be found at https://docs.unity3d.com/Packages/com.unity.inputsystem@1.3/manual/UISupport.html#handling-ambiguities-for-pointer-type-input.

IsPointerOverGameObject() can always return true if the extent of your canvas covers the camera's entire field of view.

For clarity, here is the solution which I found worked best (accumulated from several other posts across the web).

Attach this script to your UI Canvas object:

public class CanvasHitDetector : MonoBehaviour {
    private GraphicRaycaster _graphicRaycaster;

    private void Start()
    {
        // This instance is needed to compare between UI interactions and
        // game interactions with the mouse.
        _graphicRaycaster = GetComponent<GraphicRaycaster>();
    }

    public bool IsPointerOverUI()
    {
        // Obtain the current mouse position.
        var mousePosition = Mouse.current.position.ReadValue();

        // Create a pointer event data structure with the current mouse position.
        var pointerEventData = new PointerEventData(EventSystem.current);
        pointerEventData.position = mousePosition;

        // Use the GraphicRaycaster instance to determine how many UI items
        // the pointer event hits.  If this value is greater-than zero, skip
        // further processing.
        var results = new List<RaycastResult>();
        _graphicRaycaster.Raycast(pointerEventData, results);
        return results.Count > 0;
    }
}

In class containing the method which is handling the mouse clicks, obtain the reference to the Canvas UI either using GameObject.Find() or a public exposed variable, and call IsPointerOverUI() to filter clicks when over UI.

Related