MassEntity 是 UE5 里面一个面向数据的框架,与 Unity DOTS 非常相似。 引入 ECS 的主要目的也是要充分利用多核 CPU 的并行能力,发挥缓存的优势,从而提高性能。

概念简介

MassEntity Overview

这里针对虚幻引擎中提出的相关概念进行介绍,实际上发现跟 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,进行搜索,添加 MassAIMassCrowdMassEntityMassGameplay,随后根据提示重启 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 乱动。