Unity 与 Flutter 的碰撞 - UIWidgets

Unity 是 Unity Technologies 研发的跨平台 2D / 3D 游戏引擎,可用于开发各个平台的单机游戏、手机游戏。Flutter 是 Google 开源的跨平台 UI 框架,可用现代响应式框架来快速构建高性能的应用。当他们结合碰撞,会有什么火花呢?一个使用现代响应式框架快速构建含有 2D/3D 游戏的高性能应用会不会因此诞生?

一、UIWidgets 介绍

1、Unity 为什么需要 UIWidgets

如果想实现复杂的 2D/3D 效果,如游戏、虚拟人物的等,使用 Unity 会比较容易实现。但对于 App 的 UI 部分,现有 Unity 提供的 UGUI 组件,在使用上却不是那么方便,它有以下不足:

  • 需要开发者手动确定每个 UI 的精确位置,需要去适应不同的分辨率和运行平台。
  • 需要开发者管理渲染部分,和接收手势部分。
  • 渲染效率低,Canvas 只要有 UI element 改动了,整个 Canvas 都需要重新绘制。如果想区分静态的和动态变化的,分成俩个 Canvas 管理维护成本太高。

UIWidgets 吸收 Flutter 的优势,提供了一套对应的声明式相应框架,自适应布局、渲染,并对输入信息的采集处理进行封装。开发者无需关心不同平台适配、手势、布局和渲染部分,只需关心构建和触发 UI 更新的 Widget。

2、 UIWidgets 是什么?

UIWidgets 是 Unity China 团队基于 Flutter 演变的 UI 框架,它是 Unity 编辑器的一个插件包,10M 大小。可以用来构建移动 App、游戏、编辑器插件的 UI 界面。

那 UIWidgets 和 Flutter 有什么区别呢?有句话形象比喻:UIWidgets 和 Flutter 精神一样,肉体不一样。

flutter-uiwidgets

  • 在 Framework 层,俩者基本相同,使用的语言不一样。Flutter 用 Dart 构建了一整套 Widget 框架, UIWidgets 把 Widgets 使用 C# 迁移到 Unity 中。
  • 在 Engine 渲染和平台绑定层,俩者相似。Flutter 使用 Skia 引擎进行绘制,而 UIWidgets 使用 Unity 本身的图形渲染引擎进行绘制。

二、Widgets 介绍

UIWidgets 面向开发者的 Widgets 部分,和 Flutter 基本是一致的,可以直接参考 Flutter 开发文档来开发。

1、一切皆 Widget

Flutter 中一切皆 Widget,那么 Widget 到底是什么呢?Widget 是用来描述当前 UI 的配置和状态,当 widget 状态发生变化,就会触发重新构建 UI。Widget 基本结构如下

flutter-widget

  • stateless widget 没有内部状态,一旦创建就不可更改状态,除非重新创建新实例。
  • stateful widget 有自己的内部状态,用户与其交互后可以更新其 widget 显示。

2、声明式/响应式编程

对于 StatefulWidget,怎么用状态来管理和更新 UI 呢?

uiwidgets-statefuleWidget

一个 StatefuleWidget 必定与一个 State 关联,我们在 State 的 build 中构建和返回 Widget。当数据有更新时,只需要调用 setState 就能触发当前 State 的重新构建,数据源的改变就显示在 UI 上了。

3、生命周期

StatelessWidget 生命周期只有 build,每次触发构建就创建新的对象。

StatefulWidget 的生命周期如下:

statefulWidget生命周期

StatefulWidget 通过 createState 创建 State 与当前 Widget 关联。具体的 UI 构建交由 State 的 build 返回。在 State 中:

  • initState:Widget 第一次挂载到 Widget 树时被调用。需要注意的是在 initState 中引用的 Widget 可能会发生变化。
  • didChangeDependencies:第一次会在 initState 后立马调用。之后当依赖的 InheritFromWidget 发生变化时被调用,例如系统语言 Locale 或主题改变时。
  • build:当需要构建时,调用来构建 Widget 树。
  • dispose:当 State 对象从树中被永久移除时调用。

值得注意的是:

  • didUpdateWidget:当 Widget 重新构建时,若 Widget 的 oldWidget.GetType() == newWidget.GetType() && Equals(oldWidget.key, newWidget.key) 为 true,则 State 关联的 Widget 被更新,didUpdateWidget 被调用。
  • setState:当需要更新当前 State 构建的 Widget 树时调用,并触发 State 的 build

4、UIWidgets 渲染

uiwidgets-tree-update

在下一阶段检测时,发现有俩个 Element 改变后,从改变的 Element 出发检测所有影响的 Element 进行 mark dirty。之后在更新时,只需更新 mark dirty 的 Element,而不会更新没有受影响的 Element,最大限度优化 UI 效率。

渲染模式

不是每个 UI 元素都会绘制,而是先找到静态的子树(没有发生改变),提前渲染到一个离屏的 Render Texture 上缓存下来,需要的时候再将缓存的 Render Texture 贴到屏幕上。

这部分不需要开发者管理,全交由 UIWidgets 来实现。当整个UI界面全部静止时,UIWidgets 就只需要在每一帧渲染时调用一次的 Draw call 把离屏的 RT 贴上去。

三、Widgets - Native 通信

在和 Native 的通信方式上,UIWidgets 和 Flutter 有点差异,UIWidgets 对 Unity 原本的通道方式进行了简单的封装,没有 Flutter 那么丰富。

1、Flutter 通信方式

Flutter 通过平台通道(platform channel)将消息发送到 Native。Native 通过监听平台通道接收消息,然后将响应发回 Flutter 端。

flutter-platform-channel

Flutter 提供了三种通信的基础 Channel:

  • MethodChannel:用于传递方法调用
  • EventChannel:用于数据流的通信
  • BasicMessageChannel:用于传递字符串和半结构化的信息

2、Unity 通信方式

Unity 平台提供的通信方式如下:

unity通信方式

  • Unity 调用 iOS 平台,使用 DLLImport 调用 iOS 使用 extern “C” 声明的方法
  • Unity 调用 Android,使用 JNI 的方式,获取 AndroidJavaObject 对象进行静态/非静态的属性/方法调用
  • 各个平台调用 Unity,统一使用了 UnitySendMessage,发送到一个 GameObj 对象的 Method 方法上。

3、UIWidgets 通信方式

UIWidgets 到 Native

UIWidgets 到 Native:直接按 Unity 到 Native 的通信方式,没有封装。

Native 到 UIWidgets

Native 到 UIWidgets:UIWidget 对 Unity 的 UnitySendMessage 进行封装,使用了一个 game object 作为消息中心,Native 携带 channel 等信息发给消息中心,消息中心再根据 channel 进行转发。

a、Native 发送到 UIWidgets

uiwidgets-native-send

b、UIWidgets 监听 channel 处理

uiwidgets-native

四、状态管理

Flutter 的设计思想是数据和视图分离,由数据映射渲染到视图上。所以 Widget 是 immutable 不可变的,动态可变的在 State 中。但随着 State 构建 widget 和 setState 更新数据的逻辑越写越多后,面临以下问题:

  • UI Widget 和业务逻辑揉杂在一起,越写越混乱,很难管理维护。
  • 业务逻辑代码无法复用,业务、UI 无法分层结构分层
  • 不能对业务逻辑部分进行单元测试

接下来介绍俩种比较流行的状态管理方案,一个是 Google 提出的 BLoC,另一个是从 React 演变过来的 Redux。

1、BLoC

BLoC 是 Business Logic Component(业务逻辑组件) 的缩写,目的是将业务逻辑和 UI 分离,主要是通过 Stream 来实现的,而 Stream 是基于事件流驱动,通过监听订阅事件和对事件处理响应。

BLoC

  • BLoC 是业务逻辑处理层,持有 Stream 的管理 StreamController,可以通过 sink 进行 add 向 Stream 管道注入数据
  • Stream 接收到数据后,可以被订阅事件,接收监听,可以是单向订阅流(只接收单个订阅),也可以是广播流(可以随时添加订阅)。
  • StreamBuilder 或 StreamProvider 是其中允许订阅的监听器,他们包裹 UI Widget,当接收到事件时触发 Widget 重新绘制。

基于 BLoC 思想,可以设计 MVVM 的模式开发:

BLOC_MVVM

  • View:Widget 构成的 UI层,View 层不处理业务逻辑,只处理 UI 显示和交互,通过 StreamBuilder 来监听数据变化并完成重绘。
  • ViewModel:被 View 持有,可接收 View 发出的业务指令。同时持有数据流 Stream,通过 StreamControl 进行 sink,注入数据到 Stream 中,UI 层的 StreamBuilder 监听到变化后触发重绘。
  • Model:数据实体类,包括用户数据,网络回包等,供 ViewModel 使用。

在使用过程中,用 ViewModelContainer 包裹 UI Widget 和处理 UI 业务的 BusinessViewModel,Widget 通过 ViweModelContainer.of 寻找最近父节点的 BusinessViewModel。

2、Redux

Redux 整体是一个单向的数据流动,在 Redux 中定义了三个部分:

Redux

  • Action:是把数据从应用传到 Store 的载体,可以定义为一个数据变化的请求行为。
  • Reducer:根据 Action 产生新的状态并发送到 Store 中。
  • Store:管理 State 状态,当 State 变化时,发送到所有订阅者进行更新。

基于 Redux 架构,业务可设计成如下所示:

redux_flutter

  • 在需要发生变化的 StatelessWidget 中返回 StoreConnector,实现 State 和 ViewModel 关联,并将 ViewModel 和 ActionModel 传递到 Widget 中。
  • Widget 使用 ViewModel 的数据构建 UI。在 Widget 需要变化时,通过 ActionModel dispatch action,进行数据请求并返回结果。
  • Reducer 接收到 Action 后,更新对应 State 中的数据流。
  • 变化的 State 通过 Store 发送到监听变化的 StoreConnector,更新 ViewModel 的数据,并触发 Widget 的重新绘制。

五、UIWidgets 展望

UIWidgets 是聚 Unity、Flutter 之大成,吸收 Flutter 设计,结合 Unity 自身的渲染引擎,让 Unity 也能快速构建高性能的 App。

1、优点

  • 使用现代声明式框架构建高性能应用,解放开发者的渲染过程,补齐 Unity 在 App 应用开发上的短板

  • 很容易把应用和游戏效果结合,2D/3D 动画,粒子动画等很难实现的效果,可以随意搭配

2、缺点

  • Flutter 开发的 Widget 转换到 UIWidgets 中时,需要人工转,没有一键转换工具,很容易被 Flutter 吸引过去。
  • Flutter 提供了开发者的生态,开发者可以把好的轮子开源到 Flutter pub 上,使用上可以很容易安装。但 Unity 目前还没有方便的包安装工具。

3、参考

1、UIWidgets 参考

2、Flutter 参考

知道是不会有人点的,但万一被感动了呢?