Unity篇 — 事件系统

Posted by Xun on Sunday, January 29, 2023

Unity 中各种点击、拖拽的交互响应是不可或缺的。在 UGUI 中,使用了 EventSystem 进行管理。

简介

  • 一个简单的交互流程,主要有以下步骤:
    • 用户点击、滑动、拖拽鼠标(或屏幕)
    • 找到响应的对象
    • 执行对象对应的方法
  • 因此,相对地,就需要知道
    • 如何获取玩家的操作
    • 如何找到需要响应的对象
    • 如何执行对应方法
  • Unity 的事件系统,主要由输入模块(InputModule)、射线检测模块(Raycast)、事件执行模块(ExecuteEvents)组成,在事件系统(EventSystem)的管理下进行。

时序图

  • EventSystem 的时序图如下 EventSystem时序图.png

EventSystem 主流程

  • EventSystem 继承 UIBehavior ,作为组件进行使用。其定义了几个对象:
    • m_SystemInputModules :当前 EventSystem 的所有 InputModule 列表(List)。
    • m_CurrentInputModule :当前 EventSystem 使用的 InputModule 。
    • m_EventSystems :所有 EventSystem 列表(static List)。
    • current :当前使用的 EventSystem 。
    public class EventSystem : UIBehaviour
    {
        private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();

        private BaseInputModule m_CurrentInputModule;

        private  static List<EventSystem> m_EventSystems = new List<EventSystem>();

        /// <summary>
        /// Return the current EventSystem.
        /// </summary>
        public static EventSystem current
        {
            get { return m_EventSystems.Count > 0 ? m_EventSystems[0] : null; }
            set
            {
                int index = m_EventSystems.IndexOf(value);

                if (index > 0)
                {
                    m_EventSystems.RemoveAt(index);
                    m_EventSystems.Insert(0, value);
                }
                else if (index < 0)
                {
                    Debug.LogError("Failed setting EventSystem.current to unknown EventSystem " + value);
                }
            }
        }

        
        ...
    
    }
  • 当 EveneSystem 激活时,则会加入 m_EventSystems 列表中。同样,当 EveneSystem 关闭时,则会从 m_EventSystems 列表中移除自身, 并且清除掉当前的输入模块信息 m_CurrentInputModule 。
    public class EventSystem : UIBehaviour
    {
        ...
    
        protected override void OnEnable()
        {
            base.OnEnable();
            m_EventSystems.Add(this);
        }

        protected override void OnDisable()
        {
            if (m_CurrentInputModule != null)
            {
                m_CurrentInputModule.DeactivateModule();
                m_CurrentInputModule = null;
            }

            m_EventSystems.Remove(this);

            base.OnDisable();
        }

        ...
    }
  • 当 EventSystem 处于激活状态时,由于继承 UIBehavior ,每帧会执行 Update 方法进行更新,主要内容为:
    • 对 m_SystemInputModules 列表中的所有 InputModule 执行 UpdateModule 方法。
    • 如果当前输入模块 m_CurrentInputModule 为空,或者不为 m_SystemInputModules 列表中首个符合条件的模块,则进行切换。
    • 如果此帧没有进行模块切换,则对当前输入模块 m_CurrentInputModule 执行 Process 方法进行处理。
    public class EventSystem : UIBehaviour
    {
        ...
    
        protected virtual void Update()
        {
            if (current != this)
                return;
            // 对 m_SystemInputModules 列表中的所有 InputModule 执行 UpdateModule 方法。
            TickModules();

            //如果当前输入模块 m_CurrentInputModule 为空,或者不为 m_SystemInputModules 列表中首个符合条件的模块,则进行切换。
            bool changedModule = false;
            var systemInputModulesCount = m_SystemInputModules.Count;
            for (var i = 0; i < systemInputModulesCount; i++)
            {
                var module = m_SystemInputModules[i];
                if (module.IsModuleSupported() && module.ShouldActivateModule())
                {
                    if (m_CurrentInputModule != module)
                    {
                        ChangeEventModule(module);
                        changedModule = true;
                    }
                    break;
                }
            }

            // no event module set... set the first valid one...
            if (m_CurrentInputModule == null)
            {
                for (var i = 0; i < systemInputModulesCount; i++)
                {
                    var module = m_SystemInputModules[i];
                    if (module.IsModuleSupported())
                    {
                        ChangeEventModule(module);
                        changedModule = true;
                        break;
                    }
                }
            }

            // 如果此帧没有进行模块切换,则对当前输入模块 m_CurrentInputModule 执行 Process 方法进行处理。 
            if (!changedModule && m_CurrentInputModule != null)
                m_CurrentInputModule.Process();

            ...
        }

        ...
    }
  • m_SystemInputModules 列表是在 EventSystem.UpdateModules 方法中生成的,即 EventSystem 所属 GameObject 上所有激活的 BaseInputModule 对象。
    public class EventSystem : UIBehaviour
    {
        ...
    
        public void UpdateModules()
        {
            GetComponents(m_SystemInputModules);
            var systemInputModulesCount = m_SystemInputModules.Count;
            for (int i = systemInputModulesCount - 1; i >= 0; i--)
            {
                if (m_SystemInputModules[i] && m_SystemInputModules[i].IsActive())
                    continue;

                m_SystemInputModules.RemoveAt(i);
            }
        }

        ...
    }
  • 因此,GameObject 上会挂载 EventSystem 组件,同时会挂载其需要的所有 InputModule 组件。

InputModule 输入模块

  • 从 EventSystem 中知道,InputModule 组件需要挂载到同一个 GameObject 上,并且只有激活的才会加入到 EventSystem.m_SystemInputModules 列表中。
  • 可以看到,BaseInputModule 中,当对象激活时,会获取组件上的 EventSystem,并且调用 EventSystem.UpdateModules 方法,从而实现将自身加入到 EventSystem.m_SystemInputModules 列表中。同样,当对象关闭时,会再次刷新 EventSystem.m_SystemInputModules 列表,将自身移除。
    public abstract class BaseInputModule : UIBehaviour
    {
        ...

        protected EventSystem eventSystem
        {
            get { return m_EventSystem; }
        }

        protected override void OnEnable()
        {
            base.OnEnable();
            m_EventSystem = GetComponent<EventSystem>();
            m_EventSystem.UpdateModules();
        }

        protected override void OnDisable()
        {
            m_EventSystem.UpdateModules();
            base.OnDisable();
        }
        
        ...
    }

  • EventSystem 更新时,会先触发 BaseInputModule.UpdateModule 方法,默认情况下,会使用 StandaloneInputModule 组件。其中 UpdateModule 方法,主要记录上一帧和当前帧的输入坐标。另外,如果当前没有聚焦,并且处于拖拽状态,则会释放按压状态,结束拖拽。
    public class StandaloneInputModule : PointerInputModule
    {
        ...

        public override void UpdateModule()
        {
            if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
            {
                if (m_InputPointerEvent != null && m_InputPointerEvent.pointerDrag != null && m_InputPointerEvent.dragging)
                {
                    ReleaseMouse(m_InputPointerEvent, m_InputPointerEvent.pointerCurrentRaycast.gameObject);
                }

                m_InputPointerEvent = null;

                return;
            }

            m_LastMousePosition = m_MousePosition;
            m_MousePosition = input.mousePosition;
        }
        
        ...
    }

  • EventSystem 还会对当前输入模块 m_CurrentInputModule 执行 Process 方法,主要进行:
    • 对当前选中的 GameObject ,发送更新事件(UpdateEvent)。
    • 处理触碰事件(TouchEvent)。
    • 如果没有触碰事件,则处理鼠标输入事件(MouseEvent)。
    • 对当前选中的 GameObject,发送移动事件(MoveEvent)。
    public class StandaloneInputModule : PointerInputModule
    {
        ...

        public override void Process()
        {
            if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
                return;

            bool usedEvent = SendUpdateEventToSelectedObject();

            // case 1004066 - touch / mouse events should be processed before navigation events in case
            // they change the current selected gameobject and the submit button is a touch / mouse button.

            // touch needs to take precedence because of the mouse emulation layer
            if (!ProcessTouchEvents() && input.mousePresent)
                ProcessMouseEvent();

            if (eventSystem.sendNavigationEvents)
            {
                if (!usedEvent)
                    usedEvent |= SendMoveEventToSelectedObject();

                if (!usedEvent)
                    SendSubmitEventToSelectedObject();
            }
        }
        
        ...
    }

  • 其中,主要关注的是 TouchEvent 和 MouseEvent。TouchEvent 的主要流程为:
    • 调用 EventSystem.RaycastAll 方法,获取射线检测到的第一个 GameObject 。
    • 根据当前的按压(pressed)状态、释放(released)状态,发送不同的事件(PointerDown、PointerClick、Drag 等)。
    • 如果当前没有释放,则进行移动和拖拽处理。
    public class StandaloneInputModule : PointerInputModule
    {
        ...

        private bool ProcessTouchEvents()
        {
            for (int i = 0; i < input.touchCount; ++i)
            {
                Touch touch = input.GetTouch(i);

                if (touch.type == TouchType.Indirect)
                    continue;

                bool released;
                bool pressed;
                var pointer = GetTouchPointerEventData(touch, out pressed, out released);

                ProcessTouchPress(pointer, pressed, released);

                if (!released)
                {
                    ProcessMove(pointer);
                    ProcessDrag(pointer);
                }
                else
                    RemovePointerData(pointer);
            }
            return input.touchCount > 0;
        }
        
        ...
    }

    public abstract class PointerInputModule : BaseInputModule
    {
        ...

        protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
        {

            ...

            if (input.phase == TouchPhase.Canceled)
            {
                pointerData.pointerCurrentRaycast = new RaycastResult();
            }
            else
            {
                eventSystem.RaycastAll(pointerData, m_RaycastResultCache);

                var raycast = FindFirstRaycast(m_RaycastResultCache);
                pointerData.pointerCurrentRaycast = raycast;
                m_RaycastResultCache.Clear();
            }
            return pointerData;
        }
        
        ...
    }

  • MouseEvent 和 TouchEvent类似,同样是调用 EventSystem.RaycastAll 方法,获取射线检测到的第一个 GameObject ,对其发送各种事件。
    public class StandaloneInputModule : PointerInputModule
    {
        ...

        protected void ProcessMouseEvent(int id)
        {
            var mouseData = GetMousePointerEventData(id);
            var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;

            m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;

            // Process the first mouse button fully
            ProcessMousePress(leftButtonData);
            ProcessMove(leftButtonData.buttonData);
            ProcessDrag(leftButtonData.buttonData);

            // Now process right / middle clicks
            ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
            ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
            ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
            ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);

            if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
            {
                var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
                ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
            }
        }

        
        ...
    }

    public abstract class PointerInputModule : BaseInputModule
    {
        ...

        protected virtual MouseState GetMousePointerEventData(int id)
        {
            ...

            eventSystem.RaycastAll(leftData, m_RaycastResultCache);
            var raycast = FindFirstRaycast(m_RaycastResultCache);
            leftData.pointerCurrentRaycast = raycast;
            m_RaycastResultCache.Clear();

            ...

            return m_MouseState;
        }
        
        ...
    }

Raycast 射线检测

  • EventSystem.RaycastAll 方法,对 RaycasterManager 中 s_Raycasters 列表的激活对象,执行 BaseRaycaster.Raycast 方法,来查找指向的对象。
    public class EventSystem : UIBehaviour
    {
        ...
    
        public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
        {
            raycastResults.Clear();
            var modules = RaycasterManager.GetRaycasters();
            var modulesCount = modules.Count;
            for (int i = 0; i < modulesCount; ++i)
            {
                var module = modules[i];
                if (module == null || !module.IsActive())
                    continue;

                module.Raycast(eventData, raycastResults);
            }

            raycastResults.Sort(s_RaycastComparer);
        }

        ...
    }
  • s_Raycasters 列表的对象,是通过 RaycasterManager.AddRaycaster 方法加入的。BaseRaycaster 对象激活时,OnEnable 方法,将自身注册到 s_Raycasters 列表中,关闭时通过 OnDisable 方法将自身移除。所以 RayCaster 对象,也是以组件的形式挂载到 GameObject 上,根据不同的子类型,重写 Raycast 方法,可以实现不同的射线检测模式。
    public abstract class BaseRaycaster : UIBehaviour
    {
        ...

        public abstract void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList);

        ...

        protected override void OnEnable()
        {
            base.OnEnable();
            RaycasterManager.AddRaycaster(this);
        }

        protected override void OnDisable()
        {
            RaycasterManager.RemoveRaycasters(this);
            base.OnDisable();
        }
        
        ...
    }

PhysicsRaycaster

  • PhysicRaycaster 的 Raycast 代码如下:
    public class PhysicsRaycaster : BaseRaycaster
    {
        ...

        public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
        {
#if PACKAGE_PHYSICS
            Ray ray = new Ray();
            int displayIndex = 0;
            float distanceToClipPlane = 0;
            if (!ComputeRayAndDistance(eventData, ref ray, ref displayIndex, ref distanceToClipPlane))
                return;

            int hitCount = 0;

            if (m_MaxRayIntersections == 0)
            {
                if (ReflectionMethodsCache.Singleton.raycast3DAll == null)
                    return;

                m_Hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, finalEventMask);
                hitCount = m_Hits.Length;
            }
            else
            {
                if (ReflectionMethodsCache.Singleton.getRaycastNonAlloc == null)
                    return;
                if (m_LastMaxRayIntersections != m_MaxRayIntersections)
                {
                    m_Hits = new RaycastHit[m_MaxRayIntersections];
                    m_LastMaxRayIntersections = m_MaxRayIntersections;
                }

                hitCount = ReflectionMethodsCache.Singleton.getRaycastNonAlloc(ray, m_Hits, distanceToClipPlane, finalEventMask);
            }

            if (hitCount != 0)
            {
                if (hitCount > 1)
                    System.Array.Sort(m_Hits, 0, hitCount, RaycastHitComparer.instance);

                for (int b = 0, bmax = hitCount; b < bmax; ++b)
                {
                    var result = new RaycastResult
                    {
                        gameObject = m_Hits[b].collider.gameObject,
                        module = this,
                        distance = m_Hits[b].distance,
                        worldPosition = m_Hits[b].point,
                        worldNormal = m_Hits[b].normal,
                        screenPosition = eventData.position,
                        displayIndex = displayIndex,
                        index = resultAppendList.Count,
                        sortingLayer = 0,
                        sortingOrder = 0
                    };
                    resultAppendList.Add(result);
                }
            }
#endif
        }

        ...
    }
  • 可以看到,PhysicsRaycaster.Raycast 主要做了:
    • 通过反射方式,调用 Physics.RaycastAll 或 Physics.Raycast 方法,进行射线检测。
    • 对检测结果进行排序。
    • 将结果转成 List 对象返回。
  • 射线检测是 Unity 的内部方法,通过发射射线,获取所有穿过的碰撞体对象,而排序则是通过 RaycastHitComparer.Compare 实现的。
    public class PhysicsRaycaster : BaseRaycaster
    {
        ...

#if PACKAGE_PHYSICS
        private class RaycastHitComparer : IComparer<RaycastHit>
        {
            public static RaycastHitComparer instance = new RaycastHitComparer();
            public int Compare(RaycastHit x, RaycastHit y)
            {
                return x.distance.CompareTo(y.distance);
            }
        }
#endif

    }
  • RaycastHitComparer 只考虑远近距离,即通过比较射线接触的物体的距离,从近到远进行排序。

Physics2DRaycaster

  • Physics2DRaycaster 的 Raycast 代码如下:
    public class Physics2DRaycaster : PhysicsRaycaster
    {
        ...

        public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
        {
#if PACKAGE_PHYSICS2D
            Ray ray = new Ray();
            float distanceToClipPlane = 0;
            int displayIndex = 0;
            if (!ComputeRayAndDistance(eventData, ref ray, ref displayIndex, ref distanceToClipPlane))
                return;

            int hitCount = 0;

            if (maxRayIntersections == 0)
            {
                if (ReflectionMethodsCache.Singleton.getRayIntersectionAll == null)
                    return;
                m_Hits = ReflectionMethodsCache.Singleton.getRayIntersectionAll(ray, distanceToClipPlane, finalEventMask);
                hitCount = m_Hits.Length;
            }
            else
            {
                if (ReflectionMethodsCache.Singleton.getRayIntersectionAllNonAlloc == null)
                    return;

                if (m_LastMaxRayIntersections != m_MaxRayIntersections)
                {
                    m_Hits = new RaycastHit2D[maxRayIntersections];
                    m_LastMaxRayIntersections = m_MaxRayIntersections;
                }

                hitCount = ReflectionMethodsCache.Singleton.getRayIntersectionAllNonAlloc(ray, m_Hits, distanceToClipPlane, finalEventMask);
            }

            if (hitCount != 0)
            {
                for (int b = 0, bmax = hitCount; b < bmax; ++b)
                {
                    Renderer r2d = null;
                    // Case 1198442: Check for 2D renderers when filling in RaycastResults
                    var rendererResult = m_Hits[b].collider.gameObject.GetComponent<Renderer>();
                    if (rendererResult != null)
                    {
                        if (rendererResult is SpriteRenderer)
                        {
                            r2d = rendererResult;
                        }
#if PACKAGE_TILEMAP
                        if (rendererResult is TilemapRenderer)
                        {
                            r2d = rendererResult;
                        }
#endif
                        if (rendererResult is SpriteShapeRenderer)
                        {
                            r2d = rendererResult;
                        }
                    }

                    var result = new RaycastResult
                    {
                        gameObject = m_Hits[b].collider.gameObject,
                        module = this,
                        distance = Vector3.Distance(eventCamera.transform.position, m_Hits[b].point),
                        worldPosition = m_Hits[b].point,
                        worldNormal = m_Hits[b].normal,
                        screenPosition = eventData.position,
                        displayIndex = displayIndex,
                        index = resultAppendList.Count,
                        sortingLayer =  r2d != null ? r2d.sortingLayerID : 0,
                        sortingOrder = r2d != null ? r2d.sortingOrder : 0
                    };
                    resultAppendList.Add(result);
                }
            }
#endif
        }

        ...
    }
  • Physics2DRaycaster.Raycast 和 PhysicsRaycaster.Raycast 相比,少了排序的操作,其主要做了:
    • 通过反射调用 Physics2D.GetRayIntersectionAll 或 Physics2D.GetRayIntersectionNonAlloc 进行射线检测。
    • 获取射线检测到的对象上的 Renderer(SpriteRenderer、TilemapRenderer、SpriteShapeRenderer)。
    • 将结果转成 List 对象返回。

GraphicRaycaster

  • GraphicRaycaster 重写的 Raycast 方法,部分代码如下:
    public class GraphicRaycaster : BaseRaycaster
    {
        ...

         public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
        {
            if (canvas == null)
                return;
            // 获取 canvas 下的 graphic 对象
            var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);
            if (canvasGraphics == null || canvasGraphics.Count == 0)
                return;

            ...

            // 获取事件坐标
            var eventPosition = Display.RelativeMouseAt(eventData.position);
           
            ...

            float hitDistance = float.MaxValue;

            Ray ray = new Ray();

            if (currentEventCamera != null)
                ray = currentEventCamera.ScreenPointToRay(eventPosition);

            ...

            m_RaycastResults.Clear();

            // 进行 Raycast 检测
            Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);

            // 将结果转到 List<RaycastResult> 列表返回
            int totalCount = m_RaycastResults.Count;
            for (var index = 0; index < totalCount; index++)
            {
                var go = m_RaycastResults[index].gameObject;
                bool appendGraphic = true;
                
                if (ignoreReversedGraphics)
                {
                    if (currentEventCamera == null)
                    {
                        // If we dont have a camera we know that we should always be facing forward
                        var dir = go.transform.rotation * Vector3.forward;
                        appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
                    }
                    else
                    {
                        // If we have a camera compare the direction against the cameras forward.
                        var cameraForward = currentEventCamera.transform.rotation * Vector3.forward * currentEventCamera.nearClipPlane;
                        appendGraphic = Vector3.Dot(go.transform.position - currentEventCamera.transform.position - cameraForward, go.transform.forward) >= 0;
                    }
                }

                if (appendGraphic)
                {
                    float distance = 0;
                    Transform trans = go.transform;
                    Vector3 transForward = trans.forward;

                    if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
                        distance = 0;
                    else
                    {
                        // http://geomalgorithms.com/a06-_intersect-2.html
                        distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));

                        // Check to see if the go is behind the camera.
                        if (distance < 0)
                            continue;
                    }

                    if (distance >= hitDistance)
                        continue;

                    var castResult = new RaycastResult
                    {
                        gameObject = go,
                        module = this,
                        distance = distance,
                        screenPosition = eventPosition,
                        displayIndex = displayIndex,
                        index = resultAppendList.Count,
                        depth = m_RaycastResults[index].depth,
                        sortingLayer = canvas.sortingLayerID,
                        sortingOrder = canvas.sortingOrder,
                        worldPosition = ray.origin + ray.direction * distance,
                        worldNormal = -transForward
                    };
                    resultAppendList.Add(castResult);
                }
            }
        }

        ...
    }

  • 从流程上看,GraphicRaycaster 重写的 Raycast 方法,创建射线后,并没有直接使用射线检测,而是通过调用内部的 Raycast 方法进行检测,再将结果返回。
    public class GraphicRaycaster : BaseRaycaster
    {
        ...

        [NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
        private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
        {
            // Necessary for the event system
            int totalCount = foundGraphics.Count;
            for (int i = 0; i < totalCount; ++i)
            {
                Graphic graphic = foundGraphics[i];

                // -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
                if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1)
                    continue;

                if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera, graphic.raycastPadding))
                    continue;

                if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
                    continue;

                if (graphic.Raycast(pointerPosition, eventCamera))
                {
                    s_SortedGraphics.Add(graphic);
                }
            }

            s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
            totalCount = s_SortedGraphics.Count;
            for (int i = 0; i < totalCount; ++i)
                results.Add(s_SortedGraphics[i]);

            s_SortedGraphics.Clear();
        }

    }
  • GraphicRaycaster 内部的 Raycast 方法,主要步骤:
    • 剔除不需要进行检测的情况(如 raycastTarget 关闭,响应点坐标不在 rectTransform 范围内、超过远裁剪平面等)。
    • 调用 Graphic.Raycast 方法,判断当前 graphic 是否符合响应坐标,符合则加入 s_SortedGraphics 列表。
    • 对 s_SortedGraphics 列表的对象,根据深度从大到小排序。
    • 将 s_SortedGraphics 对象转到 results 中并返回。
  • Graphic.Raycast 方法的代码如下:
    public abstract class Graphic : UIBehaviour, ICanvasElement
    {
        ...

        public virtual bool Raycast(Vector2 sp, Camera eventCamera)
        {
            if (!isActiveAndEnabled)
                return false;

            var t = transform;
            var components = ListPool<Component>.Get();

            bool ignoreParentGroups = false;
            bool continueTraversal = true;

            while (t != null)
            {
                t.GetComponents(components);
                for (var i = 0; i < components.Count; i++)
                {
                    var canvas = components[i] as Canvas;
                    if (canvas != null && canvas.overrideSorting)
                        continueTraversal = false;

                    var filter = components[i] as ICanvasRaycastFilter;

                    if (filter == null)
                        continue;

                    var raycastValid = true;

                    var group = components[i] as CanvasGroup;
                    if (group != null)
                    {
                        if (ignoreParentGroups == false && group.ignoreParentGroups)
                        {
                            ignoreParentGroups = true;
                            raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
                        }
                        else if (!ignoreParentGroups)
                            raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
                    }
                    else
                    {
                        raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
                    }

                    if (!raycastValid)
                    {
                        ListPool<Component>.Release(components);
                        return false;
                    }
                }
                t = continueTraversal ? t.parent : null;
            }
            ListPool<Component>.Release(components);
            return true;
        }

        ...
    }
  • Graphic.Raycast 方法的流程为:
    • 获取自身所有 Graphic 组件。
    • 对每一个继承 ICanvasRaycastFilter 接口的 Graphic 组件,执行 ICanvasRaycastFilter.IsRaycastLocationValid 方法,检测响应点是否对此组件有效,如果无效则直接返回。
    • 逐级向上,直到 Canvas 设置了 overrideSorting ,或者直到根节点,返回有效。
  • GraphicRaycaster 内部的 Raycast 方法主要通过 RectTransform 的 Rect 来过滤组件,而 Graphic 的 Raycast 主要是用来确认该组件是否被射线检测命中。

ExecuteEvents 执行事件

  • 射线检测模块完成后,得到了 RaycastResult 数据,接下来就要执行相关的事件。继续看 StandaloneInputModule.ProcessTouchPress 方法,部分代码如下:
    public class StandaloneInputModule : PointerInputModule
    {
        ...

        protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
        {
            var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

            // PointerDown notification
            if (pressed)
            {
                ...

                var newClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

                ...

                pointerEvent.pointerClick = newClick;

                ...
            }

            // PointerUp notification
            if (released)
            {
                ...

                // see if we mouse up on the same element that we clicked on...
                var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

                // PointerClick and Drop events
                if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick)
                {
                    ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
                }

                ...

                pointerEvent.pointerClick = null;

                ...
            }

            m_InputPointerEvent = pointerEvent;
        }
        
        ...
    }

  • 这里以点击事件为例,射线检测完成后,得到了目标 gameObject ,当按下时,通过调用 ExecuteEvents.GetEventHandler 方法获取点击事件响应的最终 gameObject。当释放时,如果还是同一个 gameObject,则执行 ExecuteEvents.Execute 方法执行点击事件。
    public static class ExecuteEvents
    {
        ...

        private static bool ShouldSendToComponent<T>(Component component) where T : IEventSystemHandler
        {
            var valid = component is T;
            if (!valid)
                return false;

            var behaviour = component as Behaviour;
            if (behaviour != null)
                return behaviour.isActiveAndEnabled;
            return true;
        }

        /// <summary>
        /// Get the specified object's event event.
        /// </summary>
        private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler
        {
            // Debug.LogWarning("GetEventList<" + typeof(T).Name + ">");
            if (results == null)
                throw new ArgumentException("Results array is null", "results");

            if (go == null || !go.activeInHierarchy)
                return;

            var components = ListPool<Component>.Get();
            go.GetComponents(components);

            var componentsCount = components.Count;
            for (var i = 0; i < componentsCount; i++)
            {
                if (!ShouldSendToComponent<T>(components[i]))
                    continue;

                // Debug.Log(string.Format("{2} found! On {0}.{1}", go, s_GetComponentsScratch[i].GetType(), typeof(T)));
                results.Add(components[i] as IEventSystemHandler);
            }
            ListPool<Component>.Release(components);
            // Debug.LogWarning("end GetEventList<" + typeof(T).Name + ">");
        }

        /// <summary>
        /// Whether the specified game object will be able to handle the specified event.
        /// </summary>
        public static bool CanHandleEvent<T>(GameObject go) where T : IEventSystemHandler
        {
            var internalHandlers = s_HandlerListPool.Get();
            GetEventList<T>(go, internalHandlers);
            var handlerCount = internalHandlers.Count;
            s_HandlerListPool.Release(internalHandlers);
            return handlerCount != 0;
        }

        /// <summary>
        /// Bubble the specified event on the game object, figuring out which object will actually receive the event.
        /// </summary>
        public static GameObject GetEventHandler<T>(GameObject root) where T : IEventSystemHandler
        {
            if (root == null)
                return null;

            Transform t = root.transform;
            while (t != null)
            {
                if (CanHandleEvent<T>(t.gameObject))
                    return t.gameObject;
                t = t.parent;
            }
            return null;
        }
    }
  • 可以看到,对于点击事件 IPointerClickHandler,对射线检测得到的对象,会获取其挂载的 IPointerClickHandler 类型的激活状态的组件,找到了则返回 gameObject 。如果找不到,则从父节点中继续查找。
    public static class ExecuteEvents
    {
        ...

        public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
        {
            var internalHandlers = s_HandlerListPool.Get();
            GetEventList<T>(target, internalHandlers);
            //  if (s_InternalHandlers.Count > 0)
            //      Debug.Log("Executinng " + typeof (T) + " on " + target);

            var internalHandlersCount = internalHandlers.Count;
            for (var i = 0; i < internalHandlersCount; i++)
            {
                T arg;
                try
                {
                    arg = (T)internalHandlers[i];
                }
                catch (Exception e)
                {
                    var temp = internalHandlers[i];
                    Debug.LogException(new Exception(string.Format("Type {0} expected {1} received.", typeof(T).Name, temp.GetType().Name), e));
                    continue;
                }

                try
                {
                    functor(arg, eventData);
                }
                catch (Exception e)
                {
                    Debug.LogException(e);
                }
            }

            var handlerCount = internalHandlers.Count;
            s_HandlerListPool.Release(internalHandlers);
            return handlerCount > 0;
        }

        ...
    }
  • ExecuteEvents.Execute 方法,则根据对应的事件类型,如:点击事件 IPointerClickHandler,通过调用 GetEventList 获取类型为 IPointerClickHandler 的组件,并对所有执行 IPointerClickHandler.OnPointerClick(PointerEventData eventData) 方法,其他事件类型同理,可在 ExecuteEvents 中找到对应实现。

总结

  • 通过了解 Unity 的事件系统的运作流程,可以对其有更加清晰的认识,也能更好地使用。
  • 对于特殊的需求,也可以通过重写不同模块的方法来自定义,如:
    • 重写 InputModule 的 Process 方法, 自定义输入系统处理流程。
    • 重写 InputModule 的 GetTouchPointerEventData 方法,自定义触碰事件数据生成方法。
    • 重写 Raycast 的 Raycast 方法,自定义射线检测处理规则。