在 Unity 项目里,点击检测看起来是一个很基础的问题。
如果只是普通 UI,直接使用 Unity 自带的 EventSystem 就可以。
但项目复杂以后,输入检测往往不只是"按钮能不能点"这么简单。
比如一个游戏里可能同时存在:
-
普通 UI
-
弹窗 UI
-
多个摄像机
-
场景物体
-
角色点击
-
地图点击
-
新手引导限制点击
-
UI 点击穿透
-
只允许指定区域穿透
-
鼠标和触摸统一处理
这时候如果所有逻辑都分散在 Unity 的 Button、EventTrigger、Collider、PhysicsRaycast、GraphicRaycaster 里,后期会很难统一管理。
所以在 MyFramework 中,我没有完全依赖 Unity 默认 EventSystem,而是做了一个统一输入检测系统:
GlobalTouchSystem。
它的目标是:
把 UI、场景物体、多摄像机、点击穿透和触摸状态统一到一个系统里处理。
项目地址:
一、为什么需要统一点击检测
在小项目里,UI 点击就是 UI 点击,场景点击就是场景点击,两者分开处理没什么问题。
但在实际游戏项目里,经常会出现混合场景。
例如:
玩家点击屏幕某个位置时,可能同时点到了:
-
一个 UI 按钮
-
一个透明遮罩
-
一个角色血条
-
一个场景怪物
-
地图上的某个格子
这时候系统必须判断:
到底谁应该收到这次点击。
如果每个模块都自己判断,就会出现很多问题。
UI 觉得自己点到了。
场景也觉得自己点到了。
新手引导又想限制只能点某个按钮。
弹窗遮罩又可能想阻止下面所有对象响应。
最后点击逻辑会变得非常混乱。
所以我更倾向于让所有点击先进入一个统一系统,再由这个系统判断最终响应对象。
二、GlobalTouchSystem 的整体职责
GlobalTouchSystem 主要负责几件事:
记录当前触摸 / 鼠标状态
↓
收集可点击对象
↓
按照规则进行射线检测
↓
处理点击穿透
↓
判断最终命中的对象
↓
分发点击、按下、抬起、拖拽等事件
它不是单纯替代 Button 的点击事件。
它更像是一个统一入口。
所有需要参与点击检测的对象,都可以通过统一接口加入检测流程。
这样 UI、场景物体、新手引导、点击穿透规则,就能在同一个系统里协调。
三、TouchInfo 记录每个触点状态
输入检测最基础的一层,是触点状态。
在 PC 上可能是鼠标。
在移动端可能是多个手指。
所以 GlobalTouchSystem 中会维护一组 TouchInfo。
每个 TouchInfo 记录一个触点的状态,比如:
-
当前触点 ID
-
当前屏幕坐标
-
上一帧屏幕坐标
-
是否刚按下
-
是否正在按住
-
是否刚抬起
-
当前按下的对象
-
当前悬停的对象
-
是否发生拖拽
这样做的好处是,后续所有点击逻辑都不需要直接关心 Unity 原始输入。
它们只需要读取 GlobalTouchSystem 整理后的触点状态。
也就是说,底层输入来源可以变化,但上层事件分发逻辑是统一的。
四、为什么要自己收集可点击对象
Unity 默认的 UI 点击检测,主要依赖 Canvas 和 GraphicRaycaster。
场景点击则通常依赖 Collider 和 Physics Raycast。
但 MyFramework 的项目里,UI 本身已经有一套自己的封装体系,例如 myUGUIObject、WindowObjectUGUI 等。
很多可点击对象并不希望直接依赖 Unity 原生事件组件。
所以 GlobalTouchSystem 会收集框架内可参与点击检测的对象。
这些对象可以理解为实现了统一鼠标事件接口的对象。
它们告诉系统:
我是否可以被点击
我的检测区域是什么
我当前是否显示
我是否允许穿透
我收到点击后要执行什么逻辑
这样点击系统不需要知道具体对象是按钮、图片、窗口、场景对象,还是自定义控件。
只要它符合统一接口,就可以参与检测。
这也是框架化的关键:
点击系统处理规则,对象自己处理业务。
五、UI 和场景物体统一判断
很多游戏里,UI 和场景是分开的。
但点击时,它们必须被统一判断。
比如玩家点击屏幕时,如果点到了 UI 按钮,就不应该同时点到场景地面。
如果点到的是透明 UI,并且这个 UI 允许穿透,那么下面的场景对象才可能继续响应。
所以 GlobalTouchSystem 需要处理这种关系:
先检测最上层 UI
↓
如果 UI 阻挡点击,就停止
↓
如果 UI 允许穿透,继续向下检测
↓
再检测场景对象
这比让 UI 和场景各自处理点击要稳定得多。
因为最终是否穿透、是否阻挡,都由统一系统决定。
不会出现 UI 响应了,场景也响应了,两个系统互相不知道的情况。
六、点击穿透不是简单的开关
点击穿透听起来很简单:
允许穿透,或者不允许穿透。
但实际项目里经常没这么简单。
有些区域允许穿透到下面。
有些区域只允许穿透给指定对象。
有些情况下,新手引导只允许玩家点击某个按钮,其他地方都应该被屏蔽。
所以 GlobalTouchSystem 里不是只有一个简单的 passRaycast 开关,而是需要维护更细的穿透规则。
例如有一种规则是:
A 区域本身不响应点击
但只允许点击穿透到 B 区域
其他对象不能穿透
这类需求在新手引导、遮罩、高亮区域、复杂弹窗里很常见。
如果只靠 Unity 默认 Raycast Target 开关,很难表达这种关系。
所以 MyFramework 中会维护类似"只允许指定区域穿透"的绑定关系。
这样就能把点击穿透从一个布尔值,变成一套明确的规则。
七、多摄像机下为什么更需要统一系统
在一些项目里,UI 不一定只有一个摄像机。
可能会有:
-
主 UI 摄像机
-
场景摄像机
-
特效摄像机
-
独立渲染层
-
特殊 UI Canvas
如果点击检测分散在不同摄像机、不同 Canvas、不同 Raycaster 里,就很容易出现优先级不一致的问题。
GlobalTouchSystem 的作用,就是把这些检测结果统一收集,然后按照框架自己的规则判断谁优先。
这样点击逻辑不完全依赖 Unity 当前的组件顺序,而是由框架明确控制。
对长期项目来说,这种可控性很重要。
八、为什么不让每个按钮自己处理
最简单的做法,是每个按钮自己注册点击事件。
这个在小项目里当然可以。
但问题是,按钮自己只能知道"我被点了"。
它不知道:
-
当前是否有弹窗遮挡
-
当前是否处于新手引导
-
当前是否允许点击穿透
-
当前是否还有更高层对象
-
当前触点是否已经被其他对象占用
-
当前是点击还是拖拽
-
当前是否应该传给场景对象
这些都不是单个按钮能可靠判断的。
所以 MyFramework 的做法是:
单个对象只描述自己
全局系统决定谁真正收到事件
这能避免事件判断分散在各个业务脚本里。
点击规则越复杂,统一管理的价值越明显。
九、新手引导中的作用
新手引导是最能体现 GlobalTouchSystem 价值的场景之一。
很多游戏的新手引导都会要求:
-
当前只能点某个按钮
-
其他 UI 不能点
-
背景不能点
-
高亮区域可以穿透
-
某些区域必须拦截
-
点击错误区域可能要给提示
如果没有统一点击系统,这些逻辑通常会变成各种临时开关。
这个按钮禁用一下。
那个 CanvasGroup 拦截一下。
某个遮罩打开 Raycast Target。
某个区域再额外加一个 Collider。
最后项目越做越乱。
如果点击统一进入 GlobalTouchSystem,新手引导只需要修改全局点击规则:
当前允许谁响应点击
当前哪些区域可以穿透
当前哪些对象必须屏蔽
这样引导系统不需要到处改 UI 组件本身。
十、GlobalTouchSystem 解决的具体问题
GlobalTouchSystem 解决的不是"按钮怎么响应点击",而是复杂界面和场景混合时,点击规则怎么统一管理。
它主要解决几个问题:
-
触点状态统一记录
鼠标、触摸、按下、抬起、移动、拖拽,都先整理成统一的
TouchInfo,后面的逻辑不再直接到处读取 Unity 原始输入。 -
点击对象统一收集
不管是 UI 控件、窗口对象,还是场景中需要响应点击的对象,都通过统一接口加入检测流程,而不是每个对象各自写一套判断。
-
命中结果统一裁决
一次点击可能同时碰到 UI、遮罩、场景对象。GlobalTouchSystem 负责决定最终是谁响应,而不是让多个系统同时响应。
-
点击穿透规则集中处理
某些 UI 需要阻挡点击,某些区域允许穿透,某些情况下只允许穿透到指定对象。这个规则如果分散写在各个控件里,会很难维护,所以统一放到输入系统里处理。
-
新手引导不用侵入具体 UI
引导阶段经常需要"只能点某个按钮,其他地方都不能点"。有了统一点击系统,就不需要到处修改按钮状态或 CanvasGroup,而是通过全局规则限制当前允许响应的对象。
-
多摄像机、多层级下优先级更可控
UI 摄像机、场景摄像机、特殊层级对象都可能参与点击检测。统一系统可以按框架规则决定优先级,避免依赖各个 Unity 组件自己的执行顺序。
-
减少业务脚本里的临时判断
单个按钮只需要关心自己被点击后做什么,不需要关心当前有没有遮罩、是否处于引导、下面有没有场景对象、是否允许穿透。这些规则都交给 GlobalTouchSystem。
所以这套系统的重点不是替代 Unity 的按钮事件,而是把点击判断从"各个组件自己处理",收敛成"全局输入系统统一裁决"。
项目越复杂,UI、场景、弹窗、遮罩、新手引导之间的点击关系越多,这种统一管理的价值就越明显。
结语
点击检测看起来是一个很小的功能。
但在游戏项目里,它经常会牵扯到 UI、场景、摄像机、引导、遮罩和穿透规则。
如果这些逻辑全部分散处理,后期很容易出现各种互相影响的问题。
MyFramework 中的 GlobalTouchSystem,本质上是把点击检测从"组件级事件"提升到了"框架级输入管理"。
单个对象只需要关心自己能不能被点,以及被点以后做什么。
而谁先响应、谁被遮挡、谁能穿透、谁被新手引导允许点击,则交给 GlobalTouchSystem 统一判断。
这就是我设计这套系统的核心原因:
输入事件可以来自不同地方,但最终规则必须集中管理。