- Home /
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.
I have a similar situation in that I need to respond to an event in 1 control but not block the message from events behind it.
In my case I am using the mouse enter and exit to control when a camera is aloud to scroll, this works wonderfully in that mouse over a UI element and it wont scroll happy days, but unfortunately it also means everything behind that (selectable objects in the world) now wont respond to my events.
The ideal solution is that I can respond to mouse enter and exit, can block events with other elements such as input fields but that I dont block events with a particular element (my zoom RECT)
I can code this manually I suppose testing for cursor location in a given RECT space, testing to see if if another element control has focus, etc. but what a pain, would be so much easier if we could toggle in some way rather or not a received message was intercepted or passed on ... anyone have a simpler solution using stock Unity elements that I missed?
And no a custom handler which only listens to enter exit wont work as I need enter and exit to continue on to the game space not just the click which is what I have at the moment.
Answer by InvincibleCat · Jan 23, 2015 at 10:42 PM
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
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?
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.
Answer by immersiveGamer · Jan 26, 2015 at 10:13 PM
What you are looking for is Physics.RaycastAll. 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[i].transform.GetComponent<MyEventScript>().CallEvent();
}
}
}
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.
hmmm, so I looked into it and it seems that when an object gets interacted with it intercepts the event.
I don't know how you can over come this. I tried assorts of things but to no avail couldn't get it to work. Things like changing the graphic raycaster on the canvases, multiple canvases, dug into the documents and tried to get something. What is a real pain is that a Event Trigger actually intercepts everything! Wouldn't it be the other way round? I tried looking for a way to change the Intercepts (which I assume is some kind of delegate it gets added to) but couldn't do that either. Perhaps there is a graphical/spacial way to solve your problem? Like making sure the panel behind sticks out a bit? Or use transparency? I know I read somewhere that we have access to the source code for the new UI and thus can make changes as we need.
Also it really wouldn't take too much time to make your own event system. Unity already tracks clicks, drags, etc. and you just make delegates. I did something similar with the old OnGUI system and it worked just fine. $$anonymous$$aybe I'll look into the source code (they are just C# scripts by the way) and see if I can hack anything together.
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.
Answer by jonaslindberg · May 07, 2020 at 06:23 AM
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?
Hmm, hard to say from here of course but first check that your action is relying on the PointerDown event and not e.g. PointerClick?
Otherwise, try "bubbling" the event at the found target GameObject and then getting the actual GameObject that will be reliable for executing by using the GetEventHandler function. Something like below...
public void OnPointerDown(PointerEventData eventData)
{
List<RaycastResult> raycastResults = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, raycastResults);
newTarget = ExecuteEvents.GetEventHandler<IPointerDownHandler>(raycastResults[1].gameObject)
ExecuteEvents.Execute(newTarget, eventData, ExecuteEvents.pointerDownHandler);
}
This also means you can do something like below to do completely fake pointer events.
public Vector2 fakePointerPos;
[Context$$anonymous$$enu("Fake Click!")]
public void FakePointerDownEvent(fakePointerPos)
{
PointerEvenData constructedEventData = new PointerEvenData(EventSystem.current);
constructedEventData.position = fakePointerPos;
List<RaycastResult> raycastResults = new List<RaycastResult>();
EventSystem.current.RaycastAll(constructedEventData, raycastResults);
newTarget = ExecuteEvents.GetEventHandler<IPointerDownHandler>(raycastResults[0].gameObject) //assu$$anonymous$$g you want to collect the first object at this position
ExecuteEvents.Execute(newTarget, constructedEventData, ExecuteEvents.pointerDownHandler);
}
The Context$$anonymous$$enu stuff is just so you can run it from the dropdown menu in Editor, the three dots to the right of the script title.
Leave only "IPointerDownHandler, IPointerUpHandler" And then this script will work fine
Answer by Radetic · Apr 22, 2015 at 09:02 PM
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!
Answer by Stridemann · Apr 20, 2017 at 09:19 AM
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).