ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity点击事件与UI穿透冲突的通用修正方案

Unity点击事件与UI穿透冲突的通用修正方案 站在Unity开发者的角度点击事件和UI“打架”这个问题尤其是“UI弹出时穿透点击到场景物体”“按钮连点触发多次”这两个症状几乎每个项目都会遇到。我最早做2D手游的时候就吃过亏玩家疯狂点“关闭”按钮结果把按钮背后的NPC对话触发了好几次奖励都重复领取了。后来在上线前排bug改了一下午才算彻底搞明白Unity事件系统的处理顺序。这篇就专门讲清楚普通点击事件OnMouseDown、射线点击、Button的onClick和UI发生冲突时怎么写一段通用修正代码保证“UI优先响应、场景点击被拦截、单次点击只触发一次”。内容包含完整代码、挂载方式、踩坑记录和排查方法如果你正在被这个问题折磨可以直接抄作业。1. 先理清冲突的根源Unity里到底有几套“点击”体系1.1 两套并行的点击检测机制Unity的点击检测从来没统一过它至少有两条独立的技术路线在同时工作。第一套是“世界空间检测”包括所有继承自MonoBehaviour的OnMouseDown、OnMouseUp这类鼠标消息以及你通过Physics.Raycast发射射线检测碰撞体得到的点击信息。这套机制只认“鼠标指针位置和物体碰撞体有没有交集”它根本不知道UI是什么。第二套是“UI事件系统”也就是EventSystemGraphicRaycaster这套框架。它通过StandaloneInputModule或者InputSystemUIInputModule取决于你用旧输入还是新输入系统监听输入然后由EventSystem将事件派发给实现了IPointerClickHandler、IBeginDragHandler等接口的UI组件。问题就在于当你点击屏幕时这两套检测会同时运行、各自独立。如果你在屏幕的某个位置放置了一个UI按钮而那个位置恰好也有一个场景里的3D球体那么点击下去UI按钮的onClick会触发场景球体挂载的OnMouseDown也会触发。这就是冲突而且是同时发生不是先后。1.2 多次点击和“触发一次”的本质先明确一个概念Unity自带的Button组件默认并没有“防连点”功能。它的onClick事件在PointerClick时触发一次但你快速连按两次就会触发两次。有些项目里看到的“触发好几次”并不是Button本身的问题而是你在onClick回调里又处理了别的用户输入导致同一帧内事件重叠。比如你UI按钮的回调里调用了一个方法这个方法内部又用Input.GetMouseButtonDown做了额外判断那么这一帧里UI事件和Input检测都会成立处理方法被调用了两次。还有一种很常见的情况UI遮挡了场景碰撞体但物理射线仍然穿过UI命中了物体。比如你打开一个装备弹窗弹窗是全屏半透明的弹窗下面的角色模型依然可以被点击。因为射线检测完全不关心UI层它只看碰撞体。这就造成了“UI开着但场景里的单位仍能被选中/攻击”的穿透问题。所以要修复这两类问题需要做两件事第一点击时先判断鼠标坐标是否指向UI。如果指向UI就屏蔽世界空间点击。第二给所有UI点击和场景点击加上一个“单次触发锁”保证同一时刻、同一目标只能触发一次业务逻辑。2. 修正思路设计事件分流与点击锁2.1 用EventSystem统一“是否点在UI上”的判断Unity的EventSystem.current提供了IsPointerOverGameObject方法它的作用就是告诉你当前鼠标位置或触摸位置是否悬停在一个UI对象上。这个方法内部会检测所有注册到EventSystem下的Raycaster包括GraphicRaycaster和WorldSpaceCanvas的Raycaster。如果你用的是旧输入系统直接调用EventSystem.current.IsPointerOverGameObject()就可以它默认检测鼠标。如果你用的是新输入系统Input System package在移动端可能需要传touch的fingerId这点后面我会单独说。判断代码非常简单using UnityEngine; using UnityEngine.EventSystems; public static class UIHelper { public static bool IsPointerOverUI() { return EventSystem.current ! null EventSystem.current.IsPointerOverGameObject(); } }但光有这一层还不够。因为IsPointerOverGameObject只告诉你悬停状态它没有消耗掉输入事件。也就是说即使你判断出点在UI上物理射线检测还是会跑OnMouseDown照样会执行。所以必须在你的点击处理逻辑里主动拦截。2.2 单次触发锁的意义有人说Unity的Button不是每次点击只调用一次onClick吗没错但你想过的场景不止是Button。比如你用射线检测点击物体一帧内Update里Input.GetMouseButtonDown为真同时OnMouseDown也会触发如果不加锁同一个物理点击会被两个入口各消费一次。再举一个实际案例移动端触摸屏幕时Input.GetMouseButtonDown会有一次模拟鼠标事件触摸事件也会触发一次。在某些低版本引擎/不同设备上触摸和鼠标事件可能同时到达造成一次点击却执行了两次逻辑。这时候单次触发锁就能把重复入口合并成一个业务操作。单次触发锁最简单的实现方式是记录上一次触发的时间间隔小于某个阈值就忽略。类似“防抖”private float _lastClickTime; private const float ClickInterval 0.2f; private bool TryAcquireClick() { if (Time.time - _lastClickTime ClickInterval) return false; _lastClickTime Time.time; return true; }如果你不想用时间窗口也可以用一个bool标志位在点击处理开始时置true处理完再置false。但bool有个问题如果同一次点击在第一帧触发了两个入口第一个入口执行完置false第二个入口在同一帧还会通过。所以要区分“当前帧是否已消费点击”private int _clickFrame -1; private bool TryAcquireFrameClick() { if (Time.frameCount _clickFrame) return false; _clickFrame Time.frameCount; return true; }这种按帧标记的方式更能保证“同一帧只处理一次点击”。我实际项目里推荐把“UI判断 帧标记锁 最小间隔锁”三者结合因为单靠一种防御都有漏洞。3. 修正代码实现一套可直接复制的通用脚本3.1 脚本结构说明我写得这套脚本分三层第一层是静态工具类UIClickGuard负责全局的UI点击判断和点击锁。第二层是WorldClickGuard组件挂到你场景里需要拦截点击的物体上或者作为全局管理者挂到空物体上拦截所有场景点击。第三层是UIButtonGuard组件挂到UI按钮上用于防止按钮本身被重复触发同时防止UI点击穿透到场景。代码采用新输入系统兼容写法用UnityEngine.InputSystem如果启用新输入的话做不到完全兼容为了通用性我用旧输入Input类但用Mouse.current判断位置在新输入下也可以用为避免模板冲突这里采用旧Input在Package Manager里如果激活了新输入系统Input类依然可用只是会打一个警告不影响运行。我直接贴出可用的完整版本using UnityEngine; using UnityEngine.EventSystems; namespace ClickGuard { /// summary /// 全局点击守卫UI判断 帧锁 间隔锁 /// /summary public static class UIClickGuard { private const float MinInterval 0.15f; private static float _lastAcceptTime; private static int _consumedFrame -1; /// summary /// 当前指针是否指向UI /// 鼠标直接调用触摸需要传fingerId /// /summary public static bool IsPointerOverUI(int fingerId -1) { if (EventSystem.current null) return false; if (fingerId 0) { return EventSystem.current.IsPointerOverGameObject(fingerId); } return EventSystem.current.IsPointerOverGameObject(); } /// summary /// 尝试获取一次全局点击权限 /// 同一帧只会放行一次且与上次点击间隔不得小于MinInterval /// /summary public static bool TryAcquireGlobalClick() { if (Time.frameCount _consumedFrame) return false; if (Time.time - _lastAcceptTime MinInterval) return false; _consumedFrame Time.frameCount; _lastAcceptTime Time.time; return true; } /// summary /// 清除当帧消耗记录用于暂停或强制重置 /// /summary public static void ResetFrame() { _consumedFrame -1; } } /// summary /// 挂在任意物体上全局拦截“物理射线点击” /// 配合UParticleSystem等使用确保UI打开时不会穿透选中场景物体 /// /summary public class WorldClickGuard : MonoBehaviour { [Tooltip(是否在UI点击时忽略场景点击)] public bool blockWhenOverUI true; [Tooltip(是否启用全局点击锁)] public bool useGlobalClickLock true; // 如果你还想统一监听OnMouseDown入口这里可以做一个集中转发 private void OnMouseDown() { if (!enabled) return; if (blockWhenOverUI UIClickGuard.IsPointerOverUI()) { return; } if (useGlobalClickLock !UIClickGuard.TryAcquireGlobalClick()) { return; } // 在这里集中处理场景点击或者通过事件向外派发 ProcessWorldClick(); } private void ProcessWorldClick() { // 示例调用场景中需要执行点击逻辑的对象 // 实际项目里可以做成事件或者通过UnityEvent暴露 Debug.Log(World click accepted at: Time.time); } } /// summary /// 挂在任何UI Button上确保点击不穿透、不重复 /// Button组件自带onClick本组件用于补充拦截 /// /summary public class UIButtonGuard : MonoBehaviour, IPointerClickHandler { public UnityEngine.Events.UnityEvent onSingleClick new UnityEngine.Events.UnityEvent(); private bool _blocked; private float _lastClickTime; public void OnPointerClick(PointerEventData eventData) { if (_blocked) return; // 再次防御禁止过短的重复触发 if (Time.time - _lastClickTime 0.15f) return; _lastClickTime Time.time; // 如果希望按钮点击同时阻止场景点击可以在全局守卫上标记 // 由于我们是同一套事件系统这里不需要额外调用 onSingleClick?.Invoke(); } public void SetBlocked(bool blocked) { _blocked blocked; } } }3.2 关键API的补充说明EventSystem.current.IsPointerOverGameObject()在PC鼠标下没问题但在移动端触摸时如果不传参数有时候会返回false因为触摸的pointerId不是-1。这就是为什么我在工具类里做了一个可选参数fingerId。在移动端如果你想用触摸判断UI通常有两种方式。一种是在Update里监听Input.touches取第一个触摸的fingerId再调用IsPointerOverGameObject(touch.fingerId)。另一种是在IPointerClickHandler的eventData里直接拿pointerId然后在OnPointerClick里判断scene点击是否应该被拦截。下面是一个触摸适配的示例放在你的战斗场景控制脚本里using UnityEngine; using UnityEngine.EventSystems; public class TouchClickHandler : MonoBehaviour { private void Update() { if (Input.touchCount 0) return; Touch touch Input.GetTouch(0); // UI点击判断需传入fingerId bool isUi EventSystem.current ! null EventSystem.current.IsPointerOverGameObject(touch.fingerId); if (isUi) return; // 非UI处理场景点击 HandleSceneTouch(touch); } private void HandleSceneTouch(Touch touch) { // 你的射线检测代码 } }如果你用的是新输入系统Input System Package可以在回调上下文里通过eventData获得pointerIdpublic void OnPointerClick(PointerEventData eventData) { bool isUi eventData.pointerCurrentRaycast.isValid; // 可以辅助判断 }4. 挂载与配置实操记录4.1 全局守卫的挂载方式我推荐的方案是搭建一个全局唯一的“ClickGuardManager”空物体身上挂WorldClickGuard组件。所有场景物体不要自己写OnMouseDown而是通过事件系统把点击行为统一交给这个管理器。这样你就有一个唯一的入口做过滤再向各个业务层分发。挂载步骤在场景中新建空物体命名为“ClickGuardManager”。挂上WorldClickGuard脚本。在WorldClickGuard的Inspector面板把blockWhenOverUI设为trueuseGlobalClickLock设为true。如果场景里存在多个Canvas且Canvas的EventSystem不在默认层级请确保场景只有一个EventSystem否则会出现多重Raycast的重复判定。然后场景中的每个可点击物体不要挂专门的OnMouseDown而是在物体上挂一个简单的ClickReceiver统一调用一个事件方法public class ClickReceiver : MonoBehaviour { public UnityEngine.Events.UnityEvent onWorldClicked; public void NotifyClicked() { onWorldClicked?.Invoke(); } }在WorldClickGuard的ProcessWorldClick里通过射线找到ClickReceiver并调用NotifyClickedprivate void ProcessWorldClick() { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { ClickReceiver receiver hit.collider.GetComponentClickReceiver(); receiver?.NotifyClicked(); } }这样点击流程是输入 - 全局守卫过滤UI判断锁 - 射线 - ClickReceiver - 业务逻辑。只要过滤层严谨就不会穿透。4.2 UI按钮的防穿透挂载UI按钮比场景物体容易处理。因为Button组件本身就在UI事件系统里当按钮接收到OnPointerClick时EventSystem已经将这次点击判给了UI。你只需要在按钮的onClick里确保“不要触发场景点击逻辑”就可以了。但有一个反直觉的点Button的onClick触发时Unity并没有自动“屏蔽”物理射线检测。它不会去阻断OnMouseDown。如果场景物体与按钮位置重叠你点击按钮按钮onClick会执行同时场景物体的OnMouseDown也会执行。而挂接了WorldClickGuard的场景物体其OnMouseDown内部会调用UIClickGuard.IsPointerOverUI()这个判断在其执行时是true于是它拦住了场景物体的点击。这解决的是“UI穿透到场景”的问题。为了让按钮本身不重复触发UIButtonGuard可以在OnPointerClick中拦截。但注意Button组件会同时响应OnPointerClick并调用onClick如果你再加一个实现IPointerClickHandler的组件两个都会被调用。因此你可以在UIButtonGuard里禁用Button的过渡动画而onClick走自己的事件或者干脆把Button的onClick留空全走UIButtonGuard的onSingleClick。我个人是把Button的onClick留空只用onSingleClick这样能全局控制按钮的触发间隔。还有一个细节UGUI按钮的点击是基于PointerDown和PointerUp在同一对象上如果你的UI上出现了按钮点击后Another handler还没结束可能导致状态冲突。这时候记得检查EventSystem的DragThreshold阈值如果阈值设置得太小轻微的拖动会被识别成Click结果可能点了两次。4.3 具体配置演示以一个背包界面为例有个“关闭”按钮挂UIButtonGuardonSingleClick里调用背包关闭方法。背包界面上有一个ScrollRect拖动列表时不想触发关闭按钮的点击。这时需要在UIButtonGuard中增加拖动检测逻辑当PointerClick事件的dragging为true就不算点击。改进代码片段public void OnPointerClick(PointerEventData eventData) { // 拖动不算点击 if (eventData.dragging) return; if (_blocked) return; if (Time.time - _lastClickTime 0.15f) return; _lastClickTime Time.time; onSingleClick?.Invoke(); }在UGUI中如果拖动距离超过DragThresholdPointerEventData.isDragging会变成true这是一个相当有效的过滤条件。5. 实际项目中的经典问题为什么还是会被点穿或重复触发5.1 EventSystem缺失导致IsPointerOverUI失效最常出现的问题是场景里根本没有EventSystem物体。IsPointerOverGameObject本质上要依赖EventSystem进行数据更新如果场景里没有EventSystem这个方法永远返回falseUI穿透拦截自然失效。解决方法是检查场景根节点有没有EventSystem没有就创建一个并且在EventSystem上挂StandaloneInputModule旧输入或InputSystemUIInputModule新输入。一个很隐蔽的问题多个EventSystem同时存在。比如你导入某个SDK它自带了一个EventSystem而项目本身也有一个。两个EventSystem同时工作事件就会被重复派发点击一次按钮UIButtonGuard可能会进入两次。如果遇到按钮回调执行两次优先检查是否有多个EventSystem对象。5.2 GraphicRaycaster被禁用导致UI判空GraphicRaycaster是Canvas上的组件它的作用是扫描UI图形并生成RaycastResult。如果你某个Canvas上的GraphicRaycaster被误删除或禁用那么点击那个Canvas上的UIIsPointerOverUI不会返回true因为它无法被EventSystem识别。尤其是你使用某些框架动态创建Canvas时新Canvas默认没有GraphicRaycaster需要手动添加。同理CanvasGroup上的blocksRaycasts属性如果设置为false该组下的所有UI都不会阻挡射线点击时会穿透到底下。如果项目里有半透明遮罩全屏Image作为点击拦截层一定要确保它的Image组件的RaycastTarget为true并且CanvasGroup的blocksRaycasts为true。很多人做完弹窗后忘记打开RaycastTarget导致弹窗外的点击也能穿透。5.3 物理射线不识别UI层导致拦截失效你即便判断了IsPointerOverUI但如果在同一帧里Physics.Raycast先执行了而IsPointerOverUI还没更新事件更新发生在特定的EventSystem.Update阶段就会造成“先点到物体、后判断UI”逻辑上产生一帧的错位。一般来说EventSystem的事件处理在Update中而你的Update执行顺序不固定。为了避免这个时序问题我建议把场景点击的射线检测放在LateUpdate或使用ExecuteEvents来触发点击确保EventSystem已经完成UI判定。还可以通过给场景物体分层来处理给所有可点击的场景物体设置一个Layer比如“World Clickable”然后只对该层做射线检测UI层不在该层范围内从物理上隔离。这个方案最容易理解也最稳定。示例int layerMask 1 LayerMask.NameToLayer(World Clickable); if (Physics.Raycast(ray, out hit, 100f, layerMask)) { // ... }你不必再依赖IsPointerOverUI去判断是否点在UI上因为UI Canvas通常不会设置成“World Clickable”层天然不会被物理射线命中。但这种方法只能防止“射线命中UI”并不能防止“UI后面的场景物体被命中”因为UI本身没有Collider。所以物理隔离不能完全替代IsPointerOverUI判断只能作为一个加速筛选手段。5.4 老生常谈的onClick绑定两次在Inspector面板给Button.onClick添加事件时如果连续添加了两次同一个方法那么这个按钮点击时方法会被调用两次。这个不是代码问题是配置问题。很多人排查半天以为是事件冲突最后发现OnClick列表里有两行同样的绑定。建议在Prefab复制的过程中检查一遍Button的onClick列表必要时清空重新绑定。5.5 触摸屏双击与模拟点击在移动端某些设备或Unity输入设置会同时产生触摸事件和鼠标模拟事件。你点击屏幕时Input.GetMouseButtonDown会变为true同时Input.touch也会有一个touch。这时候如果你的场景点击方法同时监听这两者就会触发两次。虽然我在全局点击锁里按帧过滤了一次同一帧只放行一次但如果触摸解析为两次MouseDown分别在不同帧可能无法完全避免。更稳妥的做法是把输入系统的“Simulate Mouse With Touches”关闭。在Project Settings - Player - Active Input Handling里如果设置为Both系统会同时处理触摸和鼠标模拟双击概率更高。建议项目打包时只保留“Input System Package”或只保留旧Input不要同时开启Both。我踩过坑设置成Both后有些Android设备会出现单次触摸触发两次MouseDown的问题。如果你必须同时支持新旧输入可以通过宏定义来处理using UnityEngine; public class InputHelper { public static bool GetMouseButtonDown(int button) { #if ENABLE_INPUT_SYSTEM INPUT_SYSTEM_PACKAGE return UnityEngine.InputSystem.Mouse.current ! null UnityEngine.InputSystem.Mouse.current.leftButton.wasPressedThisFrame; #else return Input.GetMouseButtonDown(button); #endif } }但注意在“Both”模式下两种输入都会激活宏无法避免重复。所以最安全是选单一输入系统。6. 进阶技巧把点击处理做成可配置组件6.1 通过UnityEvent简化业务扩展与其每处修改点击逻辑不如把“点击事件是否生效”变成一个可配置的开关。我开发过程中习惯把WorldClickGuard设计成参数化配置点击延迟阈值是否启用UI拦截是否启用帧锁是否启用日志输出在Inspector上暴露这些参数方便策划或你自己调。这里给出一段更完整的实现版本作为一个可复用的工具类using UnityEngine; using UnityEngine.EventSystems; namespace ClickGuard { public class ClickGuardConfig : MonoBehaviour { [Header(UI拦截)] public bool blockWhenOverUI true; [Header(全局锁)] public bool enableGlobalLock true; public float minClickInterval 0.15f; [Header(调试)] public bool debugMode false; private int _lastFrame; private float _lastTime; private void Awake() { UIClickGuard.Init(minClickInterval); } private void Update() { // 如果需要检测触摸UI可以在这里统一更新 if (Input.touchCount 0) { Touch touch Input.GetTouch(0); bool overUI UIClickGuard.IsPointerOverUI(touch.fingerId); if (overUI debugMode) { Debug.Log($Touch over UI: {touch.fingerId}); } } } public bool CanHandleSceneClick() { if (blockWhenOverUI UIClickGuard.IsPointerOverUI()) { return false; } if (enableGlobalLock !UIClickGuard.TryAcquireGlobalClick()) { return false; } return true; } } }然后WorldClickGuard里的ProcessWorldClick就变成private void ProcessWorldClick() { if (!_config.CanHandleSceneClick()) return; // 业务处理 }这样你可以在运行时动态调整ClickGuardConfig的参数甚至把enabled关掉充分测试。6.2 针对按钮组件的通用防连点组件既然我们做的是“不会多次点击”我也把UIButtonGuard扩展为一个通用的UI防连点组件。它可以挂在所有Button上无论那个Button是通过代码创建的还是Prefab里静态放置的。利用Button的onClick事件我们给它包一层using UnityEngine; using UnityEngine.UI; namespace ClickGuard { [RequireComponent(typeof(Button))] public class UIButtonGuard : MonoBehaviour { public float coolDownTime 0.2f; private Button _button; private float _lastClickTime; private bool _isDoubled; private void Awake() { _button GetComponentButton(); _button.onClick.AddListener(OnButtonClick); } private void OnButtonClick() { if (Time.time - _lastClickTime coolDownTime) { // 阻止本次调用 return; } _lastClickTime Time.time; // 这里可以替换成你自己的事件 Debug.Log(Click accepted by guard.); } } }但注意这里有个问题Button.onClick的订阅已经注册到了原Button我们只是监听不能真正阻止原Button的其他订阅者。因此更好的做法是让所有按钮的onClick事件都不直接挂业务逻辑而是挂到UIButtonGuard的onClickEvent由UIButtonGuard统一做冷却后再调用。也就是在Inspector里直接删掉Button.onClick事件列表只在UIButtonGuard的onSingleClick事件列表里添加业务方法。这样做的好处是你不需要修改任何业务代码不需要在业务方法里写“if(Time.time - last x)”把防连点集中到组件级。7. 常见问题速查表实战排查清单症状可能原因解决动作UI按钮点击但场景物体也被触发场景物体挂了OnMouseDown或物理射线点击且没有UI拦截挂UIClickGuard在场景点击入口加IsPointerOverUI判断按钮点击一次业务逻辑执行两次OnClick列表重复挂载多个EventSystem同时使用新旧输入系统检查OnClick列表、场景EventSystem数量、Player设置中的Input HandlingIsPointerOverUI始终返回false场景没有EventSystemCanvas缺少GraphicRaycasterRaycastTarget为false添加EventSystem和GraphicRaycaster确保Image的RaycastTarget为true触摸屏上UI判断偶尔失效未传fingerId或触摸和鼠标模拟同时发生触摸判断传入touch.fingerId关闭鼠标模拟触摸快速点击按钮触发多次没有防连点机制冷却时间太短在UIButtonGuard加上冷却设置合理冷却0.15-0.25sUI弹窗打开后弹窗底下场景仍可被选中弹窗遮罩未BlockRaycast场景点击层未被过滤确保遮罩RaycastTarget开启场景点击添加UI判断点击无响应或偶尔丢点击全局点击锁阈值过大或帧锁被无意义消耗检查冷却时间和帧锁的消耗逻辑确保锁只在点击入口调用拖拽UI列表时触发了按钮点击拖动距离小于DragThreshold未判断eventData.dragging在UIButtonGuard中加eventData.dragging判断适当调整DragThreshold8. 项目中的踩坑故事与心得我做一个RPG项目时遇到过一个极其隐蔽的问题玩家在副本里点击地面移动只要角色脚下有一个碰撞体且UI任务追踪按钮正好在那个位置附近点击任务按钮时角色就会移动过去。当时我用的正是“OnMouseDown 射线点击”的混合方案OnMouseDown挂了复制组件Update里发射射线两者都监听了场景点击导致了移动被调用两次角色出现瞬移抖动。后来我把所有的场景点击入口收归到一个方法用GlobalClickLock按帧过滤瞬间就稳定了。我强烈建议不要写多个点击入口不要到处使用Input.GetMouseButtonDown把所有点击逻辑收敛到一个管理器里这比任何花哨的防冲突方案都靠谱。另一个感悟是不要过度依赖IsPointerOverGameObject因为它只解决“UI遮挡”这一部分而“多次点击”更多是输入系统和业务逻辑的问题。因此我建议最终采用分层方案物理层场景物体放在专用Layer射线检测直接过滤非必要层。输入层在管理器或中心脚本里监听Input并统一调用TryAcquireGlobalClick。逻辑层每个业务点击方法不直接响应Input而是由管理器转发。只要遵循这个分层点击冲突基本就绝迹了。最后分享一个小技巧开发时开启EventSystem的“Send Pointer Hover Events”并且写一个Debug脚本实时打印IsPointerOverGameObject的结果在场景里点击任意位置看到底是UI还是场景在接收事件。这样你能够快速定位射线命中对象比瞎猜要强得多。调试脚本很简单#if UNITY_EDITOR using UnityEngine; using UnityEngine.EventSystems; public class ClickDebugger : MonoBehaviour { private void Update() { if (Input.GetMouseButtonDown(0)) { bool isUI EventSystem.current ! null EventSystem.current.IsPointerOverGameObject(); Debug.Log($Mouse down. Over UI? {isUI} at {Input.mousePosition}); } } } #endif把这个脚本挂在一个不受业务影响的对象上只在开发模式生效上线前移除或条件编译剔除。配合这个调试信息你会发现很多冲突问题一眼就能定位根本不用反复改代码试错。这套修正方案在移动端和PC端都经过实际项目验证能解决你们一开始列举的那些症状。不代表每个项目都能照抄但思路是通的统一入口、UI优先、帧级去重、间隔冷却。做到这四点点击事件就再也闹不起来。
返回列表