解释EventSystem的射线投射排序规则
解读
在国内 Unity 面试中,这道题常被用来区分“只会拖 UI”与“真正做过交互底层”的候选人。面试官想听你回答三层含义:
- 射线到底怎么打——EventSystem 依赖 GraphicRaycaster 还是 PhysicsRaycaster?
- 打到的结果如何排序——距离、层级、RenderOrder、模块优先级谁说了算?
- 排序后如何挑中唯一目标——First Raycast All,再 Sort,再 Send Pointer Event 的完整链路。
答出“先相机深度、再距离、再层级”只能拿 60 分;把 ScreenSpaceOverlay > ScreenSpaceCamera > WorldSpace 的相机权重、SortingGroup/OrderInLayer 的插队逻辑、RaycastResult.sortOrderDepth 的源码级字段都说清,才能拿到 90+。
知识点
- RaycastComparer 静态类:EventSystem 内部用 Array.Sort 的自定义 comparer,比较顺序固定为:
- camera.depth 升序(Overlay 相机 depth 固定 −10000,永远最优先)
- sortOrder(Canvas.sortingOrder 或 SortingGroup.sortingOrder)
- orderInLayer(SpriteRenderer/Canvas 内层)
- distance 升序(到射线原点的距离,UI 射线取 z=0 平面距离)
- depth(Graphic.depth,同一 Canvas 内 UI 元素绘制顺序)
- index 升序(RaycastAll 返回数组下标,保证稳定性)
- 模块优先级:GraphicRaycaster 结果始终排在 PhysicsRaycaster 之前,因为 RaycastModule.sortOrderPriority 默认为 0,而 Physics 模块为 −1000。
- 穿透阻断:一旦 GraphicRaycaster 找到 blocker(raycastTarget=true 且位于同一 Canvas),后续 Physics 对象即使更近也会被截断;可通过 RaycastFilterType.BlockingObjects 开关控制。
- 重写规则:实现 IEventSystemHandler 的脚本可监听 OnBeginDrag 等事件,但排序阶段无法干预;若想自定义排序,需继承 BaseRaycaster 并重写 SortRaycastResults 方法,国内项目常用于“3D 场景与 UI 混合点击”需求。
答案
EventSystem 的射线投射排序由 RaycastComparer 完成,规则可背成“五键升序”:
- camera.depth 最小者优先(Overlay 相机永远最小)
- sortingOrder 小者优先
- orderInLayer 小者优先
- distance 小者优先
- graphic.depth 小者优先(同一 Canvas 内)
- index 小者优先(保证同条件稳定)
最终 RaycastAll 列表首元素 被 EventSystem 选为 pointerCurrentRaycast,后续模块不再处理;若需让 3D 物体“挡在 UI 前面”,要么把 UI 放到 ScreenSpaceCamera 并降低相机 depth,要么给 3D 物体挂 PhysicsRaycaster 并关闭 GraphicRaycaster 的 Blocking Mask。
拓展思考
- 国内手游常见“模型穿透 UI”需求:把主 UI 画布设 ScreenSpaceCamera,depth=1,3D 场景相机 depth=0,这样 camera.depth 规则 让 UI 永远压在最上;若想让某个 3D 弹窗模型盖住 UI,只能临时把模型渲染到 RenderTexture 再显示到 UI 层,不能靠改 distance。
- 热更新框架(如 XLua、ILRuntime)下,无法直接继承 BaseRaycaster,可通过 EventSystem.RaycastAll 手动调用后重写排序列表,再调用 ExecuteEvents.ExecuteHierarchy,实现 Lua 侧完全接管点击逻辑,避免版本差异。
- 性能陷阱:ScrollRect 里大量 raycastTarget=true 的文本,会在滑动时每帧触发 RaycastAll;国内项目常规优化是 动态关闭非可视区域元素的 raycastTarget,或把 Canvas 拆成“静态”与“可交互”两层,减少排序长度。