Search Unity

Passing an event through to the next object in the raycast

Discussion in 'Unity UI (uGUI) & TextMesh Pro' started by mweldon, Sep 4, 2014.

  1. mweldon

    mweldon

    Joined:
    Apr 19, 2010
    Posts:
    109
    I have a UI where there is a ScrollRect. Inside the ScrollRect are individual items with their own handler for drag events. The problem is that only one drag handler gets called, the first one that the raycast hits. If I click on an item, its handler is called but the ScrollRect's handler does not. If I click between the items, then the ScrollRect's handler gets called. Is there a convenient way allow the event to pass through to the next object in the raycast?
     
  2. mweldon

    mweldon

    Joined:
    Apr 19, 2010
    Posts:
    109
  3. mweldon

    mweldon

    Joined:
    Apr 19, 2010
    Posts:
    109
    So here's how I got it to work, though it is extremely hacky and it makes our drag component a less reusable than it was before:

    Code (CSharp):
    1.        
    2.         public void OnDrag( PointerEventData eventData )
    3.         {
    4.             // My drag handler for the individual object
    5.  
    6.             // Call the handler for the containing ScrollRect
    7.             if( scrollViewContainer )
    8.             {
    9.                 scrollViewContainer.SendMessage( "OnDrag", eventData );
    10.             }
    11.         }
    12.  
    This appears to work, but it seems like a ham-fisted way to pass events through to a parent object.

    Also as a side note, I tried SendMessageUpwards and it crashed Unity. I'm guessing infinite loop.
     
    svitlana likes this.
  4. Tim-C

    Tim-C

    Unity Technologies

    Joined:
    Feb 6, 2010
    Posts:
    2,093
    So the way out InputModules are designed is to NOT send messages up like you want to do in this case. This was a design decision on our behalf, but there are use cases where something else is desired. The InputModule is responsible for what events are sent, and how they are sent. You can write your own that has this behaviour if you want.
     
  5. vrm2p

    vrm2p

    Joined:
    Jul 23, 2014
    Posts:
    17
    Sorry to push this thread up again. There's another circumstance where passing the event to the object "behind" the current object is extremely useful: Drag and Drop to GameObjects with Mouseover

    When trying to drag an object (in my case a die)

    example script
    Code (CSharp):
    1. public void OnDrag (PointerEventData data)
    2.         {
    3.                 Vector3 pos = transform.position;
    4.                 pos.x += data.delta.x;
    5.                 pos.y += data.delta.y;
    6.                 transform.position = pos;
    7.         }
    It sticks to the mousepointer regardless the mousedown position: It's glued to the pointer.

    Moving this pointer to the droptarget (in my case an image) - the pointerenter/pointerexit events arent fired, because the pointer never enters this image. (It does enter, but the object in front of it is blocking)

    The solution here is adding an offset, so that the pointer is never over the draggable target, which is really ugly. Any Ideas on this?

    Edit: This affects OnDrop as well
     
    Last edited: Sep 11, 2014
  6. Tim-C

    Tim-C

    Unity Technologies

    Joined:
    Feb 6, 2010
    Posts:
    2,093
    If you look at the included drag and drop example we have you will see that the way we recommend doing this is that when you start a drag you mark the object to ignore raycasting so that it will not block what's behind it :)
     
    mpeddicord likes this.
  7. vrm2p

    vrm2p

    Joined:
    Jul 23, 2014
    Posts:
    17
    Many thanks :)
     
  8. Free286

    Free286

    Joined:
    Oct 29, 2013
    Posts:
    23
    You could try something like this, the implementation is quite amazing I think, as you can pass PointerEventData from OnDrag to OnEndDrag ect.. kudos to the unity team for a good implementation to a hard problem.
    Code (CSharp):
    1. GetComponentInParent<ScrollRect>().SendMessage("OnDrag", eventData);
     
  9. brainz12345

    brainz12345

    Joined:
    Dec 10, 2012
    Posts:
    12
    A use case may be a draggable object within a vertical scroll list. In this case I should evaluar if te eventDelta has no horizontal component, and if so, the recast should be ignored and passed up not the scroll rect (i.e., the user is not dragging an object but rather scrolling). Is there a way to get through this rather than by the sendMessage approach?
     
  10. brainz12345

    brainz12345

    Joined:
    Dec 10, 2012
    Posts:
    12
    Here is my working code:
    Code (csharp):
    1.  
    2.  
    3. public static GameObject itemDragged;
    4.  
    5. public void OnBeginDrag (PointerEventDataeventData)
    6. {
    7. if (eventData.delta.x != 0) {
    8.     //My Start Drag
    9.     itemDragged = gameObject;
    10.  } else {
    11.     itemDragged = null;
    12.     GetComponentInParent<ScrollRect>().SendMessage("OnBeginDrag", eventData);
    13.     return;
    14.  }
    15.  }
    16.  
    17. public void OnDrag (PointerEventDataeventData)
    18.  {
    19. if (itemDragged) {
    20.     //My Drag Code
    21.  } else {
    22.     GetComponentInParent<ScrollRect>().SendMessage("OnDrag", eventData);
    23.     return;
    24.  }
    25.  }
    26.  
    27. publicvoidOnEndDrag (PointerEventDataeventData)
    28.  {
    29. if (itemDragged) {
    30.     //My Drop Code
    31.  } else {
    32.     GetComponentInParent<ScrollRect>().SendMessage("OnEndDrag", eventData);
    33.     return;
    34.  }
    35.  }
    36.  
     
  11. Free286

    Free286

    Joined:
    Oct 29, 2013
    Posts:
    23
    After using canvas more there's been a lot of issues, including behavior that is inconsistent on device and pixel perfect not working, as well as text not working with localization. We just use sprite renderers that are sorted based on hierarchy order now, with physics2D raycasts, I recommend this approach until canvas is mature enough to be used in a shippable form.
     
  12. svitlana

    svitlana

    Joined:
    Nov 19, 2013
    Posts:
    1
    Thank you it works!

    You also can call whatever method you need
    gameObject.SendMessage("PointerEnter");
     
  13. DoomGoober

    DoomGoober

    Joined:
    Dec 12, 2013
    Posts:
    1
    brainz12345, good solution. You can don't need SendMessage though, since it's expensive. Just get the parent ScrollRect, cast it to the correct interface, and call it directly. Here's my class that allows BOTH scrolling and dragging another object (my case is different: a scroll view's vertical scroll was keeping the user from dragging its parent horizontally.) Put this MonoBehaviour on the child GameObject of the ScrollRect, called "ViewPort" by default.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4. using UnityEngine.EventSystems;
    5. using UnityEngine.UI;
    6.  
    7. //Passesdragmessagestothe parent
    8. public class PassDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
    9. {
    10.  private ScrollRect scrollRect;
    11.  public MonoBehaviour passToMonoBehaviour;
    12.  
    13. void Awake()
    14. {
    15. scrollRect = GetComponentInParent<scrollRect>();
    16. }
    17.  
    18.  public void OnBeginDrag(PointerEventData eventData)
    19.  {
    20.  scrollRect.OnBeginDrag(eventData);
    21.  ((IBeginDragHandler)passToMonoBehaviour).OnBeginDrag(eventData);
    22.  }
    23.  
    24.  public void OnDrag(PointerEventData eventData)
    25.  {
    26.  scrollRect.OnDrag(eventData);
    27.  ((IDragHandler)passToMonoBehaviour).OnDrag(eventData);
    28.  }
    29.  
    30.  public void OnEndDrag(PointerEventData eventData)
    31.  {
    32.  scrollRect.OnEndDrag(eventData);
    33.  ((IEndDragHandler)passToMonoBehaviour).OnEndDrag(eventData);
    34.  }
    35. }
    36.  
     
    vgf555, Lardalot, PhilippG and 2 others like this.
  14. Jason-Loomis

    Jason-Loomis

    Joined:
    Nov 5, 2014
    Posts:
    1
    For my scripts that implement the IDragHandler interface (and others, like the IPointer... interfaces) where I would like the OnDrag event NOT to be consumed by the first handler that encounters the event, I use SendMessageUpwards to pass the OnDrag event to the ancestors of the object.

    There is an important gotcha though--SendMessageUpwards also sends the message to the script that is sending the message, so sending "OnDrag" from within the OnDrag() event handler will result in an infinite loop. The workaround to make this work in this situation is to call SendMessageUpwards from the object that is the parent of the object to which your script is attached.

    Something like this for OnDrag (and similar for others, e.g. the OnPointer... events):

    Code (CSharp):
    1. void OnDrag(PointerEventData eventData)
    2. {
    3.     if(this.transform.parent != null)
    4.     {
    5.         this.transform.parent.gameObject.SendMessageUpwards("OnDrag", eventData, SendMessageOptions.DontRequireReceiver);
    6.     }
    7. }
     
    IgorAherne likes this.
  15. Brogan89

    Brogan89

    Joined:
    Jul 10, 2014
    Posts:
    155

    This is by far the best solution. Thanks for you're help.
     
  16. PhilippG

    PhilippG

    Joined:
    Jan 7, 2014
    Posts:
    253
    Ha, really elegant, thanks!
     
  17. Ainulindale

    Ainulindale

    Joined:
    Aug 7, 2017
    Posts:
    1
    I don't understand this decision.

    Right now I'm trying to create a feature where you can use middle mouse button to scroll around inside a RectTransform (like you can scroll around with the middle mouse button in a browser) but IPointerDownHandler will not only catch middle mouse button down but left and right mouse buttons as well.

    I don't want to consume left/right mouse events just because I am interested in the middle mouse button. Now I'm at a point where I can't use the built-in event system just because buttons etc behind the scroll RectTransform won't work. And I can't move stuff around so that for example buttons are on top because then they will block the middle mouse button.

    All these send message or GetComponentInParent workarounds are not good solutions because the thing behind the GameObject with a IPointerDownHandler is not guaranteed to be an ancestor of it. RectTransforms can overlap yet be in different hierarchies. You can even have a IPointerDownHandler on a non-canvas GameObject.

    What you could have done, Unity, is require a return on the method catching the event:
    public bool OnPointerDown (PointerEventData eventData);

    That way we could have returned true if we want the event to be stopped. In my case I would return false if I detected a middle mouse button press, and true if left/right was pressed so that the InputModule can continue to send the same event to the next target in its raycast.

    Even ActionScript 3 (flash) have this functionality, to "bubble events" to underlaying objects. They use a different approach with stopPropagation() methods.

    Another thing you could have done if you wanted to still have void as a return for OnPointerDown is to simply add a method to the PointerEventData object named continuePropagation() or similar.

    Nothing would break in old code if you add that method to the eventData object so it's not too late to improve your system without breaking compatibility. Going for this approach would probably be even better than a return value now that I think about it. In my case I would just have to check if middle mouse button was not pressed and then call eventData.continuePropagation() to pass on the event to the next raycast hit (which may have use for it).

    Tim-C: You say that it was a design decision to always consume events once received. But why exactly? I'm always open to improve as a programmer and maybe there is something I've overlooked. From where I'm standing it's strange to require all these people to use flawed workarounds instead of adding the desired functionality to Unity.

    Edit: In PointerEventData there is "used" and "Use()" but they seem to be completely unused by Unity's system. Even if I write my own InputModule, inherit from StandaloneInputModule and override Process() to make base.Process() get called twice -- the "used" field is still false, even though it gets used (consumed) by the OnPointerDown method. I checked to make sure I get the exact same PointerEventData object on the exact same OnPointerDown method as well. Why is used and Use() there?
     
    Last edited: Aug 7, 2017
  18. Tortuap

    Tortuap

    Joined:
    Dec 4, 2013
    Posts:
    46
    I do totally agree.
    Using SendMessage or calling method on interface found on ancestors are crappy workarounds that work for specific very limited context, as you explained.

    Having a method continuePropagation would be a good design solution. Unity guys, are you still there to think about it please ?
     
    Jes28 and foobar like this.
  19. Jes28

    Jes28

    Joined:
    Sep 3, 2012
    Posts:
    420
    +1 for continue propagation

    And one question.

    What is eventData.used for?
    I thought it is exactly for this but seems it just unusable property
     
  20. IgorAherne

    IgorAherne

    Joined:
    May 15, 2013
    Posts:
    344
    Cmon Unity-guys!
     
  21. GuitarBro

    GuitarBro

    Joined:
    Oct 9, 2014
    Posts:
    75
    Just adding my 2 cents:

    I also find this very unintuitive. At least in my experience with web development, the common paradigm for handling events is to propagate by default. I was expecting something similar to how jQuery handles events (requiring you to explicitly call event event.stopPropagation() if you don't wish for other deeper elements to receive the event). It also doesn't make sense that there appears to be no easy way to have one object consume, say, clicks, but allow other events such as scroll to propagate. It's all events or nothing (or I'm missing something here).

    In my particular case, I wished to detect clicks/touches on overlapping objects and have them both be able to process the response while still allowing UI such as buttons to block propagation. However, it looks like I will be needing to make my own workaround for now. :(
     
  22. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    1,504
    To be fair, a non-ancestor object behind your GameObject wouldn't receive the event even if you didn't add a pointer handler. Any raycast target will prevent clicks from passing through to hierarchically-unrelated objects behind it, even if it doesn't respond to those events in any way. The additional limitation of adding a pointer handler is only to prevent ancestors from receiving the event.

    I'm pretty sure that already works. If you put a bunch of Buttons inside of a Scroll Rect, the buttons will eat clicks, but drags will pass through and trigger a scroll. In general, I believe components only consume events of a type they handle, and any other events are propagated to the parent object.

    However, as noted above, events only propagate to parents; a sibling will be blocked by any raycast target in front of it, even if it has no event handlers at all.

    If you want to allow pointer events to "pass through" a Graphic, you can set its "raycast target" field to false, or you can use a Canvas Group to turn off raycast hits on an entire hierarchy of objects at once.
     
  23. GuitarBro

    GuitarBro

    Joined:
    Oct 9, 2014
    Posts:
    75
    That is good to know, I should have been more specific as I was referring to siblings, not parents, as you mentioned.

    However, I do believe there still is an issue here. I'm not sure I understand why the raycast should stop at the first hit, or rather, why there is no option to have it hit multiple objects (similar to Physics.RaycastAll()). If performance is a concern, then by all means, leave the current method as the default. But it seems that at least having that as an option would solve many problems.
     
  24. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    1,504
    I imagine one could probably add an invisible full-screen object in front of the world that intercepts pointer events, calls RaycastAll (or something similar), and then propagates the event to everything the ray hits.

    That should be fairly easy for clicks. For enter/leave events it's probably a bit harder, but still fairly doable. (You'd presumably need to check the mouse position every frame, and maintain a list of objects the ray hit last frame.)

    I have not tried this, however.
     
    BorisOkunskiy likes this.
  25. BorisOkunskiy

    BorisOkunskiy

    Joined:
    Oct 3, 2017
    Posts:
    11
    Hey guys. A little late to the party, but here's what I did:

    Code (CSharp):
    1. public class DragPanel : MonoBehaviour,
    2.     IDragHandler, IPointerClickHandler {
    3.  
    4.  
    5.     public void OnDrag(PointerEventData data) {
    6.         // Some dragging logic
    7.     }
    8.  
    9.     public void OnPointerClick(PointerEventData data) {
    10.         // Pass-through click event to all EventTrigger components
    11.         List<RaycastResult> results = new List<RaycastResult>();
    12.         EventSystem.current.RaycastAll(data, results);
    13.         foreach (RaycastResult res in results) {
    14.             EventTrigger trig = res.gameObject.GetComponent<EventTrigger>();
    15.             if (trig == null) {
    16.                 continue;
    17.             }
    18.             trig.OnPointerClick(data);
    19.         }
    20.     }
    21.  
    22. }
    Scene setup for which I made this tiny component:

    Code (text):
    1.  
    2. - Canvas (GraphicRaycaster)
    3.   - DragPanel (stretched to cover the screen)
    4. - Camera (PhysicsRaycaster)
    5. - Sphere (EventTrigger)
    6.  
    So the problem I was solving is pretty similar to what was discussed here, except I wanted to handle dragging on UI (canvas) whilst being able to click objects in 3d space, leading to exactly the same problem (by default this UI panel intercepts all events and doesn't propagate it to siblings). As someone mentioned above, the events are actually dispatched by Unity's Input Modules, so messing with them isn't that great an idea. Instead, I simply had this panel broadcast the click event with RaycastAll, which worked pretty fine.

    I imagine this can be further generalized into something like EventDispatcherPanel which implements all standard event handler interfaces and uses the above logic to broadcast these events. It is also possible to sniff trig.triggers in case you want to dispatch to the first EventTrigger that handles an event of interest. I don't really need anything like that (I just need a drag screen + clicking objects), but hope the above gives an idea how to work around these Unity limitations.

    ---

    Speaking of design decision, I can understand that (software engineer myself), but the design decisions should also be revised based on typical usage scenarios and user stories. I really think that event propagation is something common when working with input, and I can tell thousands (if not more) users are quite frustrated by having to search for some workarounds on forums and re-implement functionality that simply should be there in a first place. </rant> :D
     
    Jes28 likes this.
  26. joelstartech

    joelstartech

    Joined:
    Mar 6, 2019
    Posts:
    31
    How would I implement this if I had a scrollrect that's full of vertically-stacked full-width buttons that are prefabs? I just want to person to be able to touch scroll through the list, and 'tap' on an item to select it. How would I do that with these scripts?
     
  27. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    1,504
    You don't need to do anything; that is Unity's default behavior for a scrollrect full of buttons. Taps will interact with the buttons and drags will interact with the scollrect.

    The buttons listen for clicks but not drags, so drags propagate up to the object hierarchy, and the scrollrect is an ancestor of everything in the scrollable container so it will receive them.

    Just make sure the button prefabs don't include any component that would listen for drags.
     
  28. joelstartech

    joelstartech

    Joined:
    Mar 6, 2019
    Posts:
    31
    In my use case it simply isn't true. Anytime you try to scroll (or touch drag) over a button, it takes control over the scrollrect. You can scroll/drag *between* the buttons, but it will not scroll when the initial start of the drag happens over a button.
     
  29. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    1,504
    I've just created a scroll view and a bunch of buttons and have no trouble drag-scrolling on top of the buttons.

    Odds are that you have some other component in your button prefabs (not the actual Button component) that is capturing the drags. (My guess would be Event Trigger, which captures pretty much everything whether you need it or not.)
     
  30. martinasenovdev

    martinasenovdev

    Joined:
    May 7, 2017
    Posts:
    67
    this is why we'll switch to Unreal
     
  31. Harfatum

    Harfatum

    Joined:
    May 28, 2018
    Posts:
    9
    How exactly do you do this? I tried putting this in my OnBeginDrag method but it doesn't seem to do anything:
    Code (CSharp):
    1.  
    2.         gameObject.layer = LayerMask.NameToLayer("Ignore Raycast");
     
  32. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    1,504
    I believe the typical way to remove a UI object from raycasting would be to attach a "Canvas Group" component and uncheck the "Blocks Raycasts" box.
     
  33. Harfatum

    Harfatum

    Joined:
    May 28, 2018
    Posts:
    9
    It's not a UI object. I suppose I could try making it into one.
     
  34. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    1,504
    Are you aware that you're posting in the UI forum?
     
  35. Harfatum

    Harfatum

    Joined:
    May 28, 2018
    Posts:
    9
    Oh, apologies. I've searched what seemed like the whole internet trying to resolve this issue and this seemed like the closest I found, I'll make a topic elsewhere.
     
  36. jhughes2112

    jhughes2112

    Joined:
    Nov 20, 2014
    Posts:
    93
    Since there wasn't a clean implementation posted, here's a solution you can literally just add to a ScrollRect and allow it to pass drag messages to WHATEVER happens to be behind it. This is nice because you can have N pages of prefabs, each with its own vertical ScrollRect, and have a single full page ScrollRect (or custom swiper) that lives behind it, and handles horizontal movement. In the OnBeginDrag, I just look for the next object in the stack and pass all the drag messages to whatever that is. Meaning, your prefabs don't need to be tied to the rest of the scene.

    Enjoy!

    (It should be mentioned that you must declare your OnDragBegin/OnDrag/OnDragEnd to be public functions, or they won't get the SendMessage call.)

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.EventSystems;
    3. using UnityEngine.UI;
    4. using System.Collections.Generic;
    5.  
    6. public class PassDragEvents : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
    7. {
    8.     // Because UIs tend to move around, we collect the target OnBeginDrag and always send the remaining
    9.     // events to that object.
    10.     private GameObject _proxyTarget = null;
    11.  
    12.     void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
    13.     {
    14.         _proxyTarget = null;
    15.         GraphicRaycaster gr = GetComponentInParent<GraphicRaycaster>();
    16.         if (gr != null)
    17.         {
    18.             List<RaycastResult> hits = new List<RaycastResult>();
    19.             gr.Raycast(eventData, hits);  // this will include our game object and any others behind it
    20.  
    21.             bool foundUs = false;
    22.             foreach (RaycastResult rr in hits)
    23.             {
    24.                 if (foundUs)  // grab the very NEXT gameobject
    25.                 {
    26.                     _proxyTarget = rr.gameObject;
    27.                 }
    28.                 else if (rr.gameObject == gameObject)
    29.                 {
    30.                     foundUs = true;
    31.                 }
    32.             }
    33.  
    34.             if (_proxyTarget!=null)
    35.                 _proxyTarget.SendMessage("OnBeginDrag", eventData, SendMessageOptions.DontRequireReceiver);
    36.         }
    37.     }
    38.  
    39.     void IDragHandler.OnDrag(PointerEventData eventData)
    40.     {
    41.         if (_proxyTarget!=null)
    42.             _proxyTarget.SendMessage("OnDrag", eventData, SendMessageOptions.DontRequireReceiver);
    43.     }
    44.  
    45.     void IEndDragHandler.OnEndDrag(PointerEventData eventData)
    46.     {
    47.         if (_proxyTarget!=null)
    48.         {
    49.             _proxyTarget.SendMessage("OnEndDrag", eventData, SendMessageOptions.DontRequireReceiver);
    50.             _proxyTarget = null;
    51.         }
    52.     }
    53. }
    54.  
     
    GuitarBro likes this.