木匣子

Web/Game/Programming/Life etc.

Unity3d 的 GUI 框架分析

本系列的第三篇,以 Unity3d 为例,看看其如何实现基于事件的 GUI 框架。

本文分析的是 uGUI,而非 Unity3d 的 Editor UI 。后者是一种称为立即模式(Immediate Mode)的 UI 交互方式,Unity3d 的编辑器插件以及非常早期的 Unity UI 使用这种方式开发。

与 Cocos2d 或者 Egret 不同的是,Unity3d 是一个基于组件-实体-系统(Entity-Component-System[1][2])框架的游戏引擎。虽然是商业引擎,但 Unity3d 将 UI 的大部分代码托管于 Bitbucket ,详见这里。里面除了最具价值的 RectTransform 和 Canvas 之外,都开源了。花了一些时间将其中比较核心的类图画出来,被它的复杂度震惊了:

unity3d-cls-1

Unity3d 的 UI 框架非常的复杂,由于是基于 ECS 框架,代码非常零散。此外 Unity3d 允许将 GUI 渲染在屏幕甚至是 3D 表面,并且对实体的碰撞检测(hitTest)甚至可以是基于物理的。这需求远超出我的想像,所以它的抽象性非常高。

场景树

Unity3d 使用 Transform 组件实现场景树。而 UI 部分使用了继承至 Transform 的 RectTransform 。后者非常强大,只通过四个锚点就实现了各种组件的适配。不过 Unity3d 故意将其移到 Unity3d.UI 的包外,没有开源。有兴趣的话,可以在这里阅读其源码。

但是 Unity3d 并没有实现事件流的机制。可能是因为 ECS 框架本身的事件系统比较复杂。不过 Unity3d 的 UI 事件系统很灵活,可以很方便将事件委托给多个实体去处理。

UIBehaviour

整个 uGUI 组件的基类,本质是 ECS 中的 Component ,可以挂载到任何实体上。绑定组件的基本生命周期:Start()/Update()/FixedUpdate() 等。

事件?事件分发器!(UnityEvent)

UnityEvent 是所有 UI 组件事件的基类,虽然叫 Event 但它其实是个 Dispatcher 。它能够存放事件侦听器,并在 UI 响应事件的时候,分发给其绑定的所有事件侦听器。

事件侦听器(IEventSystemHandler/UnityAction)

Unity 的 EventSystem 中有两套事件侦听器,一套是用于 EventSystem 和 uGUI 组件内部使用的 IEventSystemHandler,另一套是用于在 Editor 中供用户使用的 UnityAction :

IEventSystemHandler

例如 Button 组件会实现 IPointerClickHandler 接口(继承至 IEventSystemHandler),这样的话,当用户点击鼠标的时候,EventSystem 就会把相关的事件数据发送到相应实体挂载的 Button(或其它实现该接口的组件)上。

UnityAction

UnityAction 的本质是委托(delegate)。在 Editor 里能够封装其它组件的自定义方法。该部分没有开源,暂时没有深入分析。Unity3d 是一个数据驱动的游戏引擎。可以在 Editor 内设计和持久化 GUI 甚至是 Event ,非常强大。Cocos2d 的 CocosCreator 正在向这个方向学习,但是不知道现在做得怎么样了。

事件系统(EventSystem)

System 是 ECS 框架很重要的部分,一般的 ECS 游戏引擎将其单独管理。但 Unity3d 把它设计成某种 Component ,可以挂载在 Entity 上随时启用。唯一的限制是,任一时刻有且只有一个 EventSystem 是启用的。

它的主要功能是管理 uGUI 的其它子系统。

输入模块(InputModule)

用于获取用户设备的数据,并封装成事件对象(EventData)传递给光线追踪器以及事件分发器。这一层抽象可以方便扩展,鼠标、触摸,或者是未来的 VR 设备。

光线追踪(RaycasterManager)

用来判定相应的物体是否在用户交互的坐标下,功能同 Egret 的 hitTest() 。正如前文所说,Unity3d 允许将 UI 渲染在 3D 表面,甚至是和 3D 物体进行交互。所以 Unity3d 提供了三种光线追踪子类:PhysicsRaycaster/Physics2DRaycaster 以及 GraphicRaycaster 。在 GUI 中比较常用后者。

GraphicRaycaster 会将用户在屏幕空间的鼠标坐标通过当前活动的摄像机的投影矩阵转换成世界坐标的射线,然后查找与射线相交的所有实体,并按深度排序,选择最前面的实体。

其它系统

ExecuteEvents

用于执行特定的事件。能将从输入模块获得的状态数据转递给光线追踪器获得的目标实体中相应的 EventHandlers 。

GraphicRegistry

由于 Unity3d 的游戏场景中会有非常多的元素,绝大部分是跟 UI 交互无关的。所以 Unity3d 提供了 GraphicRegistry 用于统一维护 UI 可视对象的列表。只有部分图形相关的组件会将自身注册到 GraphicRegistry ,从而减少计算量,提升性能。

布局

除了强大的 RectTransform 之外,uGUI 也提供了很强大的布局库,可以很方便的适配各种比例的屏幕。这里不作深入分析。

一个按钮

在 Cocos2d 或者 Egret 里面,按钮是一个结点,能够渲染自己,能够响应触摸,能够分发事件。但是在 Unity3d 里事情就复杂了。想要实现「按钮」,需要两个组件,一个是 Image,用于显示以及响应 RayCast ;另一个是 Button,用于绑定和响应事件。两者没有强关联(其实 Button 可以控制 Image 的一些状态,用于显示禁用、高亮等效果。)。但是如果少了 Image,Button 就不会响应任何事件。通过下面这个时序图,可以看到按钮是如何工作的:

unity3d-sq

小结

初看 uGUI 的时候非常不解,为何要把 UI 框架设计得如此复杂。后来才意识到它是一个非常有魅力的 ECS 框架。跟 Cocos2d 或者 Egret 完全不一样的风格,但绝对更加适合游戏开发。目测之后我会想更深入研究 ECS 框架的游戏引擎。


  1. 这里有一篇关于 ECS 非常详细的实现教程 http://vasir.net/blog/game-development/how-to-build-entity-component-system-in-javascript ↩︎

  2. 以及关于 ECS 的一些概概念 https://shaneenishry.com/blog/2014/12/27/misconceptions-of-component-based-entity-systems/ ↩︎