MyFramework:GlobalTouchSystem 统一点击检测的实现解析

在 Unity 项目里,点击检测看起来是一个很基础的问题。

如果只是普通 UI,直接使用 Unity 自带的 EventSystem 就可以。

但项目复杂以后,输入检测往往不只是"按钮能不能点"这么简单。

比如一个游戏里可能同时存在:

  • 普通 UI

  • 弹窗 UI

  • 多个摄像机

  • 场景物体

  • 角色点击

  • 地图点击

  • 新手引导限制点击

  • UI 点击穿透

  • 只允许指定区域穿透

  • 鼠标和触摸统一处理

这时候如果所有逻辑都分散在 Unity 的 Button、EventTrigger、Collider、PhysicsRaycast、GraphicRaycaster 里,后期会很难统一管理。

所以在 MyFramework 中,我没有完全依赖 Unity 默认 EventSystem,而是做了一个统一输入检测系统:

GlobalTouchSystem

它的目标是:

把 UI、场景物体、多摄像机、点击穿透和触摸状态统一到一个系统里处理。

项目地址:

GitHub - ZHOURUIH/MyFramework: Unity 商用级别开发框架,经过了多年经验沉淀.一个在unity上使用的网络游戏客户端开发框架,为unity所有使用方式提供完善的封装和管理,只需要专注于游戏逻辑的编写 · GitHub


一、为什么需要统一点击检测

在小项目里,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 本身已经有一套自己的封装体系,例如 myUGUIObjectWindowObjectUGUI 等。

很多可点击对象并不希望直接依赖 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 统一判断。

这就是我设计这套系统的核心原因:

输入事件可以来自不同地方,但最终规则必须集中管理。