# 源码阅读 > 防止 README 过大,不想看 ## ASpawnActor ### 配置项 ![](Image/016.png) #### UMassEntityConfigAsset 上图中配置的就是 `UMassEntityConfigAsset` 类型的资产 其中有且只有 **一个属性** 那就是 `FMassEntityConfig` 类型的 `Config` ```cpp UCLASS(BlueprintType) class MASSSPAWNER_API UMassEntityConfigAsset : public UDataAsset { protected: /** The config described in this asset. */ UPROPERTY(Category = "Entity Config", EditAnywhere) FMassEntityConfig Config; } ``` #### FMassEntityConfig ```cpp USTRUCT() struct MASSSPAWNER_API FMassEntityConfig { public: // 其他函数 Some Functions TObjectPtr Parent = nullptr; TArray> Traits; TObjectPtr ConfigOwner = nullptr; private: FGuid ConfigGuid; } ``` `FMassEntityConfig` 主要就是用来配置 `UMassEntityTraitBase` 通过设置 `Parent` 来实现继承关系,对于多种类型对象存在相同的 `UMassEntityTraitBase` 可以封装通用 `UMassEntityConfigAsset` 作为 `Parent` 实现资产复用 #### UMassEntityTraitBase `UMassEntityTraitBase` 用于在生成模板时一次性把需要的片段、标签和默认值配置好,供成千上万实体复用 > `FMassFragment` 是数据本体,表示纯数据 ```cpp 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 和 FMassFragment - `DestroyTemplate` 删除时调用 - `ValidateTemplate` 当 `FMassEntityTemplate` 构建完毕之后调用,用于检查是否有效 `BuildTemplate` 的例子如下 ```cpp void UMSMoverMassTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const { BuildContext.RequireFragment(); BuildContext.RequireFragment(); BuildContext.AddFragment_GetRef().Value = StartingForce; BuildContext.AddTag(); } ``` `ValidateTemplate` 的例子如下 ```cpp void UMassStateTreeTrait::ValidateTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const { // 其他判断、循环流程 // 判断是否存在指定的 Fragment 没有就打日志 else if (ItemDesc.Struct->IsChildOf(FMassFragment::StaticStruct())) { const bool bContainsFragment = BuildContext.HasFragment(*CastChecked(ItemDesc.Struct)); UE_CVLOG(!bContainsFragment, MassStateTreeSubsystem, LogMassBehavior, Error, TEXT("StateTree %s: Could not find required fragment %s"), *GetNameSafe(StateTree), *GetNameSafe(ItemDesc.Struct)); } } ``` ### 构建并注册 FMassEntityTemplateData ![](Image/018.png) > 大概流程 `PostRegisterAllComponents` 是 `Actor` 创建流程的一个阶段,执行顺序大致是 - UObject::PostLoad - UActorComponent::OnComponentCreated - AActor::PreRegisterAllComponents - UActorComponent::RegisterComponent - AActor::PostRegisterAllComponents - AActor::PostActorCreated - AActor::UserConstructionScript - AActor::OnConstruction - AActor::PreInitializeComponents - UActorComponent::Activate - UActorComponent::InitializeComponent - AActor::PostInitializeComponents - AActor::BeginPlay 在 `AMassSpawner::PostRegisterAllComponents` 函数中调用 `RegisterEntityTemplates` 注册 `FMassEntityTemplateData`,不过这只有在 `NM_Client` 的时候才会执行,一般单机模式是 `NM_Standalone` ```cpp void AMassSpawner::PostRegisterAllComponents() { Super::PostRegisterAllComponents(); if (GEngine->GetNetMode(GetWorld()) == NM_Client) { UMassSpawnerSubsystem* MassSpawnerSubsystem = UWorld::GetSubsystem(World); if (MassSpawnerSubsystem) { RegisterEntityTemplates(); } } } ``` 单机模式下,会在 `SpawnGeneratedEntities` 的时候,在 `EntityConfig->GetOrCreateEntityTemplate` 构建 无论如何,最终都会调用到 `FMassEntityConfig::GetOrCreateEntityTemplate` 函数 ```cpp UMassSpawnerSubsystem* SpawnerSystem = UWorld::GetSubsystem(&World); FMassEntityTemplateData TemplateData; FMassEntityTemplateBuildContext BuildContext(TemplateData, TemplateID); TArray 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` ```cpp FMassEntityTemplate::MakeFinalTemplate(*EntityManager, MoveTemp(TemplateData), TemplateID) ``` 至于 `TemplateID` 则是通过 `FMassEntityConfig::ConfigGuid` 构建的 ```cpp OutTemplateID = FMassEntityTemplateIDFactory::Make(ConfigGuid); ``` 通过上述方法,构建出了 `UMassEntityConfigAsset` 对应的 `FMassEntityTemplateID` 和 `FMassEntityTemplate` 并将 ID 和 Data 注册到 `UMassSpawnerSubsystem::TemplateRegistryInstance` 防止重复构建 #### FMassEntityTemplate 中的 FMassArchetypeData `FMassEntityTemplateData` 保存着对应 Entity 运行时所需的 `Tag` 、 `Fragment` 、 `SharedFragment` 通过这些来构建 `FMassEntityTemplate` ```cpp struct MASSSPAWNER_API FMassEntityTemplate final : public TSharedFromThis { private: FMassEntityTemplateData TemplateData; FMassArchetypeHandle Archetype; FMassEntityTemplateID TemplateID; } ``` `TemplateData` 和 `TemplateID` 都是在构建的时候就知道的,主要是构建出 `Archetype` ```cpp const FMassArchetypeHandle ArchetypeHandle = EntityManager.CreateArchetype(GetCompositionDescriptor(), FName(GetTemplateName())); ``` > `GetCompositionDescriptor` 得到的是 `TemplateData` 中存储的所有 `Fragment` 和 `Tag` 信息,不包括 `SharedFragment` `Archetype` 意为原型、原型布局,基于该 `Archetype` 创建出来的 `Entity` 具有相同的内存布局 通过 `TemplateData` 得到的 `Fragment` 和 `Tag` 只要集合一致 (不看顺序、不允许重复),那么其对应的内存布局应该也是一致的,那么他们就应该使用同一种 Archetype,并共享相同的按 Chunk 划分的内存组织方式 `Archetype` 的职责就是 1. 定义实体的内存布局(每个片段的偏移/对齐/构造/析构规则、每块可容纳多少实体) 2. 维护 chunk 列表、实体在 chunk 内的位置索引 3. 支持增删实体、移动实体到别的 Archetype 4. 支持查询执行:把查询的 **需求** 映射到本 Archetype 的片段索引,并分块执行 ```cpp struct FMassArchetypeData { private: FMassArchetypeCompositionDescriptor CompositionDescriptor; // 描述该 Archetype 拥有哪些 fragment/tag 类型 TArray ChunkFragmentsTemplate; // 当新建 chunk 时,用它们初始化 chunk-level 的片段数据 TArray> FragmentConfigs; // 每个 fragment 类型在本 Archetype 中的布局描述 TArray Chunks; // 该 Archetype 拥有的所有 chunk TMap EntityMap; // 将实体的 全局索引 映射到本 Archetype 内的 绝对索引 TMap 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 ```cpp 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` ```cpp 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` 是连续的 ![](Image/017.png) 一个 Chunk 的内存结构大致如上,前面有 `NumEntitiesPerChunk` 个 Handle,然后是 `NumEntitiesPerChunk` 个连续的相同的 `Fragment` > 没有画出空白内存 ### 创建 Entity ```cpp FFinishedGeneratingSpawnDataSignature Delegate = FFinishedGeneratingSpawnDataSignature::CreateUObject(this, &AMassSpawner::OnSpawnDataGenerationFinished, &Generator); Generator.GeneratorInstance->Generate(*this, EntityTypes, SpawnCount, Delegate); ``` 在 `Generator` 计算完需要生成的坐标对象坐标之后,会触发 `OnSpawnDataGenerationFinished` 函数,最后会调到 `SpawnGeneratedEntities` 函数 ```cpp UMassSpawnerSubsystem* SpawnerSystem = UWorld::GetSubsystem(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` ```cpp // 创建 Chunk 和 Entity TArray SpawnedEntities; TSharedRef CreationContext = EntityManager->BatchCreateEntities(EntityTemplate.GetArchetype(), EntityTemplate.GetSharedFragmentValues(), NumToSpawn, SpawnedEntities); // 设置值 TConstArrayView FragmentInstances = EntityTemplate.GetInitialFragmentValues(); EntityManager->BatchSetEntityFragmentsValues(CreationContext->GetEntityCollection(), FragmentInstances); ``` 在 `FMassEntityManager::BatchCreateReservedEntities` 函数中 初始化 `OutEntities` 数组,这个是返回给外界的数组 ```cpp // TArray& 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` 中的数 ```cpp 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` ```cpp 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` 1. 查找 或 创建 Chunk 2. 将 EntityHandle 保存到 Chunk 中 3. 将 EntityHandle 的 Index 与其数据在 Chunk 中的 AbsoluteIndex 映射关系保存到 EntityMap 中 4. 返回可以保存 Entity 数据的 Chunk 序号 ChunkIndex 通过 AbsoluteIndex 和 Archetype 从 Chunk 中获取对应数据的内存块,并初始化数据 ```cpp FMassArchetypeChunk& Chunk = Chunks[ChunkIndex]; for (const FMassArchetypeFragmentConfig& FragmentConfig : FragmentConfigs) { void* FragmentPtr = FragmentConfig.GetFragmentData(Chunk.GetRawMemory(), IndexWithinChunk); FragmentConfig.FragmentType->InitializeStruct(FragmentPtr); } ```