12

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?

Tom
  • 2,688
  • 3
  • 29
  • 53
  • 2
    right now, my workaround is to use RaycastAll - which works, but it's utterly braindead because all those raycasters already raycast in the same frame, so making them raycast once more for no good reason, seriously? – Tom Jan 10 '22 at 12:57

7 Answers7

4

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();
}
Keepps
  • 41
  • 3
  • does it work for you? As written, for me this ALWAYS returns true. – Tom Apr 19 '22 at 22:23
  • Yes, I just created a Unity project specifically to double-check this and it works. To try it yourself, create a new Unity project. In the Hierarchy, create a UI panel that covers, say, half the screen. Attach a text component to the panel. Also attach the script above. but add a line in Update(), that updates the text of the text component with the status of pointerOverUI. Run it and move the mouse on and off the panel and you'll see it works. – Keepps Apr 25 '22 at 14:14
  • Ok, from @Lowelltech's answer I figure that this is always true because I have a CANVAS (just a canvas, not a panel or any other visible UI element) that spans the screen, and this stupid thing will be true if it's over a canvas, no matter if there's any actual UI element there or not. So to use this, apparently you have to break down your UI into lots of small canvasses. Which doesn't work if you have anything with dynamic size/position (like pop-ups, tooltips, windows that users can drag around, etc.) unless you also want to write custom code that dynamically resizes and moves the canvas. – Tom Sep 23 '22 at 08:28
  • 1
    For the reason given above, I don't accept this answer, as again it won't work for a large number of very common use-cases. – Tom Sep 23 '22 at 08:28
2

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.

Lowelltech
  • 21
  • 1
  • Won't work if you have multiple raycasters, which you will have if you follow the common performance recommendation to split your UI up so not everything is redrawn when you change one thing. – Tom May 07 '23 at 10:30
2

Your frustration is well founded: there are NO examples of making UI work with NewInput which I've found. I can share a more robust version of the Raycaster workaround, from Youtube:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;

/* Danndx 2021 (youtube.com/danndx)
From video: youtu.be/7h1cnGggY2M
thanks - delete me! :) */
 
 
public class SCR_UiInteraction : MonoBehaviour
{
public GameObject ui_canvas;
GraphicRaycaster ui_raycaster;
 
PointerEventData click_data;
List<RaycastResult> click_results;
 
void Start()
{
    ui_raycaster = ui_canvas.GetComponent<GraphicRaycaster>();
    click_data = new PointerEventData(EventSystem.current);
    click_results = new List<RaycastResult>();
}
 
void Update()
{
    // use isPressed if you wish to ray cast every frame:
    //if(Mouse.current.leftButton.isPressed)
    
    // use wasReleasedThisFrame if you wish to ray cast just once per click:
    if(Mouse.current.leftButton.wasReleasedThisFrame)
    {
        GetUiElementsClicked();
    }
}
 
void GetUiElementsClicked()
{
    /** Get all the UI elements clicked, using the current mouse position and raycasting. **/
 
    click_data.position = Mouse.current.position.ReadValue();
    click_results.Clear();
 
    ui_raycaster.Raycast(click_data, click_results);
 
    foreach(RaycastResult result in click_results)
    {
        GameObject ui_element = result.gameObject;
 
        Debug.Log(ui_element.name);
    }
}
}
 

So, just drop into my "Menusscript.cs"?

But as a pattern, this is terrible for separating UI concerns. I'm currently rewiring EVERY separately-concerned PointerEventData click I had already working, and my question is, Why? I can't even find how it's supposed to work: to your point there IS no official guide at all around clicking UI, and it does NOT just drop-on-top.

Anyway, I haven't found anything yet which makes new input work easily on UI, and definitely not found how I'm going to sensibly separate Menuclicks from Activityclicks while keeping game & ui assemblies separate.

Good luck to us all.

mujadaddy
  • 36
  • 2
  • I've used your solution by replacing the `ui_raycaster.Raycast()` method with the global`EventSystem.current.RaycastAll(click_data, click_result)` to avoid binding any GraphicRaycaster component. – Jérémie Boulay Oct 23 '22 at 20:34
  • I'm accepting this answer. I need to make ui_raycaster an array and add a loop going over them as I have multiple canvases, but this is the only answer that actually works. – Tom May 07 '23 at 10:49
0

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

halfer
  • 19,824
  • 17
  • 99
  • 186
Geeky Quentin
  • 2,469
  • 2
  • 7
  • 28
  • I've tried to trim this answer a bit - answers are meant to be for a wide future readership, and should be fulsome answers to the question. If you want to give extended feedback on a single answer, I wonder if comments and/or chat would be better for that. – halfer Sep 19 '22 at 10:06
0

Reply to @Lowelltech

Your solution worked for me except that instead of Mouse I used Touchscreen

// Obtain the current touch position.
var pointerPosition = Touchscreen.current.position.ReadValue();
rsauchuck
  • 21
  • 1
  • 4
-1

An InputSytem is a way to receive new inputs provided by Unity. You can't use existing scripts there, and you'll run into problems like the original questioner. Answers with code like "if(Input.GetMouseButton(0)" are invalid because they use the old system.

-1

A really simple approach of associating keyboard input events for a particular UI control. This was the default behaviour using the InputSystemUIInputModule.

using UnityEngine.InputSystem;

        private void Update()
    {
        if (EventSystem.current.IsPointerOverGameObject())
        {
            if (Keyboard.current.rightArrowKey.wasPressedThisFrame)
            {
                if (EventSystem.current.currentSelectedGameObject.name == "VirtualizingTreeView")
                {
                    Debug.Log("Right Arrow key pressed");
                }
            }
        }
    }
            
Mark Day
  • 19
  • 2