Unity DOTS中ECS核心架构详解

发布时间 2023-10-18 11:57:24作者: rain4414
 最近DOTS终于发布了正式的版本, 我们来分享一下DOTS中ECS的几个关键概念与结构,方便大家上手学习掌握Unity DOTS开发。
 
ECS中的World
  Unity DOTS ECS架构中所有的Entity都是被放到了World对象里面,每个Entity在World里面都有唯一的Id号。Unity DOTS 可以同时支持很多个World, DOTS会在运行的时候创建一个默认的World。World包含了它所需的所有System, System迭代计算的时候,使用World里面的Entity中的Component数据。如果不想要一运行就创建一个默认的World,我们可以通过以下宏开关来控制:
#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_RUNTIME_WORLD
关闭运行模式下启动的时候,创建一个默认的World;
#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_EDITOR_WORLD
关闭编辑器模式下创建默认的World对象;
#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP
关闭掉编辑器模式/运行模式下的默认的World的创建;
EntityManager对象
  每个World, 有且只有一个EntityManager对象。EntityManager对象负责Entity的管理,包含但不限于: 创建Entity, 销毁Entity, 修改Entity中的数据等。
Entity对象
    DOTS 中的每一个实体,我们称为Entity, 它本身是一个容器,可以理解为一个轻量级的gameObject, 它的数据全部存放在它的对应的组件里面。Entiy里面的所有组件数据内存会并行一起排布。这样保证了Entity 数据内存的高效访问与Cache命中。相当于把Entity所包含的所有组件数据的内存打包到一个内存块中。比如一个A类Entity, 它包含了有M,N两个组件数据,当我们给Entity来分布组件数据内存的时候,是把M+N两个组件的内存看作一种类型,线性排布在一起分配。存储数据的时候M,就操作这个内存块的M的部分。操作N组件数据的时候,就操作这个内存块的N的部分。Entity中的所有组件组成的内存块看作的这个类型,我们叫做ArchType。相同类型的Entity(所有组件的组成结构相同)属于同一种ArchType。
 
ArchType与Chunk高效的内存分配器
 
高效的内存分配器需要具备几个特点:
 
  1: 高效地分配与释放;
 
  2: 避免大量的分配与释放后造成的内存碎片;
 
上文提到,Entity中的所有组件数据是排布在一块内存里面的,每一种不同的排布,就会对应一种"类型"ArchType, DOTS 高效的内存分配器只需要基于ArchType所对应的内存块大小来进行分配就可以了。DOTS的entity组件数据的内存分配器基于Chunk设计,每个Chunk的大小为16kb,每个Chunk只会分配同一种类型的ArchType, 根据ArchType的组件组合,我们就可以计算出它们一共所需要的内存大小,我们就从Chunk中固定分配对应的内存大小就可以了,这样Entity对应的所有组件内存非常高效的分配与释放,同时每种Chunk只存放一种ArchType类型的内存块,每次分配的内存大小都是一样,这样可以避免内存碎片。基于ArchType的内存排布如下所示:

 

 

System与JobSystem

   我们学C语言的时候,听到过一句名言,程序=数据结构+算法。Entity解决了数据存储的问题,System就是算法。算法所需要的数据,来源于Entity中的Component。DOTS提供机制,System可以访问到entity中的组件数据,拿到这些数据后再做逻辑迭代计算与处理。默认System是运行在Unity的main thread上的,为了发挥多核优势,把可以用多线程处理的任务使用多线程,Unity 还提供了JobSystem机制,通过多线程的线程池来迭代计算JobSystem,不放main thread上提升程序的效率。

 

总结如下:

DOTS中会有一个World对象,每个Word对象会有一个EntityManager负责Entity的管理,内部使用了高效的基于ArchType与Chunk机制的内存分配。所有的system会加入到World里面来进行统一迭代,System可以访问Entity中的Component数据。同时JobSystem可以让我们的算法迭代基于多线程处理。配上总结图如下: