Search code examples
unity-game-engine

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


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/[email protected]/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?


Solution

  • 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.