木匣子

Web/Game/Programming/Life etc.

Egret 的 GUI 框架分析

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

Egret 是另一款基于 html5 的国产开源游戏引擎。其框架结构大量参考 Flash ,并且使用 TypeScript 这种强类型脚本语言,特别适合早期的 ActionScript 3.0 游戏开发者迁移到 Egret 的开发环境。

虽然我并没有用它做过比较大的项目,但是对它的实现也是保持了一点的好奇心。同第一篇一样,我将 egret 框架中与 GUI 有关的类绘制出来,方便理解:

egret-cls-1

通过与上一篇的 Cocos2d-html5 对比,我们可以看到以下概念在 Egret 里有何区别。

场景树(Composite Pattern)

Cocos2d 使用的简单的树结构来实现场景树,它的基本 结点 CCNode 本身同时也是容器,可以随意放入子结点。而 Egret 使用了组合模式(Composite_pattern)。它的 DisplayObject 作为一般显示元素,其子类 DisplayObjectContainer 作为容器,职责更加明确。基本形状(Shape)和图元(Graphic)作为叶子结点不能当作容器使用。但是游戏中最常用的 Sprite 结点显然是一个容器,精灵之间常常通过相互嵌套来实现复杂的动画效果(其实 Sprite 就是对 Graphic 的简单封装)。

作为一个游戏引擎,可以说 Egret 在这里照搬 Flash 有点繁琐了,但是单一职责的设计会让类会更加好理解。从文后的事件分发机制可以看到 DisplayObject 和 DisplayObjectContainer 的职责是很不一样的。

事件流(Event Flow)

Erget 实现了第一篇文章中提到了完整的事件流的三个阶段:捕获、目标和冒泡阶段。详见事件分发器部分。

事件(Event)

事件对象与 Cocos2d 没有多少区别,记录了事件的名字以及事件发生时的一些状态。

事件箱(Event Bin)

换了个名字,但是还是换汤不换药。其实就是事件侦听器,记录了事件的类型,回调函数,优先级等数据。

事件分发器(Event Dispatcher)

Egret 与 Cocos2d 最大的不同就是,它没有中央事件管理器(Event Manager)。它的基本显示对象(DisplaObject)继承至事件分发器(Event Dispatcher)。这表明每一个显示对象都能维护自己的事件列表,能够直接订阅其它对象的事件。

这是我比较喜欢的一点。如果要在 Cocos2d 中使得两个 CCNodes 可以相互传递事件,不得不通过 CCEventManager 或者自己加入 EventEmitter 之类的第三方事件库。

更有意思的是,DisplayObject 直接重写了事件分发方法,通过自身的场景树结构实现了事件的捕获与冒泡。DisplayObject.getPropagationList() 方法向上遍历到根节点为作冒泡列表,并复制一份反转作为捕获列表。

此外 DisplayObject 实现了 hitTest() 方法用来获得指定位置处的显示元素。DisplayObjectContainer 重写的该方法,来获得指定位置处的子元素。

这样一来,就可以在舞台根结点(Stage)通过 target = DisplayObjectContainer.hitTest() 获得鼠标位置下的显示元素,然后直接把事件通过该元素派发出去 target.DispatchEvent(touchEvent)。这样事件就可以把事件流的三个阶段按需过一遍。

这一实现比起 Cocos2d 的中央事件管理器更加明智。而且当场景上的元素变化的时候,不需要另外同步事件侦听器与场景树的优先级。

事件源(Event Source)

Egret 提供了 TouchHandler 类,封装了向场景派发触摸事件的方法。并分离出设备获得指针坐标的功能,根据不同平台实现去实现。在 html5 平台上,使用 WebTouchHandler 类从 canvas 添加事件侦听器订阅 mousedown/mousemove/mouseup 等事件,并转发给 TouchHandler 。

GUI 组件

UIComponent 类继承了 DisplayObjectContainer,并设置自身的 $touchEnabled = true 使得 UI 允许接收触摸事件。此外了一些与布局尺寸相关的代码,并没有比其它 DisplayObject 差多少。因为与事件相关的功能都在其它类实现了,所以 Egret 的 UICompenent 基类不像 Cocos2d 那么臃肿。

Button

在 Cocos2d 中,由于使用的是中央事件管理器,所以按钮在初始化的时候就会把一套触摸相关事件侦听力(TouchBegan/TouchMoved/TouchEnded)注册到 EventManager 上。这导致 EventManager 在派发事件的时候,要检查 Touch 的生命周期。

而 Egret 每个结点可以管理自己需要的事件,所以一开始只需要侦听 TouchBegin 事件,然后收到该事件后,再注册 TouchMove 和 TouchEnd 。完事后再删除这些事件。更加灵活动态。

可以在这里体验 Egret GUI。

布局(Layout)

Cocos2d 将布局功能以 UIWidget 的子类的方式实现,这些子类会根据需要调整子结点的位置和尺寸。这样的设计偶合度和扩展性非常低。

而 Egret 的布局框架来自 Flex 的 spark 组件包。可以从类图看到,Group 类继承至 DisplayObjectContainer,并依赖 LayoutBase 类。Group 中可以放任意的 DisplayObject,并使用 LayoutBase 的接口为它的内容进行布局。不同的布局算法可以继承 LayoutBase 并重写其中的方法实现。这个设计非常灵活,以至于可以封装第三方的布局库,例如 Facebook 发布的 Yoga 框架,将 Flexbox 模型应用到 UI 布局中。

小结

虽然 Egret 的实现跟 Cocos2d 差异很大,但是可以看到,基于事件的 GUI 框架永远离不开事件分发机制、事件流以及场景树这几个概念。至于框架,我更喜欢 Egret ,哦不,我更喜欢 Flash/Flex 的设计。不过还是要感谢 Egret 的实现,将这个设计展示出来(毕竟 Flash 还没有开源),hah。