Unity UGUI GraphicRaycaster.Raycast详解

一、源码

/// 
/// 对当前 Canvas 上的所有可交互 UI 图形执行射线检测,判断是否被点击或触碰。
/// 
/// 指针事件的数据(包含鼠标位置、触摸点等)
/// 用于存储命中的 UI 元素结果列表
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
    // 如果 Canvas 不存在,则无法进行任何 UI 检测,直接返回
    if (canvas == null)
        return;

    // 获取当前 Canvas 中所有可以参与射线检测的 UI 元素(如 Image、Text 等)
    var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);

    // 如果没有图形元素或数量为 0,说明没有需要检测的 UI,直接返回
    if (canvasGraphics == null || canvasGraphics.Count == 0)
        return;

    int displayIndex;
    var currentEventCamera = eventCamera; // 缓存摄像机引用(避免多次调用 Camera.main)

    // 根据 Canvas 渲染模式决定使用哪个显示设备索引(多显示器支持)
    if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
        displayIndex = canvas.targetDisplay; // Overlay 模式下直接使用 Canvas 自身设置的显示器
    else
        displayIndex = currentEventCamera.targetDisplay; // 否则使用摄像机所指向的显示器

    // 获取鼠标在屏幕上的相对坐标(考虑多显示器情况)
    var eventPosition = MultipleDisplayUtilities.RelativeMouseAtScaled(eventData.position);

    // 如果返回值不是 Vector3.zero,表示平台支持多显示器系统
    if (eventPosition != Vector3.zero)
    {
        int eventDisplayIndex = (int)eventPosition.z;

        // 如果点击发生在其他显示器上,则忽略该事件,防止跨屏操作
        if (eventDisplayIndex != displayIndex)
            return;
    }
    else
    {
        // 平台不支持多显示器时,使用原始事件位置
        eventPosition = eventData.position;

#if UNITY_EDITOR
        // 在 Unity Editor 中,如果 GameView 当前目标显示器与 DisplayIndex 不一致,也忽略事件
        if (Display.activeEditorGameViewTarget != displayIndex)
            return;

        // 补充 z 值为当前 GameView 目标显示器编号
        eventPosition.z = Display.activeEditorGameViewTarget;
#endif
    }

    // 将事件位置转换为视口空间坐标(范围 [0,1])
    Vector2 pos;
    if (currentEventCamera == null)
    {
        // 如果是 ScreenSpaceOverlay 模式或没有摄像机,使用屏幕分辨率计算比例
        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;

    // 初始化阻挡距离为最大值,表示默认没有阻挡物挡住 UI
    float hitDistance = float.MaxValue;

    Ray ray = new Ray();

    // 如果有摄像机,生成从摄像机出发到鼠标位置的射线
    if (currentEventCamera != null)
        ray = currentEventCamera.ScreenPointToRay(eventPosition);

    // 如果不是 Overlay 模式,并且启用了阻挡对象检测(即检查是否有 2D/3D 物体遮挡 UI)
    if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
    {
        // 设置一个默认的射线检测距离(100单位),用于限制检测深度
        float distanceToClipPlane = 100.0f;

        // 如果有摄像机,根据摄像机参数动态计算射线长度
        if (currentEventCamera != null)
        {
            float projectionDirection = ray.direction.z;
            // 避免除以零,处理正交投影等情况
            distanceToClipPlane = Mathf.Approximately(0.0f, projectionDirection)
                ? Mathf.Infinity
                : Mathf.Abs((currentEventCamera.farClipPlane - currentEventCamera.nearClipPlane) / projectionDirection);
        }

#if PACKAGE_PHYSICS
        // 如果启用了 3D 阻挡检测
        if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All)
        {
            if (ReflectionMethodsCache.Singleton.raycast3D != null)
            {
                // 执行 3D 射线检测,获取所有命中物体
                var hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, (int)m_BlockingMask);
                if (hits.Length > 0)
                    hitDistance = hits[0].distance; // 记录最近的阻挡距离
            }
        }
#endif

#if PACKAGE_PHYSICS2D
        // 如果启用了 2D 阻挡检测
        if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All)
        {
            if (ReflectionMethodsCache.Singleton.raycast2D != null)
            {
                // 执行 2D 射线检测,获取所有命中物体
                var hits = ReflectionMethodsCache.Singleton.getRayIntersectionAll(ray, distanceToClipPlane, (int)m_BlockingMask);
                if (hits.Length > 0)
                    hitDistance = hits[0].distance; // 记录最近的阻挡距离
            }
        }
#endif
    }

    // 清空临时结果列表,准备存储本次射线检测的结果
    m_RaycastResults.Clear();

    // 执行对 UI 元素的实际射线检测
    Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);

    int totalCount = m_RaycastResults.Count;

    // 遍历所有命中候选对象,筛选出最终有效的 UI 结果
    for (var index = 0; index < totalCount; index++)
    {
        var go = m_RaycastResults[index].gameObject;
        bool appendGraphic = true;

        // 如果启用了反向剔除(ignoreReversedGraphics),检查 UI 是否朝向摄像机
        if (ignoreReversedGraphics)
        {
            if (currentEventCamera == null)
            {
                // 没有摄像机时,默认 UI 是正向的
                var dir = go.transform.rotation * Vector3.forward;
                appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
            }
            else
            {
                // 有摄像机时,比较 UI 正面和摄像机方向
                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;

            // 如果是 Overlay 模式或没有摄像机,距离为 0
            if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
            {
                distance = 0;
            }
            else
            {
                // 使用几何算法计算射线与 UI 平面的交点距离
                distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));

                // 如果物体在摄像机后方,跳过
                if (distance < 0)
                    continue;
            }

            // 如果 UI 被 3D/2D 物体挡住,跳过
            if (distance >= hitDistance)
                continue;

            // 构建最终的 RaycastResult 并添加进结果列表
            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);
        }
    }
}

这段代码是 Unity UGUI 中 GraphicRaycaster 的核心方法之一:Raycast(),用于 在 2D/3D 场景中检测鼠标点击事件是否命中 UI 元素


二、 目标

现在只关注 与 2D 点击交互相关的关键逻辑部分,并忽略以下非关键内容:

  • 多显示器支持
  • 3D 射线检测(blockingObjects)
  • 摄像机视口转换
  • 反向面剔除(ignoreReversedGraphics)

三、 整体流程简述

  1. 获取当前 Canvas 上所有可交互的 UI 图形(Graphic)
  2. 获取鼠标屏幕坐标
  3. 遍历这些图形,判断是否被鼠标“点中”
  4. 如果命中,就添加到 resultAppendList 中供后续事件系统使用

四、精简后关键代码解析

1. 获取当前 Canvas 上所有可被射线检测的 UI 元素

var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);
  • canvasGraphics 是一个 List,里面包含所有可以接收点击事件的 UI 组件(如 Image、Text 等)。
  • 这些组件必须满足两个条件:
    • raycastTarget == true
    • 不透明度大于 0(color.a > 0

2. 获取鼠标位置(简化为屏幕坐标)

eventPosition = eventData.position;
  • eventPosition 是鼠标的屏幕坐标(以像素为单位)

3. 执行实际的 Raycast 检测

/// 
/// 在屏幕上进行射线检测,并收集所有在该点下方的 Graphic(UI 元素)。
/// 用于事件系统(如点击、拖拽等)判断哪些 UI 元素被交互到。
/// 
[NonSerialized] 
static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();

private static void Raycast(
    Canvas canvas,                     // 当前要检测的 Canvas
    Camera eventCamera,                // 拍摄这个 Canvas 的摄像机(可以是 UICamera 或世界摄像机)
    Vector2 pointerPosition,           // 鼠标或触控点在屏幕上的坐标
    IList<Graphic> foundGraphics,      // 所有在这个 Canvas 上注册的可交互 Graphic 列表
    List<Graphic> results              // 最终筛选出的、在该点下的 Graphic 结果列表
)
{
    // 获取当前需要检测的 UI 元素总数
    int totalCount = foundGraphics.Count;

    // 遍历所有在这个 Canvas 上注册的 Graphic 元素
    for (int i = 0; i < totalCount; ++i)
    {
        Graphic graphic = foundGraphics[i];

        // 如果:
        // - 不允许射线检测;
        // - 已经被裁剪(未显示);
        // - depth == -1(表示尚未被 Canvas 渲染系统处理过,即还未绘制)
        // 就跳过这个元素
        if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1)
            continue;

        // 检查指针位置是否在该 UI 元素的矩形区域内(考虑 padding)
        if (!RectTransformUtility.RectangleContainsScreenPoint(
                graphic.rectTransform,
                pointerPosition,
                eventCamera,
                graphic.raycastPadding))
            continue;

        // 如果摄像机不为 null,并且该 UI 元素的位置在摄像机远裁剪面之外,则跳过
        if (eventCamera != null &&
            eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
            continue;

        // 进一步使用自定义的 Raycast 方法(例如 Image、Text 等子类可能重写)做更精确的检测
        if (graphic.Raycast(pointerPosition, eventCamera))
        {
            // 符合条件的 UI 元素加入临时结果列表
            s_SortedGraphics.Add(graphic);
        }
    }

    // 对符合条件的 UI 元素按深度(depth)从高到低排序:
    // - 深度越大,越靠上(覆盖在上面),优先响应事件
    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();
}
术语 解释
raycastTarget 控制该 UI 元素是否参与射线检测(是否能接收点击事件)
canvasRenderer.cull 表示该 UI 元素是否被裁剪(不在可视区域,不会渲染)
depth UI 元素在 Canvas 下的层级深度,决定谁在最上层
raycastPadding 可选的额外检测范围扩展,用于提高命中精度
s_SortedGraphics 临时缓存满足条件的 UI 元素,并根据深度排序
RectangleContainsScreenPoint 判断鼠标是否点击在一个 UI 元素上

4. 对结果进行筛选和排序

for (var index = 0; index < totalCount; index++)
{
    var go = m_RaycastResults[index].gameObject;

    // 如果启用了 ignoreReversedGraphics,排除背对摄像机的物体(这里省略)

    // 检查深度(distance),如果比阻挡层近才加入结果
    if (distance >= hitDistance)
        continue;

    // 构造最终的 RaycastResult 并添加进列表
    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
    };
    resultAppendList.Add(castResult);
}

五、 关键逻辑总结

步骤 描述
1. 获取 UI 列表 GraphicRegistry 获取当前 Canvas 上所有可点击的 UI 元素
2. 获取鼠标位置 通过 eventData.position 得到鼠标在屏幕上的坐标
3. 遍历 UI 元素 对每个 UI 元素执行点击检测(矩形区域 + alpha 值)
4. 排序并返回结果 按照深度(depth)、sortingLayer 排序后,返回命中的 UI 元素

六、哪些元素会被点击

只有满足以下条件的 UI 元素才会参与点击检测:

条件 说明
raycastTarget == true 在 Inspector 中勾选了 “Raycast Target”
color.a > 0 透明度不为 0,否则不会响应点击
CanvasRenderer 存在 UI 元素必须有 CanvasRenderer 组件
未被遮挡 如果前面有更靠前的 UI 元素,后面的可能不会被检测到

示例:如何让某个 UI 元素不能被点击

  • 把它的 Image.raycastTarget = false
  • 或者设置 color.a = 0(完全透明)
  • 或者移除 CanvasRenderer 组件(但这样也不会渲染)

七、如何扩展自定义点击行为

可以继承 UI.Graphic 并重写 Raycast() 方法来自定义点击范围(比如圆形、多边形等):

public class CircleGraphic : Image
{
    public override bool Raycast(Vector2 sp, Camera eventCamera)
    {
        // 自定义圆形点击检测
        return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera) 
            && IsInCircle(sp, rectTransform.rect.center, rectTransform.rect.width / 2f);
    }
}

八、 总结:

2D UI 点击机制关键点

内容 说明
点击检测方式 矩形检测(RectTransform 包围盒)
影响因素 raycastTarget, alpha, Canvas.renderMode
点击顺序 按照 depthsortingOrder 排序
事件分发 EventSystem 根据命中对象调用 IPointerDownHandler 等接口

Unity UGUI (Unity’s User Interface) 事件系统是一个复杂但灵活的机制,用于处理用户交互,如点击、拖动等。

UGUI 事件系统工作流程

  1. 输入检测

    • Unity首先从输入设备(鼠标、触摸屏、键盘等)接收输入。
    • 输入模块(例如StandaloneInputModuleTouchInputModule)监听这些输入。
  2. 射线投射(Raycast)

    • 当接收到输入后,事件系统会通过当前激活的GraphicRaycaster组件对UI进行射线投射。
    • 这个过程确定了哪个UI元素位于用户的输入位置(比如鼠标点击的位置)。
  3. 生成指针事件数据

    • 根据射线投射的结果,创建相应的指针事件数据(PointerEventData),包括位置信息、点击状态等。
  4. 事件传播

    • 事件系统根据指针事件数据触发相应的事件。这包括但不限于以下几种类型的事件:IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler, IClickHandler 等。
    • 事件按照一定的顺序在UI层次结构中传播,通常是从根到叶子节点(即从父级到子级)。
  5. 事件处理

    • 如果某个UI元素实现了对应的接口(例如实现了IPointerClickHandler接口以处理点击事件),那么该元素就会执行相应的事件处理逻辑。
    • 开发者可以通过实现这些接口来为UI元素添加自定义的交互行为。
  6. 回调和响应

    • 在事件处理过程中,可能会调用预设的回调函数或者触发其他游戏逻辑。
    • 这些回调可以用来更新UI状态、播放动画、修改数据模型等。
  7. 重复上述过程

    • 随着用户持续与界面互动,上述过程不断重复,以实时响应新的输入。

你可能感兴趣的:(unity,游戏引擎)