Skip to content

门泊吴船亦已谋

Unity ECS 框架摸索 第一章 ECS 简介

之前接触 ECS 是因为看了守望先锋的架构分析。了解了一下发现这种架构天生支持多线程,而且对缓存非常友好,似乎做联网同步也很方便,感觉这应该是个挺有前途的设计思路。

这几天翻了一下 Unity 官网,看到 ECS 有文档了,例子也更新了许多,突然兴奋,准备写个 Demo 试试。顺便想记录看看 Unity 的正式版 api 能改成个什么鬼样子(大雾)

本系列基于 Entities 0.5.1-preview.11,并非正式版。
因此更倾向于科普而不是实用(学了也没用早晚要改 API),另外需要对旧版 Unity 有少许了解。

ECS 架构

ECS,是 Entity-Component-System 的缩写,该架构也就只有这三个东西组成(严格意义上)。这个架构由数据驱动逻辑,有别于我们熟知的 OOP(面向对象)

名词解释

Entity,实体。可以近似理解为 OOP 中的对象。Entity 由 Component 组成,此外不含有其他任何成分。可以理解为一个 Entity 编号为 e1,他身上所有组件都被标记为 e1,并且具体实现中 Entity 通常也只是一个用于标记的 id。

Component,组件。存储数据,例如 Position 组件中会有 x, y, z 表示一个实体的位置。

System,系统。不含数据,只处理逻辑,固定帧率调用。且它会筛选拥有指定若干 Component 的 Entity 并对指定的 Component 进行读写。

举个例子描述一下 ECS 的运作

从前有一只鸭子 Entity 叫「老王」,它由「嘴」、「双脚」、「位置」几个 Component 组成。
另外有一只大头鱼 Entity 叫「老鱼」,显然它没有脚,它只有「嘴巴」和「位置」两个 Component。
然后有「叫系统」、「跑系统」两个 System。

「叫系统」以「嘴」组件为筛选条件。显然「老王」和「老鱼」都满足要求。它从这个组件中获取了「老王」的音量是 100 以及声音“嘎嘎”,然后每帧都调用系统 API 播放音量为 100 的“嘎嘎”的声音;同样,对于「老鱼」,它获取了音量为 50 的“啊啊”声,于是也播放相应的声音。

「跑系统」以「双脚」和「位置」组件为筛选条件。因为「老鱼」没有脚,所以它只会对「老王」作出反应。它从「双脚」中拿到「老王」的奔跑速度,每一帧根据 DeltaTime 和速度计算出「老王」的位移并更新老王的「位置」。

这样,两个简陋的 System 控制着「老王」一边向前奔跑一边嘎嘎叫,而「老鱼」只会“啊啊”叫。

其中理解 ECS 的重点有两个。

一个是数据存储,即 Entity 由多个不同种的 Component 组成。

另一个就是逻辑执行,即 System 只会筛选出拥有特定 Component 的 Entity,并只对那些特定的 Component 进行读写。System 针对的是 Component 而不是 Entity。

总结一下就是,System 执行逻辑,Component 存储数据,Entity 则是将 Component 关联起来。

瞎几把分析

天生对并行友好。观察不难发现,无论是「叫系统」还是「跑系统」,他们每次只会处理同一个 Entity 的某几个固定组件。
因此假设有四只鸭子和一枚四核的 Intel8 代酷睿 i5 处理器,是不是可以每个核心分别处理一只鸭子而完全不需要线程锁?(我对硬件一无所知不要打我)

天生对缓存友好。因为 System 只关心特定的 Component,因此如果将同种 Component 存储在线性内存空间里,System 遍历 Component 的时候缓存命中率会非常高。Unity 的具体实现中是将同类 Entity 的 Component 连续存储,可以实现缓存完全命中

耦合性低。理想状态下,系统只会对 Component 作出反应,因此系统之间的影响只有执行顺序,以及一些公共耦合。但是代码实现中,系统之间信息传递比较困难,不如直接调用更加高性能、高效率(省事)(包括 Unity),因此需要不瞎几把乱写才能发挥 ECS 低耦合的优势。

框架设计比较困难。个人感觉 ECS 有一些麻烦的地方,比如没有回调,系统执行顺序会影响实际的效果。目前成熟的 ECS 框架也不算多。

其他学习资料

感觉我写的有一点过于简单,想要深入了解可以看看下面这些文章。

https://www.cnblogs.com/zhaoqingqing/p/9718973.html
https://blog.codingnow.com/2017/06/overwatch_ecs.html