MassEntity 学习
MassEntity 是 UE5 里面一个面向数据的框架,与 Unity DOTS 非常相似。 引入 ECS 的主要目的也是要充分利用多核 CPU 的并行能力,发挥缓存的优势,从而提高性能。
概念简介
这里针对虚幻引擎中提出的相关概念进行介绍,实际上发现跟 Unity DOTS 有很多重合的地方。
-
Entity。
即是 ECS 中的 Entity,类似于 OOP 中的对象。
-
Fragment。
其实是 ECS 中的 Component,一个 Entity 会由若干个 Fragment 组合而成。
-
Archetype。
跟 Unity DOTS 中提出的 Archetype 基本上差不多。 一个 Archetype 即是拥有相同 Fragment 组合的实体集合。 相当于根据 Fragment 组合的不同而对这些实体进行分类。
-
Chunk。
太典了,Unity 中也有类似的概念。 Archetype 中的实体是按照一个一个的 Chunk 组织起来的。 与 Unity 一样,UE 中 Chunk 的目的也是为了发挥缓存优势,提高在同一个 Archetype 中连续访问 Fragment 时的性能。
-
Processor。
起到 ECS 中 System 的作用。 是一个仅针对 Fragment 处理逻辑的无状态类。
-
EntityQuery。
也很眼熟,跟 Unity 里的差不多,目的也是为了筛选出特定的 Entity。 Processor 使用它筛选出合适的 Entity 之后就可以遍历所需的组件执行它的逻辑了。
-
Tag。
可以理解成一个不含数据的 Fragment。 可以用于特殊的筛选条件。 因为不含数据,所以引擎可以对它做一些特殊优化。
-
ChunkFragment。
是一种蛇皮优化,与 Entity 无关,每个 Chunk 上都会有一个这玩意。 可以利用这玩意在 Chunk 做一些统计性的处理。 文档说它属于实体的组合的一部分,我猜测它想说的是 Archetype 的一部分,但是没资料没啃源码我也不敢断定。
-
Trait。
是一些相关性较高的 Fragment 的组合,应该可以理解成对 Fragment 进行某种意义上的分组。
Hello World
这玩意跟 Unity 的差不多,直接莽就完了。
参考资料
Large Numbers of Entities with Mass in UE5 | Feature Highlight | State of Unreal 2022
安装插件
这堆东西目前还是实验性的,想要在 UE5 中体验 MassEntity,需要先安装相应的插件。
在菜单中依次选择 Edit -> Plugins,进行搜索,添加 MassAI
、MassCrowd
、MassEntity
、MassGameplay
,随后根据提示重启 UE5。
创建预设体
在内容浏览器中创建一个 DataAsset
,其中类选择 MassEntityConfigAsset
。
这玩意类似于配置一种预设体,根据这个东西可以实例化出具体的 Entity。
然后双击打开,往 Traits 中添加一个 Debug Visualization。
其中网格体随便选一个以便于生成实例之后观察。
Entity 生成器
在关卡中添加一个 MassSpawner
用于根据预设体实例化出一些 Entity。
设置生成数量为 100 个。
Entity Types 用于指定生成的 Entity 类型,给它添加一个元素,Entity Config 设置为刚刚创建的 DataAsset
。
Spawn Data Generators 用于指定 Entity 生成器,生成器决定 Entity 的生成位置,给它添加一个元素,类型指定为 EQS SpawnPoints Generator,Query Template 点进去直接创建新的资源并使用。
记得勾上 Auto Spawn on Begin Play。
最后启动即可观察到这个 MassSpawner
附近生成了 100 个 Entity。
自定义 Trait
在内容浏览器中创建一个 C++ 类,父类选择 MassEntityTraitBase
。
于是我们就考虑自定义一个 Fragment,并给它添加到这个 Trait 中。
Fragment 是一个结构体,所以我们直接在刚刚定义的 Trait 前面定义 Fragment 就行了。
接着重载 UMassEntityTraitBase
的一个方法,就可以把这个 Fragment 加进去。
最后大概长这样
#pragma once
#include "CoreMinimal.h"
#include "MassEntityTypes.h"
#include "MassEntityTraitBase.h"
#include "SimpleRandomMovementTrait.generated.h"
USTRUCT()
struct FSimpleMovementFragment : public FMassFragment
{
GENERATED_BODY()
FVector Target;
};
UCLASS()
class HELLOWORLD_API USimpleRandomMovementTrait : public UMassEntityTraitBase
{
GENERATED_BODY()
protected:
virtual void BuildTemplate(FMassEntityTemplateBuildContext &BuildContext, UWorld &World) const override;
};
#include "SimpleRandomMovementTrait.h"
#include "MassEntityTemplateRegistry.h"
void USimpleRandomMovementTrait::BuildTemplate(FMassEntityTemplateBuildContext &BuildContext, UWorld &world) const
{
BuildContext.AddFragment<FSimpleMovementFragment>();
}
有可能会报链接错误,可以参考这个去修改相应的 XXX.Build.cs
文件添加相应的依赖模块。
自定义 Processor
在内容浏览器中创建一个 C++ 类,父类选 MassProcessor,构造函数里赋值一些属性,再给它重载几个虚函数。 跟 Unity 非常类似,往虚函数里摁填就行了,单看函数名就知道它想干什么。
#pragma once
#include "CoreMinimal.h"
#include "MassEntityQuery.h"
#include "MassProcessor.h"
#include "SimpleRandomMovementProcessor.generated.h"
UCLASS()
class HELLOWORLD_API USimpleRandomMovementProcessor : public UMassProcessor
{
GENERATED_BODY()
public:
USimpleRandomMovementProcessor();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(UMassEntitySubsystem &EntitySubsystem, FMassExecutionContext &Context) override;
private:
FMassEntityQuery EntityQuery;
};
#include "SimpleRandomMovementProcessor.h"
#include "MassCommonFragments.h"
#include "SimpleRandomMovementTrait.h"
USimpleRandomMovementProcessor::USimpleRandomMovementProcessor()
{
bAutoRegisterWithProcessingPhases = true;
ExecutionFlags = (int32)EProcessorExecutionFlags::All;
}
void USimpleRandomMovementProcessor::ConfigureQueries()
{
EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FSimpleMovementFragment>(EMassFragmentAccess::ReadWrite);
}
void USimpleRandomMovementProcessor::Execute(UMassEntitySubsystem &EntitySubsystem, FMassExecutionContext &Context)
{
EntityQuery.ForEachEntityChunk(EntitySubsystem, Context, [this](FMassExecutionContext &Context) {
const TArrayView<FTransformFragment> TransformList = Context.GetMutableFragmentView<FTransformFragment>();
const TArrayView<FSimpleMovementFragment> SimpleMovementList =
Context.GetMutableFragmentView<FSimpleMovementFragment>();
const float WorldDeltaTime = Context.GetDeltaTimeSeconds();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); EntityIndex++)
{
FTransform &Transform = TransformList[EntityIndex].GetMutableTransform();
FVector &MoveTarget = SimpleMovementList[EntityIndex].Target;
FVector CurrentLocation = Transform.GetLocation();
FVector TargetVector = MoveTarget - CurrentLocation;
if (TargetVector.Size() <= 20.f)
MoveTarget = FVector(FMath::RandRange(-1.f, 1.f) * 1000.f, FMath::RandRange(-1.f, 1.f) * 1000.f,
CurrentLocation.Z);
else
Transform.SetLocation(CurrentLocation + TargetVector.GetSafeNormal() * 400.f * WorldDeltaTime);
}
});
}
修改预设体
往之前创建的 DataAsset
的 Traits 里加上我们自定义的那个 Simple Random Movement Trait 就行了。
运行结果
再次运行,就发现这些 Entity 按照预期在 xjb 乱动。