防止 README 过大,不想看
上图中配置的就是 UMassEntityConfigAsset 类型的资产
其中有且只有 一个属性 那就是 FMassEntityConfig 类型的 Config
UCLASS(BlueprintType)
class MASSSPAWNER_API UMassEntityConfigAsset : public UDataAsset
{
protected:
/** The config described in this asset. */
UPROPERTY(Category = "Entity Config", EditAnywhere)
FMassEntityConfig Config;
}
USTRUCT()
struct MASSSPAWNER_API FMassEntityConfig
{
public:
// 其他函数 Some Functions
TObjectPtr<const UMassEntityConfigAsset> Parent = nullptr;
TArray<TObjectPtr<UMassEntityTraitBase>> Traits;
TObjectPtr<UObject> ConfigOwner = nullptr;
private:
FGuid ConfigGuid;
}
FMassEntityConfig 主要就是用来配置 UMassEntityTraitBase
通过设置 Parent 来实现继承关系,对于多种类型对象存在相同的 UMassEntityTraitBase 可以封装通用 UMassEntityConfigAsset 作为 Parent 实现资产复用
UMassEntityTraitBase 用于在生成模板时一次性把需要的片段、标签和默认值配置好,供成千上万实体复用
FMassFragment是数据本体,表示纯数据
class MASSSPAWNER_API UMassEntityTraitBase : public UObject
{
GENERATED_BODY()
public:
virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const PURE_VIRTUAL(UMassEntityTraitBase::BuildTemplate, return; );
virtual void DestroyTemplate() const {}
virtual void ValidateTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const {};
};
UMassEntityTraitBase 只有三个函数
BuildTemplate 会在构建一个 FMassEntityTemplateData 的时候调用,在此处配置所需的 FMassTag 和 FMassFragmentDestroyTemplate 删除时调用ValidateTemplate 当 FMassEntityTemplate 构建完毕之后调用,用于检查是否有效BuildTemplate 的例子如下
void UMSMoverMassTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const
{
BuildContext.RequireFragment<FTransformFragment>();
BuildContext.RequireFragment<FMassVelocityFragment>();
BuildContext.AddFragment_GetRef<FMassForceFragment>().Value = StartingForce;
BuildContext.AddTag<FMSBasicMovement>();
}
ValidateTemplate 的例子如下
void UMassStateTreeTrait::ValidateTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const
{
// 其他判断、循环流程
// 判断是否存在指定的 Fragment 没有就打日志
else if (ItemDesc.Struct->IsChildOf(FMassFragment::StaticStruct()))
{
const bool bContainsFragment = BuildContext.HasFragment(*CastChecked<UScriptStruct>(ItemDesc.Struct));
UE_CVLOG(!bContainsFragment, MassStateTreeSubsystem, LogMassBehavior, Error, TEXT("StateTree %s: Could not find required fragment %s"), *GetNameSafe(StateTree), *GetNameSafe(ItemDesc.Struct));
}
}
大概流程
PostRegisterAllComponents 是 Actor 创建流程的一个阶段,执行顺序大致是
在 AMassSpawner::PostRegisterAllComponents 函数中调用 RegisterEntityTemplates 注册 FMassEntityTemplateData,不过这只有在 NM_Client 的时候才会执行,一般单机模式是 NM_Standalone
void AMassSpawner::PostRegisterAllComponents()
{
Super::PostRegisterAllComponents();
if (GEngine->GetNetMode(GetWorld()) == NM_Client) {
UMassSpawnerSubsystem* MassSpawnerSubsystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(World);
if (MassSpawnerSubsystem) {
RegisterEntityTemplates();
}
}
}
单机模式下,会在 SpawnGeneratedEntities 的时候,在 EntityConfig->GetOrCreateEntityTemplate 构建
无论如何,最终都会调用到 FMassEntityConfig::GetOrCreateEntityTemplate 函数
UMassSpawnerSubsystem* SpawnerSystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(&World);
FMassEntityTemplateData TemplateData;
FMassEntityTemplateBuildContext BuildContext(TemplateData, TemplateID);
TArray<UMassEntityTraitBase*> CombinedTraits;
GetCombinedTraits(CombinedTraits);
BuildContext.BuildFromTraits(CombinedTraits, World);
BuildContext.SetTemplateName(GetNameSafe(ConfigOwner));
return TemplateRegistry.FindOrAddTemplate(TemplateID, MoveTemp(TemplateData)).Get();
通过 CombinedTraits 和 BuildContext 构建 TemplateData
| FMassEntityTemplateBuildContext 函数 | 作用 |
|---|---|
| AddFragment_GetRef | 向 TemplateData 中添加 Fragment,并返回可写引用,便于立刻设置默认值 |
| AddFragment | 向 TemplateData 中添加 Fragment,默认构造 |
| AddTag | 向 TemplateData 中添加 Tag。Tag 可被多个 Trait 重复添加,不会当作冲突;但仍记录“谁添加过”,便于追踪 |
| AddChunkFragment | 添加 Chunk 级别 Fragment |
| AddConstSharedFragment | 添加 const 的 Shared 级别的 Fragment |
| AddSharedFragment | 添加 Shared 级别的 Fragment |
| AddTranslator | 将当前的 TemplateData 注册到 UMassTranslator 中 |
| RequireFragment | 向 TraitsDependencies 添加,用于 ValidateBuildContext 来检测有效性 |
| RequireTag | 向 TraitsDependencies 添加,用于 ValidateBuildContext 来检测有效性 |
| TypeAdded | 记录 Trait 添加了哪些 Fragment 和 Tag |
| ValidateBuildContext | 检查 Tag 和 Fragment 的有效性 |
UMassTranslator 用于 ECS 世界的数据与 UE 世界的数据进行同步
最后通过 FMassEntityTemplateData 来构建 FMassEntityTemplate
FMassEntityTemplate::MakeFinalTemplate(*EntityManager, MoveTemp(TemplateData), TemplateID)
至于 TemplateID 则是通过 FMassEntityConfig::ConfigGuid 构建的
OutTemplateID = FMassEntityTemplateIDFactory::Make(ConfigGuid);
通过上述方法,构建出了 UMassEntityConfigAsset 对应的 FMassEntityTemplateID 和 FMassEntityTemplate
并将 ID 和 Data 注册到 UMassSpawnerSubsystem::TemplateRegistryInstance 防止重复构建
FMassEntityTemplateData 保存着对应 Entity 运行时所需的 Tag 、 Fragment 、 SharedFragment
通过这些来构建 FMassEntityTemplate
struct MASSSPAWNER_API FMassEntityTemplate final : public TSharedFromThis<FMassEntityTemplate>
{
private:
FMassEntityTemplateData TemplateData;
FMassArchetypeHandle Archetype;
FMassEntityTemplateID TemplateID;
}
TemplateData 和 TemplateID 都是在构建的时候就知道的,主要是构建出 Archetype
const FMassArchetypeHandle ArchetypeHandle = EntityManager.CreateArchetype(GetCompositionDescriptor(), FName(GetTemplateName()));
GetCompositionDescriptor得到的是TemplateData中存储的所有Fragment和Tag信息,不包括SharedFragment
Archetype 意为原型、原型布局,基于该 Archetype 创建出来的 Entity 具有相同的内存布局
通过 TemplateData 得到的 Fragment 和 Tag 只要集合一致 (不看顺序、不允许重复),那么其对应的内存布局应该也是一致的,那么他们就应该使用同一种 Archetype,并共享相同的按 Chunk 划分的内存组织方式
Archetype 的职责就是
struct FMassArchetypeData
{
private:
FMassArchetypeCompositionDescriptor CompositionDescriptor; // 描述该 Archetype 拥有哪些 fragment/tag 类型
TArray<FInstancedStruct> ChunkFragmentsTemplate; // 当新建 chunk 时,用它们初始化 chunk-level 的片段数据
TArray<FMassArchetypeFragmentConfig, TInlineAllocator<16>> FragmentConfigs; // 每个 fragment 类型在本 Archetype 中的布局描述
TArray<FMassArchetypeChunk> Chunks; // 该 Archetype 拥有的所有 chunk
TMap<int32, int32> EntityMap; // 将实体的 全局索引 映射到本 Archetype 内的 绝对索引
TMap<const UScriptStruct*, int32> FragmentIndexMap; // 把 fragment 的脚本结构类型映射到 FragmentConfigs 的下标,便于 O(1) 定位布局信息
int32 NumEntitiesPerChunk; // 一个 chunk 最多容纳多少实体
int32 TotalBytesPerEntity; // 一个 Entity 占用内存大小,也就是所有 Fragment 大小 + 内存对齐
int32 EntityListOffsetWithinChunk; // 在 chunk 原始内存中,存放实体列表(比如实体句柄/索引数组)的起始偏移
public:
// 其他工具函数
}
通过 Initialize 来初始化一个 Archetype,通过 ConfigureFragments 来计算 NumEntitiesPerChunk、TotalBytesPerEntity
通过 SortedFragmentList 存储按照 字节大小 排序后的 Fragments,遍历所有的 Fragment 得到 TotalBytesPerEntity 表示一个 Entity 数据所占内存大小
排序是为了保证布局顺序的确定性,用于生成稳定的 Archetype
int32 FragmentSizeTallyBytes = 0;
FragmentSizeTallyBytes += sizeof(FMassEntityHandle); // 初始值为 sizeof(FMassEntityHandle)
for (int32 FragmentIndex = 0; FragmentIndex < SortedFragmentList.Num(); ++FragmentIndex)
{
// 其他处理
FragmentConfigs[FragmentIndex].FragmentType = FragmentType; // 保存 Fragment 数据到 FragmentConfigs 中
AlignmentPadding += FragmentType->GetMinAlignment(); // 空白内存占用字节
FragmentSizeTallyBytes += FragmentType->GetStructureSize(); // Fragment 内存占用字节
}
// 计算得到 TotalBytesPerEntity
TotalBytesPerEntity = FragmentSizeTallyBytes;
int32 ChunkAvailableSize = GetChunkAllocSize() - AlignmentPadding;
NumEntitiesPerChunk = ChunkAvailableSize / TotalBytesPerEntity;
注意 FragmentSizeTallyBytes 大小是 所有Fragment的大小和 + sizeof(FMassEntityHandle)
FMassEntityHandle 用于存储一个 Index 和一个 SerialNumber,Index 表示全局大数组的序号, SerialNumber 用于校验,类似 WeakObjectPtr 的实现原理
ChunkAvailableSize 表示一个 Chunk 中实际有效的空间大小,其值是 GetChunkAllocSize 减去对齐填充的空白内存
NumEntitiesPerChunk 表示一个 Chunk 中有多少个 Entity 的数据,就是 ChunkAvailableSize 除以一个 Entity 占用的大小 TotalBytesPerEntity
int32 CurrentOffset = NumEntitiesPerChunk * sizeof(FMassEntityHandle);
for (FMassArchetypeFragmentConfig& FragmentData : FragmentConfigs)
{
CurrentOffset = Align(CurrentOffset, FragmentData.FragmentType->GetMinAlignment());
FragmentData.ArrayOffsetWithinChunk = CurrentOffset;
const int32 SizeOfThisFragmentArray = NumEntitiesPerChunk * FragmentData.FragmentType->GetStructureSize();
CurrentOffset += SizeOfThisFragmentArray;
}
从上面的代码就可以看到一个 Chunk 的实际内存布局是怎样的
CurrentOffset 初始化为 NumEntitiesPerChunk * sizeof(FMassEntityHandle)
可见,一个 Chunk 前面有 NumEntitiesPerChunk 个 FMassEntityHandle 来 Handle 数据
每计算一个 FragmentData 都会让 CurrentOffset += SizeOfThisFragmentArray
可见,一个 Chunk 中 NumEntitiesPerChunk 个相同的 Fragment 是连续的
一个 Chunk 的内存结构大致如上,前面有 NumEntitiesPerChunk 个 Handle,然后是 NumEntitiesPerChunk 个连续的相同的 Fragment
没有画出空白内存
FFinishedGeneratingSpawnDataSignature Delegate = FFinishedGeneratingSpawnDataSignature::CreateUObject(this, &AMassSpawner::OnSpawnDataGenerationFinished, &Generator);
Generator.GeneratorInstance->Generate(*this, EntityTypes, SpawnCount, Delegate);
在 Generator 计算完需要生成的坐标对象坐标之后,会触发 OnSpawnDataGenerationFinished 函数,最后会调到 SpawnGeneratedEntities 函数
UMassSpawnerSubsystem* SpawnerSystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(GetWorld());
// 一些其他的计算
const FMassEntityTemplate& EntityTemplate = EntityConfig->GetOrCreateEntityTemplate(*World);
if (EntityTemplate.IsValid())
{
FSpawnedEntities& SpawnedEntities = AllSpawnedEntities.AddDefaulted_GetRef();
SpawnedEntities.TemplateID = EntityTemplate.GetTemplateID();
SpawnerSystem->SpawnEntities(EntityTemplate.GetTemplateID(), Result.NumEntities, Result.SpawnData, Result.SpawnDataProcessor, SpawnedEntities.Entities);
}
通过 UMassSpawnerSubsystem 来创建 Entity
// 创建 Chunk 和 Entity
TArray<FMassEntityHandle> SpawnedEntities;
TSharedRef<FMassEntityManager::FEntityCreationContext> CreationContext = EntityManager->BatchCreateEntities(EntityTemplate.GetArchetype(), EntityTemplate.GetSharedFragmentValues(), NumToSpawn, SpawnedEntities);
// 设置值
TConstArrayView<FInstancedStruct> FragmentInstances = EntityTemplate.GetInitialFragmentValues();
EntityManager->BatchSetEntityFragmentsValues(CreationContext->GetEntityCollection(), FragmentInstances);
在 FMassEntityManager::BatchCreateReservedEntities 函数中
初始化 OutEntities 数组,这个是返回给外界的数组
// TArray<FMassEntityHandle>& OutEntities
int32 Index = OutEntities.Num();
OutEntities.Reserve(Index + Count);
for (int32 Counter = 0; Counter < Count; ++Counter)
{
OutEntities.Add(ReserveEntity());
}
注意一下 ReserveEntity 函数的实现,该函数用来构建 FMassEntityHandle
这里存在两个 FMassEntityManager 的属性 Entities 和 EntityFreeIndexList
EntityFreeIndexList 保存着可以使用的 EntityIndex,当一个 Entity 被 Destroy 的时候会将被 Destroy 的 Entity 的 EntityIndex 添加到 EntityFreeIndexList 中用于后续复用Entities 保存着所有的 FEntityData,可以通过 EntityIndex 获取 Entities 中的数FMassEntityHandle FMassEntityManager::ReserveEntity()
{
FMassEntityHandle Result;
Result.Index = (EntityFreeIndexList.Num() > 0) ? EntityFreeIndexList.Pop(/*bAllowShrinking=*/ false) : Entities.Add();
Result.SerialNumber = SerialNumberGenerator.fetch_add(1);
Entities[Result.Index].SerialNumber = Result.SerialNumber;
return Result;
}
通过 InternalBatchCreateReservedEntities 真正创建 Entity
FMassArchetypeData* ArchetypeData = FMassArchetypeHelper::ArchetypeDataFromHandle(ArchetypeHandle);
for (FMassEntityHandle Entity : ReservedEntities)
{
FEntityData& EntityData = Entities[Entity.Index];
EntityData.CurrentArchetype = ArchetypeHandle.DataPtr;
EntityData.SerialNumber = Entity.SerialNumber;
ArchetypeData->AddEntity(Entity, SharedFragmentValues);
}
通过前面 FMassArchetypeData 内存结构可以直到,每个 Archetype 都独自管理着自己的 Chunk,也相当于管理着所有派生自自己的 Entity
通过 FMassArchetypeData::AddEntityInternal
通过 AbsoluteIndex 和 Archetype 从 Chunk 中获取对应数据的内存块,并初始化数据
FMassArchetypeChunk& Chunk = Chunks[ChunkIndex];
for (const FMassArchetypeFragmentConfig& FragmentConfig : FragmentConfigs)
{
void* FragmentPtr = FragmentConfig.GetFragmentData(Chunk.GetRawMemory(), IndexWithinChunk);
FragmentConfig.FragmentType->InitializeStruct(FragmentPtr);
}