Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Why is ICanvasRaycastFilter affecting child objects?

Discussion in 'UGUI & TextMesh Pro' started by Cakebox, May 11, 2023.

  1. Cakebox

    Cakebox

    Joined:
    May 14, 2020
    Posts:
    11
    Hello everyone,

    I have a Canvas displaying an image of a hexagon. That has six children, each displaying a small circle, as in the image below. I want to achieve this behaviour:



    Instead, I have the behaviour below. The raycast does hit the circle (as evidenced by the pulse and the debug info shown under 'Current Raycast'), but not until the pointer is also inside the parent hexagon.



    I believe I've isolated the problem to this component, which is attached to the parent:

    Code (CSharp):
    1.  
    2. [RequireComponent(typeof(PolygonCollider2D), typeof(RectTransform))]
    3. public class PolygonColliderBasedRaycastFilter : MonoBehaviour, ICanvasRaycastFilter
    4. {
    5.         /// <summary>
    6.         /// Works in conjunction with a Polygon Collider 2D to filter out any raycasts
    7.         /// which would normally hit the Rect of this object, but are outside the collider.
    8.         /// For when we have UI elements that are non-rectangular, and need their
    9.         /// raycasts to be accurate to their actual shape.
    10.         /// Based on a Unity Answers post by miguelSantirso here:
    11.         /// https://answers.unity.com/questions/882844/how-to-stop-non-rectangular-buttons-from-overlappi.html?childToView=981558#answer-981558
    12.         /// </summary>
    13.  
    14.         private PolygonCollider2D myCollider;
    15.         private RectTransform myRectTransform;
    16.  
    17.         private void Awake()
    18.         {
    19.             if (!TryGetComponent(out myCollider))
    20.                throw new System.NullReferenceException();
    21.             if (!TryGetComponent(out myRectTransform))
    22.                throw new System.NullReferenceException();
    23.         }
    24.  
    25.         bool ICanvasRaycastFilter.IsRaycastLocationValid(Vector2 screenPosition, Camera eventCamera)
    26.         {
    27.             var successfullyHitPlane =
    28.                 RectTransformUtility.ScreenPointToWorldPointInRectangle(
    29.                     rect: myRectTransform,
    30.                     screenPoint: screenPosition,
    31.                     cam: eventCamera,
    32.                     out var resultingPoint);
    33.  
    34.             if (!successfullyHitPlane) return false;
    35.  
    36.             var resultingPointIsInsideCollider = myCollider.OverlapPoint(resultingPoint);
    37.             return resultingPointIsInsideCollider;
    38.         }
    39.     }

    (The hexagon has a PolygonCollider2D matching its shape, and the component above filters out any raycasts not inside that collider.)

    I can eliminate the issue by removing this component or by editing the code above so that it explicitly checks against the child colliders, too. But I still don't understand the underlying problem: why does the component on the parent object affect raycasts to its children?

    In general I didn't think raycasting was affected by parent-child relationships. And while this interface in particular should of course affect raycasts to the parent, I can't find anything to suggest it should affect raycasts to the child.

    Is this a bug, or is there some other behaviour here that I'm missing?

    Many thanks in advance for your help.
     
  2. karliss_coldwild

    karliss_coldwild

    Joined:
    Oct 1, 2020
    Posts:
    530
    Just to avoid any confusion, In the following text I am talking only about UGUI raycasts which is a completely different thing than physics raycasts.

    One of the most common case for UGUI raycasts being affected by parent child relationship is buttons. If you have a fancy button that has additional icon or text drawn on top, you probably want button to click even when you hit the icon or text instead of base image. Mouse events being affected by object hierachy is also very useful for implementing popout menus.

    If you search for use of ICanvasRaycastFilter in UGUI source code (reminder UGUI source code is available to you just like it is for many other packages) you will find Graphic component which is base class for most of the UGUI stuff.
    Looking part that is using ICanvasRaycastFilter, you will see that checking of parent object filters is clearly intentional.

    Looking at how various builtin UGUI components use ICanvasRaycastFilter explains this behavior. One of the few components using it are Mask and RectMask which are used by Scrollview. When an object inside scrollview is not visible due to current scroll position, you wouldn't want it to be clickable.

    Only other use of ICanvasRaycastFilter is pixel alpha based click filtering, but that doesn't seem like well supported feature, as indicated by the option to enable it not being exposed to editor and also it not being serialized.
     
    Last edited: May 11, 2023
    Cakebox likes this.
  3. Cakebox

    Cakebox

    Joined:
    May 14, 2020
    Posts:
    11
    Thank you very much for the detailed explanation. I really appreciate it.

    It sounds like this behaviour is intentional, then.

    In case anybody finds this post in the future, here's the extra code I wrote to work around:

    Code (CSharp):
    1. [RequireComponent(typeof(Collider2D), typeof(RectTransform))]
    2.     public class ColliderBasedRaycastFilter : MonoBehaviour, ICanvasRaycastFilter
    3.     {
    4.         /// <summary>
    5.         /// Works in conjunction with a Collider2D to filter out any raycasts which would normally hit the Rect of this object, but are outside the collider.
    6.         /// For when we have UI elements that are non-rectangular, and need their raycasts to be accurate to their actual shape.
    7.         /// Based on a Unity Answers post by miguelSantirso here: https://answers.unity.com/questions/882844/how-to-stop-non-rectangular-buttons-from-overlappi.html?childToView=981558#answer-981558
    8.         /// </summary>
    9.  
    10.         [Tooltip("Components implementing ICanvasRaycastFilter will (by design, according to a forum answer I received) filter all raycasts to not only this object but also any of its children. If they're outside the collider, they won't receive raycasts either. To counteract this, if this option is set to true, the component will check not only the collider on the object it's attached to but also the colliders on any immediate children before filtering out a raycast. Note, however, that only objects with a RectTransform will be checked.")]
    11.         [SerializeField] private bool alsoTryImmediateChildColliders;
    12.  
    13.         [SerializeField] private Collider2D myCollider;
    14.         private RectTransform myRectTransform;
    15.  
    16.         private void Awake()
    17.         {
    18.             if (myCollider == null) throw new System.NullReferenceException();
    19.             if (!TryGetComponent(out myRectTransform)) throw new System.NullReferenceException();
    20.         }
    21.  
    22.         bool ICanvasRaycastFilter.IsRaycastLocationValid(Vector2 screenPosition, Camera eventCamera)
    23.         {
    24.             var hitsParent = RaycastHitsCollider(myRectTransform, myCollider);
    25.  
    26.             if (hitsParent) return true;
    27.  
    28.             if (!alsoTryImmediateChildColliders) return false;
    29.  
    30.             // See if the raycast hits (the collider of) any immediate children
    31.             foreach (Transform childTransform in transform)
    32.             {
    33.                 if (childTransform is not RectTransform childRectTransform) continue;
    34.              
    35.                 if (!childRectTransform.TryGetComponent<Collider2D>(out var childCollider)) continue;
    36.  
    37.                 if (RaycastHitsCollider(childRectTransform, childCollider)) return true;
    38.             }
    39.  
    40.             // If not, then the raycast doesn't hit
    41.             return false;
    42.          
    43.             bool RaycastHitsCollider(RectTransform targetRectTransform, Collider2D targetCollider)
    44.             {
    45.                 var successfullyHitPlane = RectTransformUtility.ScreenPointToWorldPointInRectangle(
    46.                 rect: targetRectTransform,
    47.                 screenPoint: screenPosition,
    48.                 cam: eventCamera,
    49.                 out var resultingPoint);
    50.  
    51.                 if (!successfullyHitPlane) return false;
    52.  
    53.                 var resultingPointIsInsideCollider = targetCollider.OverlapPoint(resultingPoint);
    54.                 return resultingPointIsInsideCollider;
    55.             }
    56.         }
    57.     }