Unity DOTS 中的 ECS
因为本身就是忠实的 Overwatch 玩家,所以天然的对其应用的 ECS 架构有所兴趣。再加上最近在 Unity Connect 上看见一篇使用 Unity DOTS 实现的一个爆炸 Demo,所以就决定了这个分享的内容。
一、What 什么是 DOTS
DOTS(Data-Oriented Technology Stack 面向数据技术栈)是 Unity 提出的一个高性能多线程式数据导向的技术堆栈,能够充分利用多线程优势。
目前包含以下几个包。(除了 Job System 和 Burst,后面几个都是预览版本,API 一直在变化)
- c# Job System:一个能够安全快速利用多核处理器的 System
- Burst:一个新的基于 LLVM 的后端编译器,能够生成高度优化后的机器码
- Entites:ECS架构的核心库,使用面向数据的方式能够更容易的提供 Job System 和 Burst 所需要的良好数据结构
- Unity Physics:基于 DOTS 构建的新的物理系统当前依旧是很早期状态
- Unity NetCode:基于 ECS 构建的带有客户端预测的服务器模型,可以用于创建多人游戏
- DSPGraph:新的混音系统,c#编写,使用了 Job System,能够使用 Burst 编译,
- Unity Animation:DOTS的动画系统,当前并不能用于商业生成
二、Why 为什么提出新的 ECS 架构
使用 OOP/传统模式开发会存在的问题:
- 开发后期会出现大量的类,理解子类需要掌握父类
- 数据和其处理过程耦合在一起
- 高度依赖引用类型
虽然可以通过 Interface 来进行解耦,但是在开发中依旧需要提倡「高内聚,低耦合」,因此 Unity 和 UE4 都提出了组件的概念来解耦。一个GameObject中包含了多个组件,但是目前的组件还依旧存在有功能/行为,并不是纯粹的数据描述。
比如有个 GameObject 存在有 Location 和 Movement 组件,那么这个 GameObject 应该就可以进行移动了,那么移动这个行为/算法是放在那的?如果放在 Movement 里,那么 Movement 就与 Location 产生了关联,打破了组件之间的封闭性,并不是高内聚的,因此 ECS 提出了个组件之间的切片—— System。
ECS 将所有的行为/算法放在了不同的 System 中,而 Component 只存在数据。
因此使用 ECS 架构,并依照正确的 DO 方法论实现的游戏,获得了以下几个好处
- 对cache友好。由原来的处置管理每个对象的状态,变为相同类型数据横向聚集管理。
- 通过数据和行为分离,更加专注于正在解决的实际问题,也容易进行横向开发出更多的系统
- 由于数据被单独隔离,易于做多线程并行,为 Job System 和 Burst 提供了更好的数据结构
- 代码更容易上手,通过系统掌握行为,根据输入输出了解系统关注的数据源
当然 ECS 也不是真的就完爆原有的OO GameObject,只是 ECS 对于数据密集运算有着良好的支持。ECS 目前也有着大量的问题,这部分将会留在文章后面叙述。
三、How
3.1 ECS 是怎么实现的
C:Component 组件
与原有的挂在GameObject上的组件不同,ECS的组件就是一堆数据的集合,不存在方法,只是用来存储数据/状态。
public struct CloneCube : IComponentData
{
public int Index;
public float3 Postion;
public float3 Offset;
}
E:Entity 实例
实体只是一个概念上的定义,指的就是游戏世界里的一个物体,是一系列组件的集合。为了区分不同组件的集合,在代码层面只是使用一个 32位 的整数 ID 表示,其实并没有真正的一个 Object,存在的意义在于生命周期管理。
为了方便实例的查询,提出了一个 Archetype(原型) 的概念,能够存储记录组件组合的信息,两个实例如果有相同的组件,那么组件的信息都会被存储在一个相同的 Archetype 底下。
当组件发生增加或者删除的情况,会把当前的块移动到新的原型里,并交换原有原型里最后的实体补齐空缺。
S:System 系统
用来处理游戏逻辑的部分,我们可以在 System 中通过 Component 快速筛选出我们需要关系的 Entity 集合。 System 中不存在数据,只有行为,数据的输入和输出都依赖 Component。
System 其实还存在一个问题,就是 System 为了强调解耦,是不能直接相互调用的,因此对于共享代码需要抽离到单独的 Util 中。对于不同的 System 想访问唯一的 Component,可以在 World 创建唯一的Entity。比如一个叫玩家键盘的 Entity,由键盘组件组成。只需要安排一个System 不断更新这个 Component,就能使其他需要获得玩家键盘输入的 System 得到相同的数据。
目前 Unity 的 DOSTSample 项目中就是这么做的,World 中有一个唯一的 LocalPlay 实例,有一个 UserCommand 组件,被 BeforeClientPredictionSystem 不断 Update 更新本地用户输入。
3.2 ECS 工作流
如果真的将目前的游戏都改成 Data-Oriented 并不容易,目前直接放弃所见即所得的编辑器,显然不是当前最佳的方式(可以按照 DOD 重新编写编辑器,只是目前还不能)。Unity 团队提供了一种 ECS Conversion Workflow 。可以让开发者通过常规 GameObject 来实现编辑功能,在需要 ECS 高性能的部分使用 Conversion Workflow,将 GameObject 在 Runtime 的时候转成纯粹的 ECS data。
四、Now
通过写相关 DEMO 和粗览 Unity DOTS Sample 源码,可以明显感觉到,为了符合 ECS 的设计理念,需要仔细考虑设计 Component 中的数据,必须其进行合理的设计和拆分。真正使用起来还是相当费劲的。
ECS 对于UI、复杂技能特性支持度也不好。由于没有办法利用起多态,不能将不同数据存放在一起,所以只能靠多增加 Component 来实现。
目前 Unity DOTS 中的包还基本都处于预览版本,还有相当多的地方都不是很完善,仅仅是只能做些小的 Demo。