Unity3D游戏开发程序员Unity技术分享

「Unity3D」(3)事件系统和EventSystem详细解读

2017-11-06  本文已影响325人  飞机王

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

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

Unity事件处理的种类

1. 系统回调OnMouse事件

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

OnMouseDown
OnMouseDrag
OnMouseEnter
OnMouseExit
OnMouseOver
OnMouseUp

这个处理方式有以下几个特点:

按照官方的解释,这是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个种类。

通常,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来实现。这里需要强调几点:

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的区别

另外,当在有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的优化和改进。


「用起来」

上一篇 下一篇

猜你喜欢

热点阅读