Preventing clicks from going through UI elements in Unity

Preventing clicks from going through UI elements in Unity

In 20 lines of code 🌟

UI systems in Unity

Unity has three different packages for building user interfaces (UI): Unity UI (a.k.a. uGUI), IMGUI and UI Toolkit and all of them are used for different things:

  1. IMGUI focuses on building small in-game debugging displays or for extending the Unity Editor itself.

  2. uGUI is the default GameObject-based UI system and is the most widely used.

  3. Finally, UI Toolkit is the newest package and should replace uGUI in the near future. If you are familiar with designing web interfaces (i.e. using HTML/CSS), UI Toolkit is supposed to be easier to learn as it is heavily inspired by web technologies.

While UI Toolkit is intended to become the recommended UI system for new development projects, it is not yet at feature parity with uGUI and, as such, Unity still advises using uGUI for the time being. You can check out the Comparison of UI systems in Unity on Unity docs for more information.

In this blog post, we exclusively talk about uGUI. The issue may also happen with UI Toolkit though, but I can't confirm.

The problem

Unity is notoriously bad for designing user interfaces. Just a quick search on Google can confirm this. Looking at these twoposts on Reddit, it really shows how much hate uGUI is getting! 😤 And for valid reasons: at first, the UI system is not very intuitive. It's hard to design reusable components, and it requires lots of Unity know-hows.

Also, uGUI seems to make it hard to implement basic features that anyone would expect in any UI builder. A good example of this is capturing clicks on UI elements (e.g. buttons, dropdowns, sliders, etc…). By default, meaning if you don't write custom C# code, clicking on UI elements within a scene will trigger events that will be consumed by both the UI and the game. As stated in the docs:

UI in Unity consumes input through the same mechanisms as game/player code. Right now, there is no mechanism that implicitly ensures that if a certain input – such as a click – is consumed by the UI, it is not also "consumed" by the game.

But why ??

Yes, it's indeed pretty disappointing that such a seemingly simple use case is not taken care of by the engine itself.

A workaround exists for the now deprecated InputManager via EventSystem.IsPointerOverGameObject to check if the mouse is above a UI element (“GameObject” here is referring to UI elements only). Unfortunately with the new InputSystem, while this technically still works, it is seen as a bad practice and Unity will log a warning every time you call this method within the InputAction callback. See Unity documentation:

Calling EventSystem.IsPointerOverGameObjectfrom within InputActioncallbacks such as InputAction.performed will lead to a warning. The UI updates separately after input processing and UI state thus corresponds to that of the last frame/update while input is being processed.

Basically, the previous workaround can't be used anymore and Unity didn't expose another method to achieve the same result. If you read Unity's documentation carefully, they give some pointers, but we can pretty much summarize them by saying “please, don't design your game so that both game and UI logic are needed at the same time”…

So, what can we do ? 🤔

The solution

What we can do is reproduce what EventSystem.IsPointerOverGameObject does, minus the warning. It's actually very simple and requires less than 20 lines of code: drum-rooooolllll

Graphic raycaster

A GraphicRaycaster is an important part of uGUI and allows us to cast rays from the camera to any point on a canvas and get the list of game objects that have been hit.

Also, if you ever created a Unity Canvas before, you already used a GraphicRaycaster without even knowing it. They are attached to any Canvas when you create one from the Unity Editor (right-click > UI > Canvas):

To cast a ray, we use the GraphicRaycaster.Raycast(PointerEventData, List<RaycastResult>) where:

  • PointerEventData represents the point from which to raycast, usually the position of the mouse

  • List<RaycastResult> is used to store references to the game objects that have been hit

Putting it all together in a simple C# script as follows (the important part is the `HasClickedOverUI` method 😉):

public class PlayerInputController : MonoBehaviour
{
    // The GraphicRaycaster of your Canvas game object
    [SerializeField]
    private GraphicRaycaster graphicRaycaster;
    // Struct to hold pointer data (mainly its position)
    private PointerEventData _clickData;
    // List containing all the UI elements hit by the raycast
    private List<RaycastResult> _raycastResults;

    private void Start()
    {
        _clickData = new PointerEventData(EventSystem.current);
        _raycastResults = new List<RaycastResult>();
    }

    public void OnClick(InputAction.CallbackContext context)
    {
        if (!context.performed)
        {
            return;
        }

        // Check that user did not click over a UI element
        if (HasClickedOverUI())
        {
            return;
        }

        // Game logic continues here...
        // For example, call other C# methods, play a sound, instantiate 
        // a game object, etc...
    }

    private bool HasClickedOverUI()
    {
        // Retrieve current mouse position
        _clickData.position = Mouse.current.position.ReadValue();
        // Clear previous results
        _raycastResults.Clear();
        // Instruct the raycaster to cast a ray from current mouse
        // and stores the results in the given array
        graphicRaycaster.Raycast(_clickData, _raycastResults);

        // Optional: log all the UI elements hit by the ray
        foreach (var raycastResult in _raycastResults)
        {
            Debug.Log($"Clicked in UI element: ${raycastResult}");
        }

        // Return a boolean that will tell us whether at least one
        // UI element has been clicked on 
        return _raycastResults.Count > 0;
    }
}

Take a look at the OnClick method which calls HasClickedOverUI before executing any game logic. This will effectively disable any processing in case the click was made wile over a UI element.

It's that simple! 🔥 However, one could argue that it actually does not prevent clicks to go through UI elements. It simply enables game objects to ignore clicks if they have been made over UI elements (potato, potahto).

Final words

While uGUI, Unity default UI system, is powerful and can be used to build even the most complex user interfaces, it definitely has a steep learning curve. Also, like we saw, some of the most basic tasks may require custom C# code.

Hopefully, the newest alternative, UI Toolkit, will simplify all of that and will make it easy to design and interact with in-game UIs. If not, I have my eyes on another solution that could benefit the whole game development community: EvolveUI (currently in closed-beta, March 2024). I might even write another article if I use it, who knows ? 😇