当前位置: 首页 > news >正文

Unity UGUI GraphicRaycaster.Raycast详解

一、源码

/// <summary>
/// 对当前 Canvas 上的所有可交互 UI 图形执行射线检测,判断是否被点击或触碰。
/// </summary>
/// <param name="eventData">指针事件的数据(包含鼠标位置、触摸点等)</param>
/// <param name="resultAppendList">用于存储命中的 UI 元素结果列表</param>
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 自身设置的显示器elsedisplayIndex = 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;// 初始化阻挡距离为最大值,表示默认没有阻挡物挡住 UIfloat 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 模式或没有摄像机,距离为 0if (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 检测

/// <summary>
/// 在屏幕上进行射线检测,并收集所有在该点下方的 Graphic(UI 元素)。
/// 用于事件系统(如点击、拖拽等)判断哪些 UI 元素被交互到。
/// </summary>
[NonSerialized] 
static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();private static void Raycast(Canvas canvas,                     // 当前要检测的 CanvasCamera 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 元素是否被裁剪(不在可视区域,不会渲染)
depthUI 元素在 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. 重复上述过程

    • 随着用户持续与界面互动,上述过程不断重复,以实时响应新的输入。
http://www.xdnf.cn/news/987013.html

相关文章:

  • 免费开源的微信开发框架
  • LangSmith 实战指南:大模型链路调试与监控的深度解析
  • Linux 内核 Slab 分配器核心组件详解
  • 【Linux】Linux高级I/O
  • 循环中的break和continue
  • Redis免费客户端工具推荐
  • Altair:用Python玩转声明式可视化(新手友好向)
  • C#委托代码记录
  • 推荐系统入门最佳实践:Slope One 算法详解与完整实现
  • 记录下blog的成长过程
  • 我的世界进阶模组开发教程——制作机械动力附属模组
  • MySQL存储引擎--深度解析
  • Go 语言 JWT 深度集成指南
  • 什么是哈希函数
  • C语言——深入解析字符串函数与其模拟实现
  • const auto 和 auto
  • Bash 脚本中的特殊变量
  • python使用SQLAlchemy 库操作本地的mysql数据库
  • python基本语法元素
  • python-docx 库教程
  • Oracle中10个索引优化
  • 美团NoCode中的Dev Mode 使用指南
  • 在windows中安装或卸载nginx
  • spring boot源码和lib分开打包
  • 遍历 unordered_map
  • GFS 分布式文件系统
  • UE_Event Any Damage和OnTake Any Damage
  • JAVA CAS 详解
  • Docker完整教程 - 从入门到SpringBoot实战
  • JSON5 模块的作用与区别