SourceCode.md 16 KB

源码阅读

防止 README 过大,不想看

ASpawnActor

配置项

UMassEntityConfigAsset

上图中配置的就是 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;
}

FMassEntityConfig

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

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 和 FMassFragment
  • DestroyTemplate 删除时调用
  • ValidateTemplateFMassEntityTemplate 构建完毕之后调用,用于检查是否有效

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));
    }
}

构建并注册 FMassEntityTemplateData

大概流程

PostRegisterAllComponentsActor 创建流程的一个阶段,执行顺序大致是

  • 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

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();

通过 CombinedTraitsBuildContext 构建 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 对应的 FMassEntityTemplateIDFMassEntityTemplate

并将 ID 和 Data 注册到 UMassSpawnerSubsystem::TemplateRegistryInstance 防止重复构建

FMassEntityTemplate 中的 FMassArchetypeData

FMassEntityTemplateData 保存着对应 Entity 运行时所需的 TagFragmentSharedFragment

通过这些来构建 FMassEntityTemplate

struct MASSSPAWNER_API FMassEntityTemplate final : public TSharedFromThis<FMassEntityTemplate> 
{
private:
	FMassEntityTemplateData TemplateData;
	FMassArchetypeHandle Archetype;
	FMassEntityTemplateID TemplateID;
}

TemplateDataTemplateID 都是在构建的时候就知道的,主要是构建出 Archetype

const FMassArchetypeHandle ArchetypeHandle = EntityManager.CreateArchetype(GetCompositionDescriptor(), FName(GetTemplateName()));

GetCompositionDescriptor 得到的是 TemplateData 中存储的所有 FragmentTag 信息,不包括 SharedFragment

Archetype 意为原型、原型布局,基于该 Archetype 创建出来的 Entity 具有相同的内存布局

通过 TemplateData 得到的 FragmentTag 只要集合一致 (不看顺序、不允许重复),那么其对应的内存布局应该也是一致的,那么他们就应该使用同一种 Archetype,并共享相同的按 Chunk 划分的内存组织方式

Archetype 的职责就是

  1. 定义实体的内存布局(每个片段的偏移/对齐/构造/析构规则、每块可容纳多少实体)
  2. 维护 chunk 列表、实体在 chunk 内的位置索引
  3. 支持增删实体、移动实体到别的 Archetype
  4. 支持查询执行:把查询的 需求 映射到本 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 前面有 NumEntitiesPerChunkFMassEntityHandle 来 Handle 数据

每计算一个 FragmentData 都会让 CurrentOffset += SizeOfThisFragmentArray

可见,一个 Chunk 中 NumEntitiesPerChunk 个相同的 Fragment 是连续的

一个 Chunk 的内存结构大致如上,前面有 NumEntitiesPerChunk 个 Handle,然后是 NumEntitiesPerChunk 个连续的相同的 Fragment

没有画出空白内存

创建 Entity

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 的属性 EntitiesEntityFreeIndexList

  • 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

  1. 查找 或 创建 Chunk
  2. 将 EntityHandle 保存到 Chunk 中
  3. 将 EntityHandle 的 Index 与其数据在 Chunk 中的 AbsoluteIndex 映射关系保存到 EntityMap 中
  4. 返回可以保存 Entity 数据的 Chunk 序号 ChunkIndex

通过 AbsoluteIndex 和 Archetype 从 Chunk 中获取对应数据的内存块,并初始化数据

FMassArchetypeChunk& Chunk = Chunks[ChunkIndex];
for (const FMassArchetypeFragmentConfig& FragmentConfig : FragmentConfigs)
{
	void* FragmentPtr = FragmentConfig.GetFragmentData(Chunk.GetRawMemory(), IndexWithinChunk);
	FragmentConfig.FragmentType->InitializeStruct(FragmentPtr);
}