Catch pointer events by multiple gameObjects

I noticed if there is a panel in front of another, mouse events will not reach panels in the back. Is there a way around? I guess it is not working because RayCast is blocked. But can’t RayCasts get multiple hits? Why was it designed like this?

Example Case: The panel on front uses OnPointerClick event and panel on the back uses OnPointerEnter. When you come over panels I want OnPointerEvent on the back panel to be called and when I click I want OnPointerClick to be called.

6 Answers

6

No one correct solution here, tat’s why I add my one:

public void OnPointerDown( PointerEventData eventData )
{
    
    {
        if( eventData.used ) // Check mark
        {
           
            return;
        }
        eventData.Use(); // Mark object as used

        List<RaycastResult> raycastResults = new List<RaycastResult>();
        EventSystem.current.RaycastAll( eventData, raycastResults );
        foreach( var raycastResult in raycastResults ) // send for all other receivers
        {
            var newTarget = ExecuteEvents.GetEventHandler<IPointerDownHandler>( raycastResult.gameObject );
            ExecuteEvents.Execute( newTarget, eventData, ExecuteEvents.pointerDownHandler );
        }
    }
}

Use the layers.

You can set the blocking mask directly on your canvas.

UPDATE:

FOUND THE SOLUTION

You need to have your 2 Canvas set to Screen Space Camera and affect the same Camera to the canvas

What if I want both panels on same canvas?

What for?

Okay, seems like I misunderstood the whole canvas thing. What you say works. Do you know if this will work if I want to catch same event in different panels at the same time?

Yes. If it is in 2 separate layers

Actually, It doesn't work. I don't think you understand what I mean or I don't understand you. When I add CanvasGroup on the panel in the front and set "BlocksRaycast" to false, I can catch events in the back but not in the front. If I don't add CanvasGroup, I can catch events in the front but not back. I am actually looking for a way to catch events in both panels. I can't put them in seperate canvas because panel in the front is child object of panel in the back.

What you are looking for is [Physics.RaycastAll][1]. This returns all hits along the ray instead of the first one. Of course this means it returns an array of RaycastHits which you will need to loop through.

Here is an example of use. Hopefully this helps in allowing for you to have overlapping objects and multiple events:

void SendEvents()
{
     Ray ray = rayFromMouse; //the ray you are making
     RaycastHit[] hits; //the array of hits. 

     hits = Physics.RaycastAll(rayFromMouse);

     if(hits.Length > 0)
     {
          for(int i = 0; i < hits.Length; i++)
          {
               hits*.transform.GetComponent<MyEventScript>().CallEvent();*

}
}
}
[1]: Unity - Scripting API: Physics.RaycastAll

Hi. Thanks for reply but I am looking for a way to call pointer events on 4.6 UI elements. Like OnPointerExit, OnBeginDrag etc. I think they would be more efficient and robust if there was an easy way (Which I think there is certainly one but I don't know yet) Also implementing every evet this way would take much time.

You can always design your own raycaster, collider, input manager etc. There is always a way around any problem. But what is the purpose of the game engine then? If there was a way to do it naturally, it would also be faster. I just think this behavior is not intended or planned to be improved in newer versions. It makes no sense that it lacks this functionality, because it seems to me like it is something anyone could have needed.

This is how I solved passing on any PointerEvent type to the next Raycast-blocking item underneath. It’s easy to implement some check for a specific Tag, Component etc. if you have many blocking objects stacked…

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

public class PassOnPointerEvent : MonoBehaviour, IPointerDownHandler, IDragHandler, IBeginDragHandler, IPointerUpHandler, IEndDragHandler
{
    GameObject newTarget;

    public void OnPointerDown(PointerEventData eventData)
    {
        List<RaycastResult> raycastResults = new List<RaycastResult>();
        EventSystem.current.RaycastAll(eventData, raycastResults);
        newTarget = raycastResults[1].gameObject; //Array item 1 should be the one next underneath, handy to implement for-loop with check here if necessary.
        print($"Passing on click to {newTarget}"); //Just make sure you caught the right object

        ExecuteEvents.Execute(newTarget, eventData, ExecuteEvents.pointerDownHandler);
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        ExecuteEvents.Execute(newTarget, eventData, ExecuteEvents.pointerUpHandler);
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        ExecuteEvents.Execute(newTarget, eventData, ExecuteEvents.beginDragHandler);
    }

    public void OnDrag(PointerEventData eventData)
    {
        ExecuteEvents.Execute(newTarget, eventData, ExecuteEvents.dragHandler);
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        ExecuteEvents.Execute(newTarget, eventData, ExecuteEvents.endDragHandler);
    }  
}

Hope it can help someone!

Hm, it seems to call ExecuteEvents.Execute on the correct game object, but that game object's OnPointerDown() is not called :/ Any idea what I could be doing wrong?

It seems like you’re trying to do some sort of event bubbling in a sense.

What you can do, since the front panel is a child of the back panel, is calling the same method in back panel that is called OnPointerEnter by it’s EventTrigger in the front panel’s EventTrigger as well.

In other words, you would make the child intercept the event and send it to it’s parent inside your own callback (whatever public void method you give to the EventTrigger for this).

Hope it works!

If someone will read this…
I found solution for me.

In my case the event is fired for first layer, then to parent layers, but I need only the first event (for top object), so its easy to filter events by frame number:

private int frameNum;
public void OnPointerEnter(PointerEventData eventData)//OnPointerEnter, but you can use any other events
{
      if (frameNum == Time.frameCount) return;
    frameNum = Time.frameCount;

    //Do your stuff here
}

Only the first event will pass (for toppest object).