ue5 Mass 框架简介
UE5 Mass 框架介绍
UE5 中 Mass 分为几个库,都是以插件的形式放在引擎源码中,默认是关闭的,目前还是试验性插件。
官方给出了示范工程:城市示例 1,大致模块组织如下:
其中 MassEntity 是这个 Mass 框架的基础,这里先从 MassEntity 库开始。
MassEntity
在介绍 MassEntity 之前,先引入几个概念:
- Archetype:原型,可以类比面向对象编程里的类定义,包含 Fragment,Tag 等
- Entity:使用原型创建的实例对象,可以类比类实例化后的对象
- Fragment:每个原型的数据片段的定义
- Tag:原型的标签,不包含数据
- Handle:句柄,MassEntity 中有两种句柄:ArchetypeHandle 跟 EntityHandle
- Trait:特性,由若干个 Fragment 组成
- Processor:处理器,用来对 Entity 数据进行处理的类
MassEntity 插件中的 MassEntityTestSuite 模块中给出了一个种田游戏的案例,一般按照面向对象,我们会定义如下类型:
class Plant {
/// 当前水量
float CurrentWater = 1.0f;
// 每秒耗水量
float DeltaWaterPerSecond = -0.01f;
// 成熟时间
uint32 NumSecodsLeft = 15;
}
class Flower: public Plant {
uint32 NumBonusTicks = 0;
uint16 FlowerType = 0;
}
class Corp: public Plant {
unit16 CorpType = 0;
}
Fragment
Fragmenet 是 Entity 的数据部分,继承基类 FMassFragment
struct FMassFragment
{
FMassFragment() {}
};
测试示例中的 Fragment 定义:
// 基类拆分后的 Fragment
USTRUCT()
struct FFarmWaterFragment : public FMassFragment
{
GENERATED_BODY()
float CurrentWater = 1.0f;
float DeltaWaterPerSecond = -0.01f;
};
USTRUCT()
struct FHarvestTimerFragment : public FMassFragment
{
GENERATED_BODY()
uint32 NumSecondsLeft = 15;
};
// Flower 的 Fragment
USTRUCT()
struct FFarmFlowerFragment : public FMassFragment
{
GENERATED_BODY()
uint32 NumBonusTicks = 0;
uint16 FlowerType = 0;
};
// Corp 的 Fragment
USTRUCT()
struct FFarmCropFragment : public FMassFragment
{
GENERATED_BODY()
uint16 CropType = 0;
};
Shared Fragments
同一个 Archetype 下的多个 Entity 公用的数据可以使用 SharedFragment,例如 LOD,可以理解为是类的静态(static)成员变量
USTRUCT()
struct FMassSharedFragment
{
GENERATED_BODY()
FMassSharedFragment() {}
};
struct MASSMOVEMENT_API FMassMovementParameters : public FMassSharedFragment
{
float MaxSpeed = 200.f;
float DefaultDesiredSpeed = 140.f;
}
ChunkFragment
ChunkFragment 也是表示多个 Entity 公用的数据,但是是在同一个 Chunk 上的 Entity 共享的数据。
USTRUCT()
struct FMassChunkFragment
{
GENERATED_BODY()
FMassChunkFragment() {}
};
struct MASSLOD_API FMassVisualizationChunkFragment : public FMassChunkFragment
{
EMassVisibility Visibility = EMassVisibility::Max;
}
UE5.1.1 版本中,同一个 Archetype 的数据以 128K 大小的内存空间做为一个 chunk 来存储当前 Archetype 类型的 Entity。
Tag
Tag 不包含数据成员,主要使用来做查询过滤。
USTRUCT()
struct FMassTag
{
GENERATED_BODY()
FMassTag() {}
};
struct FFarmJustBecameReadyToHarvestTag : public FMassTag
{
GENERATED_BODY()
};
USTRUCT()
struct FFarmReadyToHarvestTag : public FMassTag
{
GENERATED_BODY()
};
Tag 不能有成员变量!!!!
Archetype
Archetype(原型)就是定义 Entity 组成成分的类结构,包括上面的:Fragment、Tag、Trait,他们的关系如下图:
创建 Archetype
首先我们需要先创建 Archetype,通过使用 FMassEntityManager 提供的接口,我们可以根据我们现有的 Fragment、Tag 创建出我们需要的 Archetype。
/// 示例中自己创建了一个 FMassEntityManager
AMassEntityTestFarmPlot::AMassEntityTestFarmPlot()
: SharedEntityManager(MakeShareable(new FMassEntityManager(this)))
{
}
void AMassEntityTestFarmPlot::BeginPlay()
{
/// 5.1.1 创建 Archetype 跟 Entity 都是用 EntityManager
FMassEntityManager& EntityManager = SharedEntityManager.Get();
// 自己创建的 EntityManager 要先初始化
EntityManager.Initialize();
// 通过 Tag、Fragment、ChunkFragment、SharedFragment 列表创建一个原型
// Crop 原型
FMassArchetypeHandle CropArchetype = EntityManager.CreateArchetype(
TArray<const UScriptStruct*>{
FFarmWaterFragment::StaticStruct(),
FFarmCropFragment::StaticStruct(),
FHarvestTimerFragment::StaticStruct()
});
// Flower 原型
FMassArchetypeHandle FlowerArchetype = EntityManager.CreateArchetype(
TArray<const UScriptStruct*>{
FFarmWaterFragment::StaticStruct(),
FFarmFlowerFragment::StaticStruct(),
FHarvestTimerFragment::StaticStruct()
});
}
创建原型后会返回原型的句柄,同样创建 Entity 也是返回句柄,在使用中,句柄跟 Archetype 跟 Entity 的对应关系如下,
原型类详细定义:
struct FMassArchetypeData
{
private:
// 原型描述类
FMassArchetypeCompositionDescriptor CompositionDescriptor;
// 记录原型每个 Fragment 在单个 chunk 上的地址偏移量
TArray<FMassArchetypeFragmentConfig, TInlineAllocator<16>> FragmentConfigs;
// Fragment 序号 map
TMap<const UScriptStruct*, int32> FragmentIndexMap;
// chunks
TArray<FMassArchetypeChunk> Chunks;
// { key: Entity.Index, value: Entity 在当前 Chunk 上的位置 }
TMap<int32, int32> EntityMap;
}
原型描述器
首先注意到 FMassArchetypeCompositionDescriptor,它用来存储 MassArchetype 上 Fragment、Tags、ChunkFragment、SharedFragment 的信息,并且根据这些信息生成一个 Hash 值,这个 Hash 值可以用来标识 MassArchetype 唯一性。
struct FMassArchetypeCompositionDescriptor
{
/// 一个可以记录 Fragment 列表,并转成 bit 位的数据结构
FMassFragmentBitSet Fragments;
FMassTagBitSet Tags;
FMassChunkFragmentBitSet ChunkFragments;
FMassSharedFragmentBitSet SharedFragments;
}
/// FMassFragmentBitSet
DECLARE_STRUCTTYPEBITSET_EXPORTED(MASSENTITY_API, FMassFragmentBitSet, FMassFragment);
// 宏展开后
struct FMassFragmentBitSetStructTracker
{
MASSENTITY_API static FStructTracker StructTracker;
};
template struct MASSENTITY_API TStructTypeBitSet<FMassFragment,
FMassFragmentBitSetStructTracker, UScriptStruct>;
using FMassFragmentBitSet = TStructTypeBitSet<FMassFragment,
FMassFragmentBitSetStructTracker, UScriptStruct>
TSructTypeBitSet 提供了接口,增加类型,并且生成对应的 bit 位,
template<typename TBaseStruct,
typename TStructTrackerWrapper, typename TUStructType = UScriptStruct>
struct TStructTypeBitSet
{
void Add(const TUStructType& InStructType)
{
// 这里会访问 TStructTrackerWrapper::StructTracker
// 即 FMassFragmentBitSetStructTracker::StructTracker
const int32 StructTypeIndex =
TStructTrackerWrapper::StructTracker.FindOrAddStructTypeIndex(InStructType);
// 获取到 Fragment 的 Index 后 生成对应 bit 位
StructTypesBitArray.AddAtIndex(StructTypeIndex);
}
}
因此所有增加了的 Fragment 类型会存放到 FMassFragmentBitSetStructTracker::StructTracker 这个静态变量上。
struct FStructTracker
{
int32 FindOrAddStructTypeIndex(const UStruct& InStructType)
{
const uint32 Hash = PointerHash(&InStructType);
FSetElementId ElementId = StructTypeToIndexSet.FindIdByHash(Hash, Hash);
if (!ElementId.IsValidId())
{
// .. or create new one
ElementId = StructTypeToIndexSet.AddByHash(Hash, Hash);
StructTypesList.Add(&InStructType);
}
const int32 Index = ElementId.AsInteger();
return Index;
}
TSet<uint32> StructTypeToIndexSet;
TArray<TWeakObjectPtr<const UStruct>, TInlineAllocator<64>> StructTypesList;
}
下面是内存结果:
生成的 Archetype 的 Fragment bit 位数据如下:
下面是 CropArchetype 生成的 Fragment 的比特位
Fragment 排序规则:内存空间大小(字节大的排前面) > Fragmnet 名字
struct FScriptStructSortOperator
{
template<typename T>
bool operator()(const T& A, const T& B) const
{
return (A.GetStructureSize() > B.GetStructureSize())
|| (A.GetStructureSize() == B.GetStructureSize()
&& B.GetFName().FastLess(A.GetFName()));
}
};
最后 Fragment、Tag、SharedFragment、ChunkFragment 会合并生成该 Archetype 的 Hash 值。
struct FMassArchetypeCompositionDescriptor
{
uint32 CalculateHash() const
{
return CalculateHash(Fragments, Tags, ChunkFragments, SharedFragments);
}
static uint32 CalculateHash(const FMassFragmentBitSet& InFragments,
const FMassTagBitSet& InTags,
const FMassChunkFragmentBitSet& InChunkFragments,
const FMassSharedFragmentBitSet& InSharedFragmentBitSet)
{
const uint32 FragmentsHash = GetTypeHash(InFragments);
const uint32 TagsHash = GetTypeHash(InTags);
const uint32 ChunkFragmentsHash = GetTypeHash(InChunkFragments);
const uint32 SharedFragmentsHash = GetTypeHash(InSharedFragmentBitSet);
return HashCombine(HashCombine(HashCombine(FragmentsHash, TagsHash)
, ChunkFragmentsHash), SharedFragmentsHash);
}
}
最后注意,FMassEntityManager::Initialize 时就会找到所有类型,并初始化 FStructTracker
void FMassEntityManager::Initialize()
{
// 初始化时会创建一个 Index 0 的 Entity 作为无效 Entity
Entities.Add();
FMassFragmentBitSet Fragments;
FMassTagBitSet Tags;
FMassChunkFragmentBitSet ChunkFragments;
FMassSharedFragmentBitSet LocalSharedFragments;
for (TObjectIterator<UScriptStruct> StructIt; StructIt; ++StructIt)
{
if (StructIt->IsChildOf(FMassFragment::StaticStruct()))
{
if (*StructIt != FMassFragment::StaticStruct())
{
// TStructTypeBitSet.Add 函数,上面已经提到过了
Fragments.Add(**StructIt);
}
}
else if (StructIt->IsChildOf(FMassTag::StaticStruct()))
{
if (*StructIt != FMassTag::StaticStruct())
{
Tags.Add(**StructIt);
}
}
else if (StructIt->IsChildOf(FMassChunkFragment::StaticStruct()))
{
if (*StructIt != FMassChunkFragment::StaticStruct())
{
ChunkFragments.Add(**StructIt);
}
}
else if (StructIt->IsChildOf(FMassSharedFragment::StaticStruct()))
{
if (*StructIt != FMassSharedFragment::StaticStruct())
{
LocalSharedFragments.Add(**StructIt);
}
}
}
bInitialized = true;
}
FragmentConfigs
回到 FMassArchetypeData 中的 FragmentConfigs,这个主要是存储原型存储时,每个 chunk 上每个 Fragment 的内存偏移地址。
内存布局以及计算方式如下:
计算时减去 AlignmentPadding 这里没想明白,还要研究下。
FMassArchetypeChunk
终于来到 FMassArchetypeChunk,这个类型是用来存放 Entity。
namespace UE::Mass
{
constexpr int32 ChunkSize = 128*1024;
}
struct FMassArchetypeChunk
{
private:
/// 指向内存块的指针
uint8* RawMemory = nullptr;
/// Chunk 大小
int32 AllocSize = 0;
int32 NumInstances = 0;
int32 SerialModificationNumber = 0;
/// 保存 ChunkFragment 数据
TArray<FInstancedStruct> ChunkFragmentData;
/// 保存 SharedFragment 数据
FMassArchetypeSharedFragmentValues SharedFragmentValues;
}
Mass 在上一个版本的时候,Chunk 的大小还是 64KB,UE5.1.1 代码已经改成 128KB 了,主要是现在 CPU L2 缓存都是 128 的倍数了。
- 一级缓存:有数据 Cache 跟 指令 Cache 之分,每个 CPU 各自有一个 32KB 的 Cache
- 二级缓存:每个核心内各自有一个 512KB 的 Cache
- 三级缓存:上图的三级缓存没有写数量,表示 8 个核心共享一块三级缓存,大小是 16MB(共享的三级缓存,速度会慢很多)。
Chunk 内存示意图如下:
创建实例
有了原型后,我们就可以通过原型来创建我们需要的实例对象了:
/// 通过 ArchetypeHandle 创建单个 Entity
FMassArchetypeHandle Archetype = CropArchetype;
FMassEntityHandle NewItem = EntityManager.CreateEntity(Archetype);
/// 通过 ArchetypeHandle 批量创建 100 个 Entity
TArray<FMassEntityHandle> Entities;
EntityManager->BatchCreateEntities(Archetype, 100, Entities);
- Entity.Index : Entity 在 MassEntityManager.Entities 数组中的位置,Entity 移除时,Index 会放入 EntityFreeIndexList 里,下次创建时优先从 EntityFreeIndexList 拿出下一个可分配的 Index
- Entity.SerialNumber : MassEntityManager 维护的唯一自增 Id
下图是这几个类的关系:
查询
有了实例后,我们后续想更新,修改实例需要通过查询获取到这些 Entity,UE 提供了 UMassProcessor,我们需要查询时,只需要写一个子类继承该类,然后自己实现如下两个函数:
class UMyProcessor : public UFarmProcessorBase
{
GENERATED_BODY()
protected:
FMassEntityQuery MyQuery;
public:
UMyProcessor::UMyProcessor()
{
// 设置为 true 后会添加到工程里的 Mass 配置中.
bAutoRegisterWithProcessingPhases = true;
// 上面设置为 true 后,这里设置更新阶段
ProcessingPhase = EMassProcessingPhase::PrePhysics;
// Using the built-in movement processor group
ExecutionOrder.ExecuteInGroup = UE::Mass::ProcessorGroupNames::Movement;
// You can also define other processors that require to run
// before or after this one
ExecutionOrder.ExecuteAfter.Add(TEXT("MSMovementProcessor"));
// This executes only on Clients and Standalone
ExecutionFlags = (int32)(EProcessorExecutionFlags::Client |
EProcessorExecutionFlags::Standalone);
// This processor should not be multithreaded
bRequiresGameThreadExecution = true;
}
}
各个更新阶段的配置:
EMassProcessingPhase | Related ETickingGroup | Description |
---|---|---|
PrePhysics | TG_PrePhysics | Executes before physics simulation starts. |
StartPhysics | TG_StartPhysics | Special tick group that starts physics simulation. |
DuringPhysics | TG_DuringPhysics | Executes in parallel with the physics simulation work. |
EndPhysics | TG_EndPhysics | Special tick group that ends physics simulation. |
PostPhysics | TG_PostPhysics | Executes after rigid body and cloth simulation. |
FrameEnd | TG_LastDemotable | Catchall for anything demoted to the end. |
自定义的 Processor 还需要自己实现两个函数,首先是配置查询条件:
void UMyProcessor::ConfigureQueries()
{
// 增加 Fragment 条件
MyQuery.AddRequirement<FHitLocationFragment>(EMassFragmentAccess::ReadOnly,
EMassFragmentPresence::Optional);
// 增加 Tag 条件
MyQuery.AddTagRequirement<FMoverTag>(EMassFragmentPresence::All);
// 增加 subsystem 条件
MyQuery.AddSubsystemRequirement<UMassDebuggerSubsystem>
(EMassFragmentAccess::ReadWrite);
// 将 MyQuery 注册到 Processor 的 Query 列表中,用处不明。
MyQuery.RegisterWithProcessor(*this);
}
然后是 Processor 具体的操作:
void UMyProcessor::Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
MyQuery.ForEachEntityChunk(EntityManager, Context, [](FMassExecutionContext& Context)
{
//Loop over every entity in the current chunk and do stuff!
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
// ...
}
});
}
查询权限
查询器获取 Fragment、Subsystems 权限:
// 获取 Fragment 的读写权限
enum class EMassFragmentAccess : uint8
{
None,
ReadOnly, // 只读
ReadWrite,// 读写
MAX
};
查询器查询条件:
// 获取查询的筛选规则
enum class EMassFragmentPresence : uint8
{
All, /** All of the required fragments must be present */
Any, /** One of the required fragments must be present */
None, /** None of the required fragments can be present */
Optional, /** If fragment is present we'll use it, but it
missing stop processing of a given archetype */
MAX
};
- All: 全部满足
- Any:包含其中之一
- None:不能包含
- Optional:可有可无?
其他示例(SharedFragment、Subsystem):
void UMyProcessor::ConfigureQueries()
{
// 读写有 FTransformFragment 的实例
MyQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
// 只读访问有 FMassForceFragment 的实例
MyQuery.AddRequirement<FMassForceFragment>(EMassFragmentAccess::ReadOnly);
// 读写 SharedFragment
MyQuery.AddSharedRequirement<FClockSharedFragment>(EMassFragmentAccess::ReadWrite);
// 读写 UMassDebuggerSubsystem
MyQuery.AddSubsystemRequirement<UMassDebuggerSubsystem>(
EMassFragmentAccess::ReadWrite);
// Registering the query with UMyProcessor
MyQuery.RegisterWithProcessor(*this);
}
对应的获取函数如下表:
Type | EMassFragmentAccess | Function | Description |
---|---|---|---|
Fragment | ReadOnly | GetFragmentView | 返回包含 fragment 的只读数组 TConstArrayView |
ReadWrite | GetMutableFragmentView | 返回包含 fragment 的可读写数组 TArrayView | |
Shared Fragment | ReadOnly | GetConstSharedFragment | 返回常引用 shared fragment. |
ReadWrite | GetMutableSharedFragment | 返回引用 shared fragment. | |
Subsystem | ReadOnly | GetSubsystemChecked | 返回常引用 subsystem. |
ReadWrite | GetMutableSubsystemChecked | 返回引用 subsystem. |
查看源码可以得知,ReadOnly 跟 ReadWrite 其实没有做强限制,定义 ReadOnly 的限制,使用时调用 GetMutableFragmentView,也可以返回可修改的 View 因此使用时,需要开发者自己规范化,下面是源码部分。
/* Fragments related operations */
template<typename TFragment>
TArrayView<TFragment> GetMutableFragmentView()
{
const UScriptStruct* FragmentType = TFragment::StaticStruct();
const FFragmentView* View = FragmentViews.FindByPredicate(
[FragmentType](const FFragmentView& Element) {
return Element.Requirement.StructType == FragmentType;
});
// 官方把 check 代码注释掉的!!!!
//checkfSlow(View != nullptr, TEXT("Requested fragment type not bound"));
//checkfSlow(View->Requirement.AccessMode == EMassFragmentAccess::ReadWrite,
// TEXT("Requested fragment has not been bound for writing"));
return MakeArrayView<TFragment>((TFragment*)View->FragmentView.GetData(),
View->FragmentView.Num());
}
template<typename TFragment>
TConstArrayView<TFragment> GetFragmentView() const
{
const UScriptStruct* FragmentType = TFragment::StaticStruct();
const FFragmentView* View = FragmentViews.FindByPredicate(
[FragmentType](const FFragmentView& Element) {
return Element.Requirement.StructType == FragmentType;
});
//checkfSlow(View != nullptr, TEXT("Requested fragment type not bound"));
return TConstArrayView<TFragment>((const TFragment*)View->FragmentView.GetData(),
View->FragmentView.Num());
}
下面的代码是示例中的一个更新 Water 的处理代码:
class UFarmWaterProcessor : public UFarmProcessorBase
{
GENERATED_BODY()
protected:
FMassEntityQuery EntityQuery;
public:
UFarmProcessorBase() : EntityQuery(*this)
{
bAutoRegisterWithProcessingPhases = false;
}
/// 配置 Requirement
virtual void ConfigureQueries() override
{
EntityQuery.AddRequirement<FFarmWaterFragment>(EMassFragmentAccess::ReadWrite,
EMassFragmentPresence::All);
}
/// 执行处理操作
virtual void Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context) override
{
EntityQuery.ForEachEntityChunk(EntityManager, Context,
[this](FMassExecutionContext& Context) {
const float DeltaTimeSeconds = Context.GetDeltaTimeSeconds();
/// 获取读写视图列表
TArrayView<FFarmWaterFragment> WaterList =
Context.GetMutableFragmentView<FFarmWaterFragment>();
for (FFarmWaterFragment& WaterFragment : WaterList)
{
/// 修改 Fragment 数据
WaterFragment.CurrentWater = FMath::Clamp(WaterFragment.CurrentWater
+ WaterFragment.DeltaWaterPerSecond * DeltaTimeSeconds, 0, 1);
}
});
}
};
/// 在 Actor BeginPlay 创建 Processor
void AMassEntityTestFarmPlot::BeginPlay()
{
this.Processor = NewObject<UFarmWaterProcessor>(this);
}
/// 使用自己的 Ticker 来运行 Processor
void AMassEntityTestFarmPlot::TickActor()
{
FMassProcessingContext Context(this.EntityManager, DeltaTime);
UE::Mass::Executor::Run(this.Processor, Context);
}
// 当然也可以执行一组 Processor
// 定义个数组,将所有的 Processor 放到这个数组里
// TArray<TObjectPtr<UMassProcessor>> PerFrameSystems;
void AMassEntityTestFarmPlot::BeginPlay()
{
this.PerFrameSystems.Add(NewObject<UFarmWaterProcessor>(this));
}
void AMassEntityTestFarmPlot::TickActor()
{
FMassProcessingContext Context(this.EntityManager, DeltaTime);
UE::Mass::Executor::RunProcessorsView(this.PerFrameSystems, Context);
}
注意:使用 Processor 时我们创建的是 FMassProcessingContext,而 Processor 执行 Execute 时,参数是 FMassExecutionContext。
Tags 操作
下面是 Tags 的使用示例:
MyQuery.ForEachEntityChunk(EntityManager, Context, [](FMassExecutionContext& Context)
{
/// 判断 Archetype 是否有 Tag
if(Context.DoesArchetypeHaveTag<FOptionalTag>())
{
// I do have the FOptionalTag tag!!
}
});
修改 Entity
当 Processor 需要对 Entity 进行修改时,要使用 Defer()
/// 判断是不是成熟了
void UFarmHarvestTimerExpired::ConfigureQueries()
{
EntityQuery.AddRequirement<FHarvestTimerFragment>(EMassFragmentAccess::ReadOnly);
/// 已经有 HarvestTag 的 Entity 不需要再次计算
EntityQuery.AddTagRequirement<FFarmJustBecameReadyToHarvestTag>(
EMassFragmentPresence::None);
}
void UFarmHarvestTimerExpired::Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
QUICK_SCOPE_CYCLE_COUNTER(UFarmHarvestTimerExpired_Run);
EntityQuery.ForEachEntityChunk(EntityManager, Context,
[this](FMassExecutionContext& Context) {
const int32 NumEntities = Context.GetNumEntities();
TConstArrayView<FHarvestTimerFragment> TimerList =
Context.GetFragmentView<FHarvestTimerFragment>();
for (int32 i = 0; i < NumEntities; ++i)
{
if (TimerList[i].NumSecondsLeft == 0)
{
FMassEntityHandle Handle = Context.GetEntity(i);
Context.Defer().AddTag<FFarmJustBecameReadyToHarvestTag>(Handle);
}
}
});
}
// FMassEntityHandle EntityHandle = Context.GetEntity(EntityIndex); 获取单个实例
// auto EntityHandleArray = Context.GetEntities(); 获取全部实例
使用 Context.Defer(),其实就是返回 FMassCommandBuffer 对象,通过该对象将需要的操作生成对应的操作指令,并压入 DeferredCommandBuffer ,类似 UE 中渲染管线的实现。
Context 还提供了一些其他的操作:
/// Fragment
Context.Defer().AddFragment<FMyFragment>(EntityHandle);
Context.Defer().RemoveFragment<FMyFragment>(EntityHandle);
/// Tag
Context.Defer().AddTag<FMyTag>(EntityHandle);
Context.Defer().RemoveTag<FMyTag>(EntityHandle);
Context.Defer().SwapTags<FOldTag, FNewTag>(EntityHandle);
/// Destroying entities:
Context.Defer().DestroyEntity(EntityHandle);
Context.Defer().DestroyEntities(EntityHandleArray);
Tag 修改
从内存布局来看,Chunk 里没有给 Tag 预留控件,那要如何实现给某个具体的 Entity 修改 Tag 的呢?
答案是:创建了新的 Archetype。之前说过, Archetype 其实是由一组 Fragment、Tag、SharedFragment、ChunkedFragment 的组成的,不同的组成规则,得到的是不同的 Archetype,因此,Tag 的修改,其时是创建了新原型,然后把修改的 Entity 从之前的 ArchetypeData 中移动到新的原型中。
ForEachEntityChunk 做了啥
Processor 中的 Execute 都使用到了这个函数,那这个函数具体做了啥呢:
void FMassEntityQuery::ForEachEntityChunk(FMassEntityManager& EntityManager,
FMassExecutionContext& ExecutionContext, const FMassExecuteFunction& ExecuteFunction)
{
// 是否设置了具体的 ArchetypeHandle
if (ExecutionContext.GetEntityCollection().IsSet())
{
}
else
{
// 找到 EntityManager 下符合要求的所有 Archetype,并更新缓存
// 如果缓存不需要修改,则直接使用缓存
CacheArchetypes(EntityManager);
// CacheArchetyps 会对 Fragment 排序,因此之后才能调用该函数,
// 将当前 Query 的 Requirement 设置给 Context
ExecutionContext.SetFragmentRequirements(*this);
// 逐 Archetypes 遍历
for (int i = 0; i < ValidArchetypes.Num(); ++i)
{
const FMassArchetypeHandle& ArchetypeHandle = ValidArchetypes[i];
FMassArchetypeData& ArchetypeData =
FMassArchetypeHelper::ArchetypeDataFromHandleChecked(ArchetypeHandle);
ArchetypeData.ExecuteFunction(ExecutionContext, ExecuteFunction,
ArchetypeFragmentMapping[i], ChunkCondition);
ExecutionContext.ClearFragmentViews();
}
}
ExecutionContext.ClearExecutionData();
ExecutionContext.FlushDeferred();
}
CacheArchetypes 最重要的功能是根据 Requirement 来获取满足条件的 Archetype
// BitArray 使用 bit 位加速判断
bool HasAll(const TStructTypeBitSet& Other) const
{
return StructTypesBitArray.HasAll(Other.StructTypesBitArray);
}
void FMassEntityManager::GetValidArchetypes(const FMassEntityQuery& Query,
TArray<FMassArchetypeHandle>& OutValidArchetypes,
const uint32 FromArchetypeDataVersion) const
{
TSet<TSharedPtr<FMassArchetypeData>> AnyArchetypes;
for (const FMassFragmentRequirementDescription& Requirement :
Query.GetFragmentRequirements())
{
if (Requirement.Presence != EMassFragmentPresence::None)
{
// 将含有 Requirement 中的 Fragment、Tag 等类型 Archetype 先找出来
// FragmentTypeToArchetypeMap 加速查找
if (const TArray<TSharedPtr<FMassArchetypeData>>* pData =
FragmentTypeToArchetypeMap.Find(Requirement.StructType))
{
AnyArchetypes.Append(*pData);
}
}
}
for (TSharedPtr<FMassArchetypeData>& ArchetypePtr : AnyArchetypes)
{
FMassArchetypeData& Archetype = *(ArchetypePtr.Get());
// EMassFragmentPresence::All
if (Archetype.GetFragmentBitSet().HasAll(Query.GetRequiredAllFragments())
== false)
{
// missing some required fragments, skip.
continue;
}
// EMassFragmentPresence::None
if (Archetype.GetFragmentBitSet().HasNone(Query.GetRequiredNoneFragments())
== false)
{
// has some Fragments required to be absent
continue;
}
// EMassFragmentPresence::Any
if (Query.GetRequiredAnyFragments().IsEmpty() == false
&& Archetype.GetFragmentBitSet().HasAny(Query.GetRequiredAnyFragments())
== false)
{
continue;
}
// ... 省略 Tag、 SharedFragment、ChunkFragment 的判断
// 返回通过所有 Requirement 的原型
OutValidArchetypes.Add(ArchetypePtr);
}
}
找到所有符合要求的 Archetype 后,会逐个 Archetype 遍历,代码如下:
/// Archetype 遍历
void FMassArchetypeData::ExecuteFunction(FMassExecutionContext& RunContext,
const FMassExecuteFunction& Function,
const FMassQueryRequirementIndicesMapping& RequirementMapping,
const FMassChunkConditionFunction& ChunkCondition)
{
if (GetNumEntities() == 0)
{
return;
}
// mz@todo to be removed
RunContext.SetCurrentArchetypesTagBitSet(GetTagBitSet());
/// 逐 Chunk 遍历
for (FMassArchetypeChunk& Chunk : Chunks)
{
if (Chunk.GetNumInstances())
{
/// ...
if (!ChunkCondition || ChunkCondition(RunContext))
{
/// 生成 Fragment 的 ViewList
BindEntityRequirements(RunContext, RequirementMapping.EntityFragments,
Chunk, 0, Chunk.GetNumInstances());
Function(RunContext);
}
}
}
}
BindEntityRequirements 的作用就是找到当前 Chunk 中对应 Requirement 中的 Fragment 对应的内存空间,然后开始执行 Processor::Excute 后面就可以通过 GetMutableFragmentView 获取对应 Fragment 数组。
上图展示了 Requirement 为 FFarmWaterFragment 执行步骤:
- 首先找到了两个符合要求的 Archetype
- 然后遍历 ArchetypeCorp 中所有的 chunk,将 Context 中的是 FragmentView 绑定到对应的内存地址,然后执行 Processor::Excute