Unity3D事件系统和EventSystem

Unity的事件系统提供了多种使用方式,又和物理碰撞结合在一起,所以同样使用Unity事件处理,就能写出各种各样的风格。很多项目还会自己对事件在进行一次封装,有的还会使用第三方插件。无论是手势插件还是UI插件,都是要建立在事件系统之上的,这些插件都会各自针对事件进行封装。所以,混乱,未知,冲突在所难免。

本文针对Unity2017的版本,对事件系统进行梳理和解读,然后对EventSystem的使用和最佳实践给出一套方案。

Unity事件处理的种类

1. 系统回调OnMouse事件

首当其冲的就是MonoBehavior上的事件回调,可以参看MonoBehaviour文档。这是一系列的OnMouse开头的回调函数。

OnMouseDown
OnMouseDrag
OnMouseEnter
OnMouseExit
OnMouseOver
OnMouseUp
1
2
3
4
5
6
这个处理方式有以下几个特点:

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); 
       } 
    }
}
1
2
3
4
5
6
7
8
9
10
这是官方的例子,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();
}
1
2
3
4
5
6
7
8
9
轮询需要每一帧都去检测判断Input的状态,如果这样的检测散落在代码的各处是非常不好的。难道Unity的本意就是实现一个轮询的插件,在用消息驱动去分发事件 ?于是EventSystem就出现了。

总结
--------------------- 
作者:scottcgi 
来源:CSDN 
原文:https://blog.csdn.net/tom_221x/article/details/78457615 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/liu943367080/article/details/86287771