Unity 的事件系统提供了多种使用方式, 又和物理碰撞结合在一起, 所以同样使用 Unity 事件处理, 就能写出各种各样的风格. 很多项目还会自己对事件在进行一次封装, 有的还会使用第三方插件. 无论是手势插件还是 UI 插件, 都是要建立在事件系统之上的, 这些插件都会各自针对事件进行封装. 所以, 混乱, 未知, 冲突在所难免.
本文针对 Unity2017 的版本, 对事件系统进行梳理和解读, 然后对 EventSystem 的使用和最佳实践给出一套方案.
Unity 事件处理的种类
1. 系统回调 OnMouse 事件
首当其冲的就是 MonoBehavior 上的事件回调, 可以参看 MonoBehaviour 文档. 这是一系列的 OnMouse 开头的回调函数.
- OnMouseDown
- OnMouseDrag
- OnMouseEnter
- OnMouseExit
- OnMouseOver
- OnMouseUp
这个处理方式有以下几个特点:
MonoBehavior 所在的 GameObject 需要有 Collider 碰撞组件, 并且 Physics.queriesHitTriggers 设置为 True, 这个在 Edit -> Physics Settings -> Physics or Physics2D 中设置.
或者 MonoBehavior 所在的 GameObject 存在 GUIElement.
OnMouse 处理函数可以是协程.
GameObject 所有 MonoBehavior 实现 OnMouse 的函数都会调用.
Collider 或 GUIElement 的层级顺序, 会遮挡事件的传递.
按照官方的解释, 这是 GUI 事件的一部分, 参看 EventFunctions. 设计的初衷也是为了 GUI 服务的. 参看 ExecutionOrder 最后的 unity 执行流程图, 会发现 OnMouse 事件是一个独立的 Input Event.
可以看到, OnMouse 事件在, Physics 事件之后, Update 之前, 记住这个顺序, 后面会用到. 并且, 这是引擎本身回调的, 就引擎使用而言可以看成是, 消息驱动. 至于引擎的实现, 可是轮询也可以是消息驱动.
2. 在 Update 中轮询 Input 对象
- public class ExampleClass : MonoBehaviour
- {
- public void Update()
- {
- if (Input.GetButtonDown("Fire1"))
- {
- Debug.Log(Input.mousePosition);
- }
- }
- }
这是官方的例子, Input 拥有各种输入设备的数据信息. 每一帧不断的检测, 查看有没有需要处理的输入信息, 利用 GameObject 本身的层级顺序来控制 Update 的调用顺序, 从而控制了 Input 的处理顺序.
Input 的信息由引擎自己设置的, 明显 Unity 需要实现不同平台的事件处理, 然后对 Input 进行设置. 另外有一个 InputManager 面板用来配置 Input 相关属性的, 在 Edit -> Physics Settings -> Input 中.
由前面的执行流程图可知, OnMouse 事件会在 Update 之前调用, 当然我们也可以在 OnMouse 中使用 Input, 这样就变成了消息驱动, 而不是轮询了. 但这样的缺点是, 事件必须由 touch 或 pointer 碰撞触发, 比如键盘或控制器按钮的事件就没有办法捕获了.
3. EventSystem
最常见的是在 UGUI 中, 用来进行 UI 的事件处理和分发. 但看其命名, 就知道这并不是一个仅仅针对 UI 的事件系统. 参看文档介绍, EventSystem, 可以看到:
The Event System is a way of sending events to objects in the application based on input, be it keyboard, mouse, touch, or custom input. The Event System consists of a few components that work together to send events.
EventSystem 基于 Input, 可以对键盘, 鼠标, 触摸, 以及自定义输入进行处理. EventSystem 本身是一个管理控制器, 核心功能依赖 InputModule 和 Raycaster 模块.
Input Module
用来处理 Input 数据, 管理事件状态, 和发送事件给 GameObject.
这是一个可替换模块, 比如引擎自带了, StandaloneInputModule 和 TouchInputModule, 也可以自定义.
Raycaster
用来捕获哪些 GameObject 需要执行事件处理. 一共有 3 个种类.
Graphic Raycaster 用于 UI 元素就是继承自 Graphic 的对象. 所以 button 这样的 Selectable 对象需要一个 Target Graphic 对象.
Physics 2D Raycaster 用于 2D 物理碰撞元素, 依赖于 Collider2D.
Physics Raycaster 用于 3D 物理碰撞元素, 依赖于 Collider.
通常, canvas 只用了 Graphic Raycaster, 用来处理 UI 的事件. 所以只要是继承 Graphic 对象都会自动获得 EventSystem 事件监听. 但官方文档有这样的说明:
If you have a 2d / 3d Raycaster configured in your scene it is easily possible to have non UI elements receive messages from the Input Module. Simply attach a script that implements one of the event interfaces.
也就是说, 场景如果添加了 2d / 3d Raycaster 的射线检测, 那么 EventSystem 也会检测相应的物理元素.(后面会详细介绍这种混合的使用模式)
SupportedEvents
这是 EventSystem 默认支持的事件处理回调, 当然也可以自定义, 就需要扩展自己的 Input Module 来实现. 这里需要强调几点:
IMoveHandler,ISubmitHandler 这样的回调事件可以接受键盘输入, 可以在 InputManager 面板里配置自定义的值, 不然就会使用默认值.
键盘事件需要 Selectable 对象, 比如 button 就是继承自 Selectable. 所以当 button 被选中的时候, 就会响应键盘事件, 比如回车和上下左右方向键, 还有空格键. 这时候, 在 button 所在 GameObject 绑定一个实现了 ISubmitHandler 或 IMoveHandler 接口的脚本, 也会同时触发.
另外, 如果我想使用 Collider 来触发这个键盘事件, 就需要使用一个 Selectable 对象. Collider 与 Selectable 放在一起, 并且挂载一个实现了实现了 ISubmitHandler 或 IMoveHandler 接口的脚本, 当 Collider 被选中的时候, 就可以触发键盘事件了.
最后, 系统提供了一个 EventTrigger 组件. 这仅仅是针对 SupportedEvents 的可视化封装. 在面板上拖放配置就用 EventTrigger, 用代码绑定就用实现接口的方法. 这就像 UnityEvent 和 C# event 的关系.
MessagingSystem
这是 EventSystem 的消息传递系统, UGUI 就是使用了这个机制来发送事件消息的. 文档写的比较清楚, 我们可以自定义自己消息传递. 值得注意的有两句话:
The new UI system uses a messaging system designed to replace SendMessage. The messaging system is generic and designed for use not just by the UI system but also by general game code.
这个消息系统是用来替换 SendMessage, 实际项目估计也很少会用 SendMessage, 因为效率不高. 另外, 这是一个通用的消息系统, 不仅仅是针对 UI 的, 而是通用的机制.
不过, 我仍然觉得这种搜索 GameObject 查找接口类型调用的方式, 没有 Action 直接订阅调用来的高效.
EventSystem 与 射线检测的冲突问题
如果 EventSystem 仅仅用来处理 UI 事件的时候, 就会与我们自己手动的射线检测产生冲突,
Physics.Raycast(ray, out hit)
, 原因是显而易见的, 因为 PhysicsGraphic 只会过滤 Graphic 对象并且有自己的 Raycast 调用. 我们自己手动的 Raycast 就会穿透过去.
那为什么我们需要自己调用 Raycast 呢 ? 其原因在于, 我们使用了 Collider 碰撞检测, UI 系统并不会处理. 这时候, 我们就需要使用 EventSystem 的 IsPointerOverGameObject()方法来判断, 有没有选中了 UI 元素. 具体的解决方案参看我的上一篇文章.
但现在我们知道 EventSystem 也是可以处理 Physics 元素的, 那么我们就可以放弃手动 Raycast, 转而让 EventSystem 统一处理.
EventSystem 混合处理 Physics
首先, 我们看一个官方文档的说明 Raycasters.
If multiple Raycasters are used then they will all have casting happen against them and the results will be sorted based on distance to the elements.
当多个 Raycaster 被使用的时候, 结果会按照元素之间的距离排序, 然后事件就会按照这个顺序被传递.
第一步
在相机上添加 Physics2DRaycaster, 我这里只需要对 Physics2D 检测, 如果是 3D 就用 Physics3DRaycaster.Physics Raycaster 依赖一个相机, 如果没有会自动添加. 我挂载在相机上, 射线检测就会依赖这个相机.
这里我用在 GameCamera 上面, 当然也可以放在 UICamera 上面, Physics Raycaster 挂载在哪个相机上面, 射线就依赖这个相机的 Culling Mask.
另外需要注意的是, Physics Raycaster 所在的相机层级, 也就是 Depth, 会影响到事件传递的顺序. 比如, UI Camera 层级高于 Game Camera, 就会永远先出发 UI 上的事件. 同样, OnMouse 事件会默认依赖 Main Camera 的层级.
第二步
给需要碰撞检测的 GameObject, 添加 Collider 和 EventSystem 的事件处理回调接口. 注意 GameObject 的 Layer 也要与 Camera 和 Raycaster 一致, 才能正确被检测到.
事件接口实现脚本 (图中的 Test) 需要 Collider, 事件才能正确回调, 并且 GameObject 和相机的距离决定了 Collider 的层级, 也就是事件阻挡关系.
第三步
这样一来, EventSystem 的 SupportEvents 的接口全部被应用到了 Physics 上面. 也就不再需要自己手动去调用射线去检测 Physics 碰撞了. 那么, 还隐含着一个事情就是, EventSystem 的 IsPointerOverGameObject()就无法在判断对 UI 的点击了. 因为现在点击到 Physics 也会让这个函数返回 True.
EventSystem 与 OnMouse 的区别
OnMouse 会先于 EventSystem 触发. 因为 EventSystem 的源码显示, 其在 Update 中去轮询检测处理 Input 的输入. 而 OnMouse 事件先于 Update 调用.
OnMouse 脚本需要在同一个 GameObject 上挂载 Collider 才能检测. EventSystem 的脚本会根据子节点的 Collider 来触发(平行节点不行).
Rigidbody 有个特点, 会把子节点所有的 Collider 统一检测和处理. 也就是说, OnMouse 脚本与 RigidBody 在一起就可以检测所有的子节点 Collider, 而不再需要同级的 Collider. 而 EventSystem 的脚本则不依赖于 Rigidbody, 都可以检测子节点的 Collider.
OnMouse 依赖于 Tag 为 MainCamera 相机的 Culling Mask 来过滤射线. EventSystem 则是依赖挂载 Physics Raycaster 的相机.
另外, 当在有 Collider 的子节点都挂载 OnMouse 或 EventSystem 事件的时候, 只会触发一次事件. 但在同一个 GameObject 上挂载多个脚本, 就会触发多次.
消息轮询 VS 消息驱动
奇怪的是 Unity 好像比较推荐消息轮询的方式, 就是在 Update 里面每一帧去检测 Input 的变化, 来处理事件. 从引擎的实现方式来看, 完全可以采用消息驱动, 来暴露 API. 因为不同的平台肯定都会提供, 事件的回调函数. 平台自身的事件有些是启动线程轮询的, 有些是从底层操作系统拿到的事件回调. 当然, 消息驱动往往回调函数会在独立的线程里, 不在渲染线程就无法调用渲染的 API.
不过 Unity 引擎完全可以提供一组事件的回调, 就像 OnMouse 事件一样. 但 Input 的设计就已经是基于轮询的事件查询机制了. 我们可以看到在 EventSystem 的源码实现里, 也是在 Update 里去轮询 Input Module 的状态.
- protected virtual void Update()
- {
- // ...
- TickModules();
- // ....
- if (!changedModule && m_CurrentInputModule != null)
- m_CurrentInputModule.Process();
- }
轮询需要每一帧都去检测判断 Input 的状态, 如果这样的检测散落在代码的各处是非常不好的. 难道 Unity 的本意就是实现一个轮询的插件, 在用消息驱动去分发事件 ? 于是 EventSystem 就出现了.
总结
EventSystem 的设计和功能, 就能够统一所有的事件处理. 其提供的事件回调接口也很丰富, 基本可以满足各种需求. 基于这些接口手势检测也很容易实现. 也会受益于未来 Unity 的优化和改进.
来源: http://www.92to.com/bangong/2018/04-25/33663789.html