Unity2018 ECS框架Entities源码解析(二)组件与Chunk的内存布局

为了性能考虑,所有Entity里的组件数据都会存放在被称为Chunk的连续内存空间里,因为cpu访问某内存时会顺便加载附近的一段数据,所以数据的连续存放有利于提高缓存命中。

当然,并不是所有的A组件的值都放在同一个Chunk里的,Chunk是按EntityArchetype分配的。EntityArchetype核心是Archetype类,其实就是一个组件的数组,拥有相同组件集合的Entity指向相同的Archetype,Archetype对应一个Chunk数组,Chunk是一个固定大小的缓存空间,当一个Chunk空间用光了就创建多一个。

当你对一个特定的entity调用EntityManager:AddComponent时传入A组件就会获取或创建一个仅有A的Archetype(拥有相同组件集合的Archetype只会存在一份,由ArchetypeManager.m_TypeLookup管理),再次调用AddComponent传入B组件时就会创建一个{A,B}组件集合的Archetype,并把之前A组件的数据复制到新的Archetype指向的Chunk上,所以官方也推荐先创建好EntityArchetype来CreateEntity,或者一次性传入所有需要的组件。

创建Archetype时会预计算好各个组件占用的字节大小和内存偏移等信息,然后分配一段固定大小的内存空间给Chunk。在取某Entity某组件数据时就找到该Chunk,根据自己的IndexInChunk和组件在Chunk里的偏移就可以访问到了。

我们先看看创建一个Entity时做的操作:

首先我们知道Entity只是一个Index,关于它的信息都交由EntityDataManager管理,m_Entities数组就是干这个用的(注意Entity的值(Index和Version两int而已)还会和它的组件值保存在同个Chunk上,这样用EntityArray时才能在连续内存高速访问),Entity的Index就是在此数组的索引,m_Entities数组里的元素类型是EntityData结构体,其中Archetype字段指定了Entity的Archetype(即拥有的组件类型集合),ChunkData字段保存着该Entity的组件具体值在哪个Chunk上以及Entity在Chunk的序号,可用于定位特定组件的值。

m_Entities数组会预告分配好内存,只够容纳10个Entity的上述信息,每次不够就扩大两倍。创建Entity时其Index就依靠m_EntitiesFreeIndex变量每次加1。

大致分为几步:

1)通过EntityManager:CreateEntity方法,传入所需组件类型比如{A,B,C}

2)根据传入的组件类型获取或创建一个Archetype,注意不管传入什么类型都会在第一个加上Entity即:{Entity,A,B,C}

3)获取或创建一个Chunk,并把相应内存位置的组件值置为0

4)EntityDataManager为新Entity分配一个Index(其实就是用一个每次加1的变量给它赋值),然后把Archetype、Chunk指针和Entity在其的序号保存在m_Entities里。

具体一点的代码分析:

注:为了简洁描述和减少理解成本,本文出现的代码均经过删减

从EntityManager:CreateEntity方法开始,其有两种重载,一是传入EntityArchetype,二是传入若干ComponentType,其实还是会先把传入的ComponentType们通过CreateArchetype创建一个EntityArchetype的,然后调用EntityDataManager:CreateEntities,那里才是核心:

public void CreateEntities(ArchetypeManager archetypeManager, Archetype* archetype, Entity* entities, int count)
{
     while (count != 0)
     {
         //取得或创建一个Chunk,之所以需要在一个while里多次获取或创建,是因为一个Chunk可以容纳的Entity和其组件值是有限的
         //所以拿到一个Chunk先用光其空间,下一次就会创建另外一个新的Chunk了。一个Chunk的空间大小可以看其常量成员:
         //kChunkSize=16 * 1024 - 256,够放好几百个组件了。
         var chunk = archetypeManager.GetChunkWithEmptySlots(archetype);
         int allocatedIndex;
         var allocatedCount = archetypeManager.AllocateIntoChunk(chunk, count, out allocatedIndex);
         //记录下新创建的Entity的数据,包括它在Chunk的偏移,和它引用哪个Archetype等
         AllocateEntities(archetype, chunk, allocatedIndex, allocatedCount, entities);
         //把Chunk中对应新创建的Entity的所有组件值设置为空(比如int类型就为0)
         ChunkDataUtility.InitializeComponents(chunk, allocatedIndex, allocatedCount);
         entities += allocatedCount;
         count -= allocatedCount;
     }
}

//从InitializeComponents方法里可以看出组件在Chunk的内存分布:
public static void InitializeComponents(Chunk* dstChunk, int dstIndex, int count)
{
     var arch = dstChunk->Archetype;
     //每个组件在Chunk中的偏移,Entity不是组件但也加了进来,总放在第1个,即Offsets为0
     var offsets = arch->Offsets;
     //每个组件类型占用空间大小,比如A组件只有一个int的话就是4字节(不同平台可能不一样的,所以实际是sizeof(int)字节)
     var sizeOfs = arch->SizeOfs;
     //Chunk结构体用于存储信息的起始地址就是Buffer
     var dstBuffer = dstChunk->Buffer;
     var typesCount = arch->TypesCount;
     var types = arch->Types;
     //遍历Archetype里所有的组件类型,把从dstIndex开始的count个组件值置为0
     for (var t = 1; t != typesCount; t++)
     {
         var offset = offsets[t];
         var sizeOf = sizeOfs[t];
         //某entity的A组件值的地址就是Chunk.Buffer+组件类型A在Chunk的偏移+组件A的大小*entity在Chunk的序号
         var dst = dstBuffer + (offset + sizeOf * dstIndex);
         UnsafeUtility.MemClear(dst, sizeOf * count);
     }
}

Archetype的SizeOfs,就是上面注解说的各组件类型的占空间字节,我们假设该Archetype有三个组件:

{Entity两个int字段,A组件:一个int字段,B:两个int,C:三个double},那SizeOfs数组的内容就是:[4*2,4,4*2,8*3],假定int4字节,double8字节。

Offsets数组指定各组件在Chunk的偏移,以上面假设的Archetype看可以简单地认为Entity和ABC三大组件瓜分了整个Chunk,组件类型越大自然占越多空间,具体算法可见ArchetypeManager:CreateArchetypeInternal方法:

int bytesPerInstance=所有组件类型占用字节之和:4*2+4+4*2+8*3;

archetype->ChunkCapacity = chunk的缓存空间大小 / bytesPerInstance;

这个ChunkCapacity就是Chunk可以容纳多少个拥有此组件集合的Entity,然后就给每个组件设置偏移了:

var usedBytes = 0;
for (var i = 0; i < count; ++i)
{
       var index = type->TypeMemoryOrder[i];
       var sizeOf = type->SizeOfs[index];
       type->Offsets[index] = usedBytes;
       usedBytes += sizeOf * type->ChunkCapacity;
}

所以如果有六个Entity甲乙丙丁戊己,且都有上面假设的A,B,C组件的话,其Chunk布局如图(顺序为从左到右再由上到下): 

当然实际上一个Chunk的空间大小为16 * 1024 - 256,已经够放好几百个这种Entity了。

还有其实Chunk的末尾那部分缓存还需要保存ShardComponent和ChangeVersion等信息,但为了避免一次性接收过多概念就略过了,留待读者自己翻阅源码吧。

有了上述知识再来看SetComponentData时就简单多了,就是按照上面的公式算出该Entity的那个组件的值的指针地址,然后直接把新的值拷贝过去就行了,代码不多就不贴了。

下一章我们看看System是怎么高效注入组件信息的:ComponentDataArray的实现

<p> </p> <p><strong>一: 什么是ECS?</strong><br />       ECS是一种新的架构模式(当然是针对Unity本身来说),这是一个取代GameObject / Component 的模式。 其模式遵循组合优于继承原则,游戏内的每一个基本单元都是一个实体,每个实体又由一个或多个组件构成,每个组件仅仅包含代表其特性的数据(即在组件中没有任何方法)。系统便是来处理拥有一个或多个相同组件的实体集合的工具,其只拥有行为(即在系统中没有任何数据)。<br />       ECS旨在比GameObject / MonoBehaviour更容易处理大量物体。ECS由于是面向数据的设计,所以很容易并行高速处理,以及与Unity开发的C#JobSystem/Burst Compiler一起工作,从而获取更高的项目运行性能。</p> <p><img src="https://img-bss.csdn.net/201912160343306524.jpg" alt="" /></p> <p><strong>ECS总体发展历史</strong><br />       目前Unity公司由Mike Acton 大佬主持DOD(Data Oriented Design 面向数据设计)的全面改革与推进工作,目的是旨在进一步推进Unity系统本身与Unity项目的执行效率。<br />    ECS总体发展:<br />    A: 在Unity2018引入Entities之前,其实ECS框架早已经出现,但是ECS还是因为守望先锋采用了这个框架才被我们所熟知,后来Git上面出现了一个Entitas的插件可以直接用在Unity上面。Entitas-Unity(有多个语言的port版本,Entitas-Unity下统一称为Entitas) 是 Unity的一个ECS(Entity/Component/System)框架,是在github上面的一个开源项目。<br />    B: 经过Unity公司的认可与改造,目前Unity2018.x 版本已经通过 "Package Manager" 提供了Unity版本的ECS插件,名称为:“Entities”。<br />    C: Unity2019.x 版本,对“Entities”插件的API进行了进一步改造与完善,以更规范的API实现更加高效的与规范的编程模式。</p> <p><img src="https://img-bss.csdn.net/201912160343545743.jpg" alt="" /></p> <p> </p> <p><strong>三:ECS(一)轻松入门篇</strong><br />       本“ECS入门篇”旨在全面讲解ecs 的相关理论与简单Hello World 的编程模式,且通过Unity2018.x与Unity2019.x 两个不同API的编程模式,初步讲解ECS的编程规范与模式规范。</p> <p>        需要进一步学习ECS的小伙伴们,可以关注后续“ECS()小试牛刀”篇的进一步讲解。</p> <p><img src="https://img-bss.csdn.net/201912160344095065.jpg" alt="" /></p> <p> </p> <div><br /><br />  《Unity ECS() 小试牛刀》<br />https://edu.csdn.net/course/detail/27096<br /><br /></div> <p> </p>
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页