UGUI中点击判断的原理

UGUI中点击判断的原理_第1张图片
首选需要理解 EventSystem 中的代码结构,EventSystem 目录下包含4个子文件夹,分别是 EventData、InputModules,Raycasters 和 UIElements,UIElements 下是 UI Toolkit 相关代码,这里不做研究,主要关注其他三部分内容

EventData

UGUI中点击判断的原理_第2张图片
EventData存储事件信息,PointerEventData存储点击,拖动等信息,AxisEventData存储轴事件数据,如手柄摇杆

InputModules

UGUI中点击判断的原理_第3张图片
BaseInputModule(抽象类)引发事件并将其发送给 GameObjects 进行处理

Input是输入系统接口,可以获取输入信息如鼠标位置,但是这部分代码没有开源,而 BaseInput 是对 Input 类的封装

BaseInputModule 有个 BaseInput 类型的变量 m_DefaultInput,子类通过它获取一些输入信息

PointerInputModule(抽象类)用于处理点击类型的输入

StandaloneInputModule 是PC、Mac&Linux上的具体实现,而 TouchInputModule 是IOS、Android等移动平台上的具体实现

UGUI中点击判断的原理_第4张图片
EventSystem 上面挂着 StandaloneInputModule 组件,并且运行时会添加 BaseInput 脚本

Raycasters

UGUI中点击判断的原理_第5张图片
RaycasterManager 中有个List管理所有的Raycaster

BaseRaycaster(抽象类)负责对场景元素进行射线检测以确定光标是否在它们上面,sortOrderPriority和renderOrderPriority这两个字段用来对结果进行排序,在OnEnable中注册到RaycasterManager,在OnDisable移除,Raycast是抽象方法,在子类中实现

PhysicsRaycaster 是对3D组件进行射线检测,结果根据距离进行排序

Physics2DRaycaster 是对2D组件进行射线检测

GraphicRaycaster 对Graphic元素进行射线检测,所有UI元素(Image,RawImage等)的基类都是Graphic,这里最重要的是2个Raycast重载函数,这里略去不重要的代码

public class GraphicRaycaster : BaseRaycaster
{
    [NonSerialized] private List<Graphic> m_RaycastResults = new List<Graphic>();

    /// 
    /// 对Canvas相关的Graphic元素进行射线检测,结果存入resultAppendList
    /// 
    public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
    {
        if (canvas == null)
            return;

		// 获取Canvas上所有可点击,即raycastTarget = true的Graphic元素
        var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);
        if (canvasGraphics == null || canvasGraphics.Count == 0)
            return;

        int displayIndex;
        // Canvas为ScreenSpaceOverlay模式下默认为null
        var currentEventCamera = eventCamera;

        if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
            displayIndex = canvas.targetDisplay;
        else
            displayIndex = currentEventCamera.targetDisplay;

		// 获取鼠标位置
        var eventPosition = Display.RelativeMouseAt(eventData.position);
        if (eventPosition != Vector3.zero)
        {
            int eventDisplayIndex = (int)eventPosition.z;
            if (eventDisplayIndex != displayIndex)
                return;
        }
        else
        {
            eventPosition = eventData.position;
        }

        // 把eventPosition转换到相机空间
        Vector2 pos;
        if (currentEventCamera == null)
        {
            float w = Screen.width;
            float h = Screen.height;
            if (displayIndex > 0 && displayIndex < Display.displays.Length)
            {
                w = Display.displays[displayIndex].systemWidth;
                h = Display.displays[displayIndex].systemHeight;
            }
            pos = new Vector2(eventPosition.x / w, eventPosition.y / h);
        }
        else
            pos = currentEventCamera.ScreenToViewportPoint(eventPosition);

        // 不在相机视口内
        if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
            return;

        float hitDistance = float.MaxValue;

        Ray ray = new Ray();
		// currentEventCamera不为空,摄像机发射射线
        if (currentEventCamera != null)
            ray = currentEventCamera.ScreenPointToRay(eventPosition);

        if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
        {
			// 这里主要检测2D/3D物体对UI的遮挡的,略
        }

        m_RaycastResults.Clear();

        Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);

        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)
                {
                    // 点乘判断元素是否面向前方
                    var dir = go.transform.rotation * Vector3.forward;
                    appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
                }
                else
                {
                    // 与摄像头的前方进行比较
                    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
                {
                    distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));

                    // 是否在相机后面
                    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);
            }
        }
    }

    /// 
    /// 重载函数,收集点击位置处的Graphic元素
    /// 
    [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)
    {
		// foundGraphics是Canvas上所有可点击的Graphic元素
        int totalCount = foundGraphics.Count;
        for (int i = 0; i < totalCount; ++i)
        {
            Graphic graphic = foundGraphics[i];

            // -1表示Canvas尚未对其进行处理,也就是说它实际上并未绘制
            if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1)
                continue;

			// 检测点击位置是否在RectTransform形成的矩形内
            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();
    }
}

跳转到 graphic.Raycast 方法查看后续的检测,这里获取 graphic 上的所有组件转换为接口 ICanvasRaycastFilter,然后调用接口的 IsRaycastLocationValid 方法进行最后的检测

public abstract class Graphic: UIBehaviour,ICanvasElement
{
    /// 
    /// 确定点击的位置是否合法,sp是Screen point,返回True表示合法
    /// 
    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;
    }
}

UGUI中点击判断的原理_第6张图片
Image、Mask以及CanvasGroup都实现了ICanvasRaycastFilter接口,查看Image的具体实现

public class Image : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
{
	/// 
    /// 计算位置是否为有效的命中位置,根据图片的Alpha值是否大于阈值
    /// 
	public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
    {
        if (alphaHitTestMinimumThreshold <= 0)
            return true;

        if (alphaHitTestMinimumThreshold > 1)
            return false;

        if (activeSprite == null)
            return true;

        Vector2 local;
        if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local))
            return false;

        Rect rect = GetPixelAdjustedRect();

        // Convert to have lower left corner as reference point.
        local.x += rectTransform.pivot.x * rect.width;
        local.y += rectTransform.pivot.y * rect.height;

        local = MapCoordinate(local, rect);

        // Convert local coordinates to texture space.
        Rect spriteRect = activeSprite.textureRect;
        float x = (spriteRect.x + local.x) / activeSprite.texture.width;
        float y = (spriteRect.y + local.y) / activeSprite.texture.height;

        try
        {
            return activeSprite.texture.GetPixelBilinear(x, y).a >= alphaHitTestMinimumThreshold;
        }
        catch (UnityException e)
        {
            Debug.LogError("Using alphaHitTestMinimumThreshold greater than 0 on Image whose sprite texture cannot be read. " + e.Message + " Also make sure to disable sprite packing for this sprite.", this);
            return true;
        }
    }
}

注意:所有勾选 RaycastTarget 的UI元素都会进行射线检测,参与一些坐标,排序计算,数量太多会影响效率,因此不需要交互的元素不要勾选 RaycastTarget

EventSystem

EventSystem 处理输入、射线检测和发送事件,在 EventSystem 中有个列表 m_SystemInputModules 保存所有的输入模块,在Update中更新当前输入模块m_CurrentInputModule,并调用CurrentInputModule.Process方法进行输入处理

在输入模块如 PointerInputModule 中会调用EventSystem.RaycastAll,进行射线检测并将结果保存到列表中,然后调用EventSystem.RaycastComparer对结果进行排序

public class EventSystem : UIBehaviour
{
	// 所有的输入模块
    private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();
	// 当前输入模块,PC上就是StandaloneInputModule
    private BaseInputModule m_CurrentInputModule;

    protected virtual void Update()
    {
    	// 更新m_SystemInputModules中所有的输入模块
        TickModules();

		// 更新当前输入模块
        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;
            }
        }

        // 当前输入模块为空,选择第一个有效的,略

		// Process方法中处理输入
        if (!changedModule && m_CurrentInputModule != null)
            m_CurrentInputModule.Process();
    }

    /// 
    /// 进行射线检测并将结果添加到raycastResults
    /// 
    public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
    {
        raycastResults.Clear();
        // 获取所有注册的BaseRaycaster对象,如GraphicRaycaster,PhysicsRaycaster,Physics2DRaycaster
        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;

			// 进行射线检测,并将结果添加到raycastResults
            module.Raycast(eventData, raycastResults);
        }

		// 对结果进行排序,保证UI优先处理
        raycastResults.Sort(s_RaycastComparer);
    }

	private static readonly Comparison<RaycastResult> s_RaycastComparer = RaycastComparer;

	/// 
    /// 对射线检测的结果进行比较
    /// 
    private static int RaycastComparer(RaycastResult lhs, RaycastResult rhs)
    {
        if (lhs.module != rhs.module)
        {
            var lhsEventCamera = lhs.module.eventCamera;
            var rhsEventCamera = rhs.module.eventCamera;
            if (lhsEventCamera != null && rhsEventCamera != null && lhsEventCamera.depth != rhsEventCamera.depth)
            {
                // 比较camera深度
                if (lhsEventCamera.depth < rhsEventCamera.depth)
                    return 1;
                if (lhsEventCamera.depth == rhsEventCamera.depth)
                    return 0;

                return -1;
            }
			
			// 比较排序优先级
            if (lhs.module.sortOrderPriority != rhs.module.sortOrderPriority)
                return rhs.module.sortOrderPriority.CompareTo(lhs.module.sortOrderPriority);

			// 比较渲染优先级
            if (lhs.module.renderOrderPriority != rhs.module.renderOrderPriority)
                return rhs.module.renderOrderPriority.CompareTo(lhs.module.renderOrderPriority);
        }

		// 比较sortingLayer
        if (lhs.sortingLayer != rhs.sortingLayer)
        {
            var rid = SortingLayer.GetLayerValueFromID(rhs.sortingLayer);
            var lid = SortingLayer.GetLayerValueFromID(lhs.sortingLayer);
            return rid.CompareTo(lid);
        }

		// 比较sortingOrder 
        if (lhs.sortingOrder != rhs.sortingOrder)
            return rhs.sortingOrder.CompareTo(lhs.sortingOrder);

        // 比较深度
        if (lhs.depth != rhs.depth && lhs.module.rootRaycaster == rhs.module.rootRaycaster)
            return rhs.depth.CompareTo(lhs.depth);

		// 比较距离
        if (lhs.distance != rhs.distance)
            return lhs.distance.CompareTo(rhs.distance);

		// 比较index
        return lhs.index.CompareTo(rhs.index);
    }
}

查看 StandaloneInputModule 中鼠标事件相关方法,这里为了简洁只关注点击方法

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

        bool usedEvent = SendUpdateEventToSelectedObject();

        // 优先处理触摸事件,如果有鼠标则处理鼠标事件
        if (!ProcessTouchEvents() && input.mousePresent)
            ProcessMouseEvent();
    }

	protected void ProcessMouseEvent()
    {
        ProcessMouseEvent(0);
    }

	/// 
    /// 处理所有的鼠标事件
    /// 
    protected void ProcessMouseEvent(int id)
    {
        var mouseData = GetMousePointerEventData(id);
        var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;

        m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;

        // 优先处理鼠标左键
        ProcessMousePress(leftButtonData);
        // 处理右键和中键,按下和拖动事件,略。。。
    }

    /// 
    /// 计算和处理任何鼠标按钮的状态变化
    /// 
    protected void ProcessMousePress(MouseButtonEventData data)
    {
        var pointerEvent = data.buttonData;
        var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

        // 按下
        if (data.PressedThisFrame())
        {
        	// 给pointerEvent填充数据,略
        }

        // 抬起
        if (data.ReleasedThisFrame())
        {
            ReleaseMouse(pointerEvent, currentOverGo);
        }
    }

    private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
    {
    	// 触发抬起事件
        ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
        var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick)
        {
        	// 触发鼠标点击事件
            ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
        }
        // 处理拖动相关事件,略
    }
}

查看 ExecuteEvents 的具体实现,简单说就是将组件转为IEventSystemHandler,然后执行委托方法

public static class ExecuteEvents
{
    public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
    {
        var internalHandlers = ListPool<IEventSystemHandler>.Get();
        GetEventList<T>(target, internalHandlers);

        var internalHandlersCount = internalHandlers.Count;
        for (var i = 0; i < internalHandlersCount; i++)
        {
            T arg;
            try
            {
            	// 转换成目标类型,如IPointerClickHandler
                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;
        ListPool<IEventSystemHandler>.Release(internalHandlers);
        return handlerCount > 0;
    }

	/// 
    /// 将组件转换成IEventSystemHandler,并放入列表中
    /// 
    private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler
    {
        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;
            results.Add(components[i] as IEventSystemHandler);
        }
        ListPool<Component>.Release(components);
    }
}

UGUI中点击判断的原理_第7张图片
上述代码中的 IEventSystemHandler 是很多其他接口的父接口,Button实现了IPointerClickHandler
所以点击Button,会通过 IPointerClickHandler 接口调用它的 OnPointerClick 方法

参考

关于UGUI中点击判断的原理探究

[UGUI源码一]6千字带你入门UGUI源码

UGUI的点击事件机制

你可能感兴趣的:(Unity,unity)