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

版权声明:欢迎评论和转载,共同进步 https://blog.csdn.net/yudianxia/article/details/80498015

为了性能考虑,所有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的实现

没有更多推荐了,返回首页