Unity事件系统与射线检测:从原理到实战的UI交互深度解析
1. 项目概述为什么Unity事件系统与射线检测是UI交互的基石在Unity里做UI尤其是用UGUI你绕不开两样东西事件系统和射线检测。新手可能会觉得我拖个按钮挂个OnClick监听功能就实现了这有什么难的但当你开始做复杂的UI交互比如一个可拖拽的背包系统、一个带悬停提示的技能图标、或者一个需要精确点击3D模型上UI元素比如血条的AR应用时各种诡异的问题就来了为什么我的按钮有时候点不到为什么拖拽物品会“穿模”为什么射线检测不到我想要的物体这些问题本质上都是对Unity底层交互机制理解不透彻造成的。UnityEngine.EventSystems这个命名空间下的整套事件系统配合上Physics Raycaster或Graphic Raycaster构成了UGUI乃至3D对象交互的神经中枢。它绝不仅仅是几个IPointerClickHandler接口那么简单。深入理解它意味着你能从“碰运气调参数”的开发模式切换到“精准控制交互逻辑”的专家模式。无论是解决棘手的UI Bug还是实现炫酷的交互特效亦或是进行深度的性能优化这套知识都是你的核心弹药。接下来我就结合自己踩过的无数个坑把这套系统的里里外外、从原理到实战给你彻底讲透。2. 核心架构拆解事件系统如何运作Unity的事件系统是一个典型的事件驱动架构它抽象了来自鼠标、触摸屏、游戏手柄甚至XR控制器等各种输入设备的交互并将其转化为统一的事件分发给场景中注册了监听的对象。理解它的工作流程是解决一切交互问题的起点。2.1 核心组件与职责分工整个系统由几个关键组件协同工作缺一不可。你可以把它们想象成一个高效的公司组织架构。1. EventSystem 组件CEO与调度中心这是整个系统的单例核心一个场景通常只有一个激活的EventSystem。它不处理具体的点击逻辑而是负责两件大事输入模块管理它持有一个BaseInputModule列表如StandaloneInputModule用于PCTouchInputModule用于移动端。每一帧EventSystem的Update方法会调用当前激活模块的Process方法来轮询输入状态。射线检测调度它协调Raycaster们进行检测并管理当前被选中的GameObject比如按钮高亮状态。你可以通过EventSystem.current在代码中随时访问它。2. BaseInputModule 及其子类数据采集部门输入模块负责从具体的硬件设备如Input类读取原始输入数据并将其封装成PointerEventData等事件数据对象。StandaloneInputModule处理鼠标和键盘TouchInputModule处理触摸你还可以自定义模块来处理VR手柄等特殊输入。3. BaseRaycaster 及其子类探测雷达部门这是射线检测的核心执行者。它们负责从输入点屏幕坐标向场景中发射射线并检测命中了哪些对象。主要有三类Graphic RaycasterUGUI专属。它只检测带有Canvas组件且Raycast Target为true的UI元素Image,Text,RawImage等。它的检测顺序受Canvas的渲染模式和Sort Order/Order in Layer影响。Physics Raycaster用于3D物理对象。它向场景发射物理射线检测带有Collider的物体。通常挂在Camera上。Physics2D Raycaster用于2D物理对象检测带有Collider2D的物体。4. 事件接口 (如 IPointerClickHandler)业务执行部门这是你编写具体交互逻辑的地方。GameObject或其附加的MonoBehaviour脚本通过实现特定的事件接口如IPointerClickHandler,IDragHandler,IPointerEnterHandler来声明“我能处理某种事件”。当射线检测命中该物体且事件数据传递过来时接口上对应的方法如OnPointerClick就会被调用。关键理解EventSystem是大脑InputModule是感官Raycaster是触手而事件接口是肌肉。一个完整的交互需要这四者通力合作。2.2 一帧内的完整事件流水线让我们跟踪一次鼠标点击看看数据是如何流动的输入采集StandaloneInputModule在Process阶段检测到鼠标左键被按下Input.GetMouseButtonDown(0)。它创建一个PointerEventData对象填充当前鼠标位置、按下的按钮、点击次数等信息。射线检测EventSystem调用RaycastAll方法。这个方法会遍历场景中所有激活的BaseRaycaster比如一个Graphic Raycaster和一个Physics Raycaster。每个Raycaster根据自己的规则发射射线。Graphic Raycaster会遍历Canvas下所有可射线检测的图形元素通过图形网格和Alpha阈值测试来判定命中。所有Raycaster的检测结果一个RaycastResult列表会被汇总、排序。排序规则通常是先按Raycaster的优先级priority属性同优先级下再按深度对UI是渲染顺序对3D物体是距离相机的远近。事件分发系统得到了一个按优先级排序的命中对象列表ListGameObject。它从列表的第一个对象最顶层、最优先的开始尝试分发事件。执行回调系统检查当前目标GameObject是否实现了对应事件阶段的接口。例如对于点击会按顺序尝试调用IPointerDownHandler.OnPointerDown(按下瞬间)如果发生了拖拽则进入IDragHandler流程IPointerUpHandler.OnPointerUp(抬起瞬间)如果按下和抬起发生在同一个对象上且未发生拖拽则调用IPointerClickHandler.OnPointerClick。事件传播如果当前目标对象没有处理该事件即未实现对应接口事件会沿着其父对象链向上“冒泡”直到找到能处理的对象或到达根节点。这对于在父节点统一处理子元素事件非常有用。这个流程解释了为什么有时候下层UI点击不到——因为上层有一个透明的、但Raycast Target为true的Image挡住了射线。3. 深度解析射线检测的机制与陷阱射线检测是事件系统的“眼睛”它决定了“点哪里哪里才有反应”。理解不透这里UI交互的Bug就会像幽灵一样时隐时现。3.1 Graphic RaycasterUI世界的探测器Graphic Raycaster挂在Canvas上它的检测逻辑比想象中复杂。检测范围与层级Canvas Render Mode至关重要Screen Space - Overlay射线直接基于屏幕坐标检测无视相机。所有Overlay模式的Canvas共享同一个“屏幕空间”其检测顺序由Canvas组件的Sort Order决定值越大越靠前越先被检测。Screen Space - Camera和World Space射线从相机穿过屏幕点击位置发射检测与Canvas平面的交点。这里的层级由Canvas的Order in Layer和物体在3D空间中的深度共同决定。一个常见陷阱如果两个World Space UI的Order in Layer相同那么距离相机更近的Z值更小会被优先检测这可能导致意料之外的遮挡。Alpha Hit Test 与 Raycast TargetGraphic组件Image,Text等上的Raycast Target复选框是控制该图形是否参与射线检测的总开关。性能核心提示务必为不需要交互的静态文本、背景图片取消勾选此选项。UGUI的射线检测是遍历和测试每个可检测图形的像素或几何形状大量无用的Raycast Target会严重拖累UI性能尤其是在移动设备上。Image组件有一个Alpha Hit Test Minimum ThresholdAlpha命中测试最小阈值。只有当像素的Alpha值大于这个阈值默认0.1时该像素点才被认为“可点击”。这用于实现不规则形状按钮如一个圆形图标只有中间圆形区域可点。但注意这需要该图片的Read/Write Enabled在导入设置中打开并且会占用额外内存。3.2 Physics Raycaster连接3D与UI的桥梁当你的UI需要和3D场景中的物体交互比如点击一个3D模型弹出信息框或者你的UI本身就是3D物体World Space Canvas时Physics Raycaster就上场了。配置要点通常将其添加到主摄像机上。在它的属性中你可以指定检测哪几个物理层Event Mask这非常有用可以避免UI射线误检测到背景或无关的物体。你可以设置Depth用于在多个Physics Raycaster间排序。混合检测的优先级问题 这是最容易出错的场景。假设你有一个Screen Space - Camera的UI同时场景中有3D物体。你既想点UI按钮又想点3D物体。你需要两个RaycasterGraphic Raycaster在Canvas上和Physics Raycaster在Camera上。问题点击3D物体前方的UI按钮事件却被3D物体处理了。原因EventSystem对所有Raycaster的结果进行全局排序。默认情况下Physics Raycaster的priority可能和Graphic Raycaster相同或更高而3D物体可能因为距离相机更近而被排在UI前面。解决方案方法A推荐调整Raycaster的priority属性。将Graphic Raycaster的优先级设得比Physics Raycaster更高。方法B确保UI Canvas的Order in Layer足够高并且在Screen Space - Camera模式下让UI的Z坐标离相机足够近但要在Clipping Planes内。方法C代码控制在3D物体的事件处理函数中通过EventSystem.current.IsPointerOverGameObject()判断当前指针是否在UI上如果是则忽略本次3D物体的事件。这是防止UI点击穿透的常用技巧。// 在3D物体的点击处理函数中 public void OnMouseDown() { // 或者IPointerClickHandler if (EventSystem.current.IsPointerOverGameObject()) { // 点击在了UI上不处理3D物体的点击 return; } // 处理真正的3D物体点击逻辑 Debug.Log(3D Object Clicked!); }3.3 自定义Raycaster与高级检测有时内置的Raycaster不能满足需求比如你需要检测一个非标准形状的2D精灵不是UI Graphic或者需要基于自定义逻辑如网格导航来确定命中目标。这时就需要继承BaseRaycaster来自定义。自定义Raycaster的核心步骤继承BaseRaycaster抽象类。必须实现Raycast方法在这里你需要根据传入的PointerEventData包含射线起点、方向填充一个RaycastResult列表。实现eventCamera属性返回用于生成射线的相机。设置priority和sortOrderPriority来控制排序。一个简单的2D精灵检测Raycaster示例框架using UnityEngine; using UnityEngine.EventSystems; using System.Collections.Generic; public class SpriteRaycaster : BaseRaycaster { public Camera eventCamera; // 指定一个相机 public ListSpriteRenderer targetSprites new ListSpriteRenderer(); public override Camera eventCamera this.eventCamera; public override void Raycast(PointerEventData eventData, ListRaycastResult resultAppendList) { if (eventCamera null) return; // 将屏幕点击位置转换为从相机出发的射线 Ray ray eventCamera.ScreenPointToRay(eventData.position); foreach (var spriteRenderer in targetSprites) { // 进行射线与精灵边界框的相交测试这里简化了实际需结合精灵纹理 if (spriteRenderer.bounds.IntersectRay(ray, out float distance)) { RaycastResult result new RaycastResult { gameObject spriteRenderer.gameObject, module this, distance distance, worldPosition ray.GetPoint(distance), worldNormal Vector3.zero, // 简化 screenPosition eventData.position, index resultAppendList.Count, sortingLayer spriteRenderer.sortingLayerID, sortingOrder spriteRenderer.sortingOrder }; resultAppendList.Add(result); } } // 系统会自动对resultAppendList中的所有结果进行排序 } }4. 实战构建一个健壮的可拖拽物品系统理论说再多不如动手做一遍。我们来实现一个背包内可拖拽的物品槽系统这里会集中暴露事件系统和射线检测的许多典型问题。4.1 基础结构与接口实现首先我们有两个核心脚本DraggableItem挂在可拖拽的物品图标上和ItemSlot挂在物品槽上。DraggableItem.cs - 处理拖拽逻辑using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; public class DraggableItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { private CanvasGroup canvasGroup; private RectTransform rectTransform; private Canvas rootCanvas; private ItemSlot currentSlot; private Vector2 originalPosition; private Transform originalParent; void Awake() { rectTransform GetComponentRectTransform(); canvasGroup GetComponentCanvasGroup(); if (canvasGroup null) { canvasGroup gameObject.AddComponentCanvasGroup(); } // 找到最顶层的Canvas用于正确转换坐标 rootCanvas GetComponentInParentCanvas().rootCanvas; currentSlot GetComponentInParentItemSlot(); } public void OnBeginDrag(PointerEventData eventData) { originalPosition rectTransform.anchoredPosition; originalParent transform.parent; // 关键步骤1将自己移到Canvas顶层确保拖拽时显示在最前 transform.SetParent(rootCanvas.transform, true); transform.SetAsLastSibling(); // 关键步骤2禁用Raycast Target防止拖拽物本身阻塞对下方Slot的检测 canvasGroup.blocksRaycasts false; canvasGroup.alpha 0.8f; // 半透明显示拖拽中 // 通知原槽位物品被拿起 if (currentSlot ! null) { currentSlot.OnItemTaken(this); currentSlot null; } } public void OnDrag(PointerEventData eventData) { // 关键步骤3使用rootCanvas的scaleFactor正确转换屏幕坐标到UI局部坐标 if (rootCanvas.renderMode RenderMode.ScreenSpaceOverlay) { rectTransform.anchoredPosition eventData.delta / rootCanvas.scaleFactor; } else { // World Space或Screen Space - Camera模式需要使用RectTransformUtility Vector3 globalMousePos; if (RectTransformUtility.ScreenPointToWorldPointInRectangle( rectTransform, eventData.position, eventData.pressEventCamera, out globalMousePos)) { rectTransform.position globalMousePos; } } } public void OnEndDrag(PointerEventData eventData) { canvasGroup.blocksRaycasts true; // 恢复射线检测 canvasGroup.alpha 1.0f; // 关键步骤4检测释放点下方是否有有效的ItemSlot GameObject dropTarget eventData.pointerCurrentRaycast.gameObject; ItemSlot slot (dropTarget ! null) ? dropTarget.GetComponentItemSlot() : null; if (slot ! null slot.CanAcceptItem(this)) { // 成功放入新槽位 AttachToSlot(slot); } else { // 放回原处或原槽位 ReturnToOriginalPosition(); } } private void AttachToSlot(ItemSlot newSlot) { transform.SetParent(newSlot.transform, false); rectTransform.anchoredPosition Vector2.zero; // 居中放置 currentSlot newSlot; currentSlot.OnItemPlaced(this); } private void ReturnToOriginalPosition() { if (originalParent ! null) { transform.SetParent(originalParent, false); } rectTransform.anchoredPosition originalPosition; if (currentSlot ! null) { // 如果之前有槽位通知它物品放回了 currentSlot.OnItemPlaced(this); } } }ItemSlot.cs - 槽位逻辑与放置检测using UnityEngine; using UnityEngine.EventSystems; public class ItemSlot : MonoBehaviour, IDropHandler { public System.ActionDraggableItem onItemPlaced; public System.ActionDraggableItem onItemTaken; private DraggableItem currentItem; public bool CanAcceptItem(DraggableItem item) { // 这里可以添加放置条件判断比如物品类型、槽位是否已满等 return currentItem null; // 示例仅当槽位为空时可接受 } public void OnItemPlaced(DraggableItem item) { currentItem item; onItemPlaced?.Invoke(item); Debug.Log($Item placed into slot: {gameObject.name}); } public void OnItemTaken(DraggableItem item) { if (currentItem item) { currentItem null; } onItemTaken?.Invoke(item); Debug.Log($Item taken from slot: {gameObject.name}); } // 实现IDropHandler当有物体在此释放时被调用 public void OnDrop(PointerEventData eventData) { // 注意这个回调的触发依赖于释放时指针下方的Raycast能检测到这个Slot。 // 而由于拖拽物在OnBeginDrag时设置了blocksRaycastsfalse所以射线可以穿透它到达Slot。 DraggableItem draggedItem eventData.pointerDrag?.GetComponentDraggableItem(); if (draggedItem ! null CanAcceptItem(draggedItem)) { // 这里通常不直接处理附着而是由DraggableItem在OnEndDrag中统一处理逻辑更清晰。 // 但为了演示IDropHandler的用法也可以在这里处理 // draggedItem.AttachToSlot(this); } } }4.2 关键技巧与深度解析这个简单的拖拽系统蕴含了几个至关重要的技巧blocksRaycasts的妙用在OnBeginDrag中设置canvasGroup.blocksRaycasts false是解决拖拽物品“挡住”自身释放目标的核心。拖拽过程中物品图标不再参与射线检测这样EventSystem就能检测到它下方的ItemSlot或其他UI元素。在OnEndDrag中必须记得恢复为true否则物品之后就无法被再次拖拽了。层级管理transform.SetParent(rootCanvas.transform, true)和SetAsLastSibling()确保了拖拽物在视觉上处于所有UI之上避免被其他UI元素遮挡。这是实现“拿起”视觉效果的关键。坐标转换在OnDrag中直接使用eventData.delta本帧与上一帧的位置差并除以rootCanvas.scaleFactor是针对Screen Space - Overlay模式最简洁高效的跟随方式。对于其他渲染模式必须使用RectTransformUtility.ScreenPointToWorldPointInRectangle并传入正确的eventCameraeventData.pressEventCamera通常就是触发拖拽时所用的相机。释放判定逻辑在OnEndDrag中我们通过eventData.pointerCurrentRaycast.gameObject获取释放瞬间指针下方最顶层的物体。这个数据是由EventSystem在本次事件处理中提供的它反映了在物品blocksRaycastsfalse的状态下射线检测到的真正目标。这种模式比在ItemSlot的OnDrop方法中处理更清晰因为所有释放逻辑都集中在拖拽物自身。5. 性能优化与高级调试技巧理解了原理我们还要让系统跑得又快又稳。UGUI事件系统在复杂界面下可能成为性能瓶颈。5.1 性能优化要点减少Raycast Target这是最立竿见影的优化。遍历成百上千个Graphic组件进行射线检测的成本很高。为所有纯装饰性的Image、Text取消勾选Raycast Target。一个检查方法是在编辑器场景视图中选择Canvas在右上角下拉菜单中选择Raycast所有可射线检测的UI会以绿色高亮一目了然。合理划分CanvasUI的合批Batching基于Canvas。频繁改变顶点数据的UI元素如动态变化的血条、滚动列表会导致其所在的整个Canvas重新合批。将动态UI和静态UI分离到不同的Canvas中可以极大减少重绘范围。但注意每个Canvas都有一个Graphic Raycaster过多的Canvas也会增加射线检测的遍历开销。需要权衡。使用GraphicRaycaster的ignoreReversedGraphics勾选此选项后只会检测面向相机的UI图形即法线方向与相机视线方向大致相同的。对于World SpaceUI如果背面不需要交互勾选此项可以节省一半的检测计算。优化Physics Raycaster的Event Mask只勾选需要交互的物理层避免射线与无关的Collider进行昂贵的物理检测。避免在每帧的事件回调中进行昂贵操作例如在IDragHandler.OnDrag中不要执行FindGameObjectsWithTag、GetComponent可缓存或复杂的数学运算。5.2 调试与问题排查当事件不触发或触发异常时可以按以下步骤排查检查基本配置场景中是否有且仅有一个激活的EventSystemCanvas上是否有Graphic Raycaster组件需要交互的UI元素Raycast Target是否勾选如果是3D交互相机上是否有Physics Raycaster且Event Mask设置正确使用EventSystem的调试工具在EventSystem组件的 Inspector 中勾选Selection下的Send Navigation Events和Visualize高版本Unity可以在运行时看到当前选中的对象。更直接的方法是在脚本中打印EventSystem.current.currentSelectedGameObject或EventSystem.current.IsPointerOverGameObject()的结果。可视化射线检测结果void Update() { if (Input.GetMouseButtonDown(0)) { PointerEventData pointerData new PointerEventData(EventSystem.current); pointerData.position Input.mousePosition; ListRaycastResult results new ListRaycastResult(); EventSystem.current.RaycastAll(pointerData, results); Debug.Log($Raycast hit {results.Count} objects:); foreach (var result in results) { Debug.Log($ - {result.gameObject.name} (Depth: {result.depth}, Module: {result.module})); } } }这段代码可以手动模拟一次射线检测并打印出所有命中的对象及其顺序是诊断遮挡问题的利器。检查层级与顺序对于Screen Space - Overlay检查Canvas的Sort Order。对于Screen Space - Camera和World Space检查Canvas的Order in Layer和UI的Z轴位置。检查父物体是否有Canvas Group且Blocks Raycasts被设置为false这会阻止所有子元素的射线检测。注意Canvas Group的继承性Canvas Group的Blocks Raycasts属性会影响其所有子对象。如果你发现一组UI元素突然都无法点击了检查一下它们的公共父节点是否添加或修改了Canvas Group。6. 常见问题与解决方案速查表下表汇总了开发中最常遇到的事件与射线检测问题及其解决方法问题现象可能原因解决方案UI按钮完全无法点击1. 场景中没有EventSystem。2.Canvas上没有Graphic Raycaster。3. 按钮的Image组件Raycast Target未勾选。4. 按钮被一个更大的、Raycast Target为true的透明UI如全屏背景完全覆盖。1. 检查并添加EventSystem。2. 检查并添加Graphic Raycaster。3. 勾选Raycast Target。4. 检查上层UI取消其Raycast Target或调整层级。按钮点击时灵时不灵1. 按钮上有多个重叠的Graphic组件且Raycast Target都开启导致检测冲突。2. 按钮的Alpha Hit Test Minimum Threshold设置过高图像边缘Alpha值低的地方无法点击。1. 确保按钮上只有一个用于检测的Graphic通常是底图子物体上的文本等取消Raycast Target。2. 降低Alpha阈值或确保按钮图片边缘Alpha足够高。拖拽物品时无法放到下方的槽位拖拽物在拖拽过程中blocksRaycasts仍为true挡住了对槽位的检测。在拖拽脚本的OnBeginDrag中设置CanvasGroup.blocksRaycasts false在OnEndDrag中恢复为true。3D物体点击被前方的UI拦截Graphic Raycaster和Physics Raycaster检测结果排序后UI对象排在了3D物体前面。1. 调整Raycaster的priority降低Graphic Raycaster或提高Physics Raycaster的优先级。2. 在3D物体的点击代码开头用EventSystem.current.IsPointerOverGameObject()判断并跳过。World Space UI互相穿透点击多个World Space Canvas的Order in Layer相同系统按Z轴深度排序导致更远的UI可能被更近的遮挡。为需要区分点击优先级的Canvas设置不同的Order in Layer值大的优先。滚动视图(ScrollRect)内的按钮很难点中ScrollRect本身处理了拖拽事件(IBeginDragHandler,IDragHandler)事件可能被它“吃掉”没有传递到子按钮。确保按钮的OnPointerClick等事件处理函数被正确调用。ScrollRect的Movement Type设置为Elastic或Clamped时轻微的拖动可能会被识别为滚动而非点击可尝试调整ScrollRect的Drag Threshold拖拽阈值。在UI上点击却触发了OnMouseDownOnMouseDown是旧的输入系统不与EventSystem集成。当点击落在UI区域外或EventSystem未处理时仍会触发。统一使用EventSystem的事件接口(IPointerXXHandler)。如果必须用OnMouseDown同样用EventSystem.current.IsPointerOverGameObject()进行过滤。移动端触摸感觉不跟手TouchInputModule的Input Actions Per Second较低或UI元素对快速连续触摸响应不佳。1. 适当提高Input Actions Per Second但会增加CPU开销。2. 检查是否有昂贵的操作在事件回调中。3. 对于拖拽确保使用eventData.delta而非每帧重新计算位置。7. 进阶实现一个简单的输入模块与事件拦截为了更彻底地掌控输入我们有时需要定制输入行为。比如实现一个全局的“双击”手势检测或者在某些模式下如打开菜单时禁用所有其他UI交互。创建一个简单的自定义输入模块using UnityEngine; using UnityEngine.EventSystems; // 这个模块在标准模块基础上增加了双击检测 public class CustomStandaloneInputModule : StandaloneInputModule { public float doubleClickTime 0.3f; private float lastClickTime; private GameObject lastClickObject; public override void Process() { base.Process(); // 调用基类处理标准点击、拖拽等 // 在基类处理完后我们检测双击 ProcessDoubleClick(); } private void ProcessDoubleClick() { if (!input.GetMouseButtonDown(0)) return; GameObject currentObject GetCurrentGameObjectUnderPointer(); float currentTime Time.unscaledTime; if (currentObject ! null currentObject lastClickObject (currentTime - lastClickTime) doubleClickTime) { // 触发双击事件 ExecuteEvents.ExecuteHierarchy(currentObject, GetBaseEventData(), ExecuteEvents.pointerDoubleClickHandler); } lastClickTime currentTime; lastClickObject currentObject; } private GameObject GetCurrentGameObjectUnderPointer() { PointerEventData pointerData GetLastPointerEventData(kMouseLeftId); if (pointerData ! null) { return pointerData.pointerCurrentRaycast.gameObject; } return null; } } // 双击事件接口 public interface IPointerDoubleClickHandler : IEventSystemHandler { void OnPointerDoubleClick(PointerEventData eventData); }使用这个模块你需要禁用默认的StandaloneInputModule将CustomStandaloneInputModule添加到EventSystem上。然后任何需要响应双击的UI对象只需实现IPointerDoubleClickHandler接口即可。全局事件拦截 有时我们需要暂时“冻结”UI输入比如播放过场动画时。一个有效的方法是通过控制EventSystem的enabled属性。public class UIInputBlocker : MonoBehaviour { private EventSystem eventSystem; private bool wasEnabled; void Awake() { eventSystem EventSystem.current; } public void BlockInput(bool block) { if (eventSystem ! null) { if (block) { wasEnabled eventSystem.enabled; eventSystem.enabled false; } else { eventSystem.enabled wasEnabled; } } } }更精细的控制可以通过一个全局的CanvasGroup来实现将其Blocks Raycasts设置为false并覆盖在所有UI之上这样可以更灵活地控制部分UI的交互。