文章

UE5 MassEntity

Data-Oriented Design - 数据导向的设计

MassEntity is a gameplay-focused framework for data-oriented calculations. ==> DOTS

  • 其他架构设计优先考虑开发便利性而非效率
  • 如何充分利用现代硬件性能:
    • Cache Memory 的访问效率比 主RAM的效率快几个量级
    • 避免 Cache misses 的发生
    • 使用连续的数据块填充 Cache Memory
    • 打包处理相似的数据块

ECS术语 - Mass

ECSMass
EntityEntity
ComponentFragment
SystemProcessor

Mass 关键术语

术语解释
EntityMass的基类,持有指向所有Fragment的指针。
Fragment用紧凑数组存储实体的数据/状态(例如,变换、速度、当前LOD等)。
Archetype具有相同 Fragment 组成的实体被归为一组。
Entity 组成可以在运行时改变,导致 Archetype 迁移。
Trait将 Fragment 归为一组,通常代表 Entity 的特性(例如,移动、区域图导航、智能对象用户)。
Tags原型级别的、无数据的片段,可用于基于其存在或不存在来筛选原型的查询
Chunk Fragment应用于原型中一部分实体的片段,而不是直接应用于单个实体。
Shared Fragment用于内存优化的多个 Entity 共享的片段数据。
ProcessorMass中逻辑执行发生的地方。
Processor 可以更改 Fragment 的值以及 Entity 的组成(添加或删除)。
Entity QueryProcessor 使用的基于 Fragment 、Tags 要求筛选 Archetype 的查询。
Entity Query 返回符合条件的所有 Entity 。
Mass Spawner在运行时向关卡添加 Entity 的系统。
Mass Entity Config定义要生成的Mass代理的资产,通过指定 Entity 的特征(Trait)

在 Mass - ECS 中,

  • entity 仅由 fragments 组成,entityfragments 都是纯数据元素,不包含任何逻辑;

  • entity 实际上只是一个指向某些fragments的小的唯一标识符(简单的整数ID);

  • Processor 是无状态类,为fragments提供处理逻辑;

    img

    • 定义查询,执行数据运算;
    • eg. “movement” Processor : 可以查询拥有 transform velocity entity
  • archetypes 指 内存中相同fragment 紧密排列的数组,这确保了从内存中检索与相同原型的实体关联的片段时的最佳性能;

    img

Archetype - 原型

Enity是Fragment和Tag的独特组合,这些组合称为 Archetype

大众原型定义

每个原型(FMassArchetypeData)使用 BitSet 来记录 Tag信息(TScriptStructTypeBitSet<FMassTag>),而位集中的每个位代表原型中是否存在标签。

Fragment 在 Archetype 的内存组织形式

  • 在 Archetype 中使用 TArray<FMassArchetypeChunk> 持有 fragment 数据
  • 数据的组织方式采用 SoA Structure of arrays

MassArchetypeChunks

内存布局示意

MassArchetypeMemory

分块的数据布局设计,使得允许大量的 整体Entity 放入 CPU Cache;

  • 如果在迭代实体时访问A 片段,则线性方法会很快
  • 但是,通常,当我们迭代实体时,倾向于访问多个片段,因此将它们全部放在缓存中会更高效

海量原型缓存

Entity’s How to

Fragment 定义

  • FMassFragment 定义的数据是每个实体各自拥有;
  • FMassSharedFragment 定义的数据可以在多个实体间共享,通常用于一组实体通用的配置;
  • FMassTag 可以看成是不带数据的Fragment,仅用于 Processer的查询过滤;
1
2
3
4
5
6
7
8
9
10
11
12
13
USTRUCT()
struct MASSBOIDSGAME_API FBoidsLocationFragment : public FMassFragment
{
	GENERATED_BODY()
	
	UPROPERTY()
	FVector Location;

	FBoidsLocationFragment()
		: Location(ForceInitToZero)
	{
	}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
USTRUCT(BlueprintType)
struct MASSBOIDSGAME_API FBoidsMeshFragment : public FMassSharedFragment
{
	GENERATED_BODY()

	/** The render mesh for a boid */
	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	UStaticMesh* BoidMesh;

	FBoidsMeshFragment()
		: BoidMesh(nullptr)
	{
	}
};

运行时创建与销毁

最原始的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Get EntityManager
TSharedPtr<FMassEntityManager> EntityManager = World->GetSubsystem<UMassEntitySubsystem>()->GetMutableEntityManager().AsShared();

// CreateArchetype
FMassArchetypeHandle MoverArchetype =  EntityManager->CreateArchetype(
{
    FTransformFragment::StaticStruct(),
    FMassVelocityFragment::StaticStruct()
});

// CreateEntity by Archetype
FMassEntityHandle NewEntity = EntityManager->CreateEntity(MoverArchetype);

// Add Tag
EntityManager->AddTagToEntity(NewEntity,FMSGravityTag::StaticStruct());
// Add Fragment
EntityManager->AddFragmentToEntity(NewEntity,FSampleColorFragment::StaticStruct());


// changing NewEntity's data on a fragment
EntityManager->GetFragmentDataChecked<FMassVelocityFragment>(NewEntity).Value = FMath::VRand()*100.0f;
EntityManager->GetFragmentDataChecked<FSampleColorFragment>(NewEntity).Color = FColor::Blue;

// Destroy
EntityManager->BatchDestroyEntityChunks(Collection);

延迟创建 <常用/CommandBuffer>

  • 可以通过添加或删除片段或标签来更改实体的组成。但是,在处理实体时更改实体的组成将导致该实体从一种原型移动到另一种原型。

  • 在当前处理批次结束时进行批处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// We reserve an entity here, this way the system knows not to give this index out to other processors/deferred actions etc
FMassEntityHandle ReserverdEntity = EntityManager->ReserveEntity();

FTransformFragment MyTransformFragment;
MyTransformFragment.SetTransform(FTransform::Identity);

FSampleColorFragment MyColorFragment;

// We build a new entity and add fragments to it in one command!
EntityManager->Defer().PushCommand<FMassCommandBuildEntity>(ReserverdEntity,MyColorFragment);


// Flush the commands so this new entity is actually around
EntityManager->FlushCommands();


// Sets fragment data on an existing entity
EntityManager->Defer().PushCommand<FMassCommandAddFragmentInstances>(ReserverdEntity,MyColorFragment,MyTransformFragment);


// Reserve yet another entity...
ReserverdEntity = EntityManager->ReserveEntity();

FMSExampleSharedFragment SharedFragmentExample;
SharedFragmentExample.SomeKindaOfData = FMath::Rand() * 10000.0f;
FMassArchetypeSharedFragmentValues SharedFragmentValues;

// This is what traits use to create their shared fragment info as well
FConstSharedStruct& SharedFragmentSharedStruct = EntityManager->GetOrCreateConstSharedFragment(SharedFragmentExample);
SharedFragmentValues.AddConstSharedFragment(SharedFragmentSharedStruct);

EntityManager->Defer().PushCommand<FMassCommandBuildEntityWithSharedFragments>(ReserverdEntity, MoveTemp(SharedFragmentValues), MyTransformFragment, MyColorFragment);

// Destroy
EntityManager->Defer().DestroyEntities(Entities);
EntityManager->Defer().DestroyEntity(Entity);

Processor’s How to

  • 过滤符合条件的数据实体
  • 同时处理这块被打包的内存 => 这块内存是连续且相同类型,避免 Cache miss 的发生

Processor 处理数据的流程

并不是一次性处理所有Entity,而是在原型中分块处理Entity的。

执行步骤:

  1. 每个 Processor 首先配置 Entity Query
    • 配置需要的 Entity 特征, 如 Tags、Fragments、 SharedFragments、ChunkFragments 、 SubSystems。
  2. 然后,Processor 通过调用 ForEachEntityChunk 批量更新 实体块
  3. MassEntityQuery 将要求与原型匹配,并可以根据块片段过滤器筛选匹配原型的块。
    • 虽然要求通常是在原型上存在标签或片段,但它们也可以用于选择没有指定标签和片段的原型。
  4. 在过滤后,Mass实体查询触发每个实体块上的一个函数,通过 FMassExecutionContext 可以访问块内的单个实体。
    • 插件代码在执行 ForEachEntityChunk 时,大部分都使用lambda表达式。

通过这个执行流程,

  • Mass处理器能够高效地处理大量实体,并对实体进行逐块更新。
  • 同时确保处理器能够根据特定要求选取和操作实体。

如何注册 & 指定Processor的执行顺序

  • bAutoRegisterWithProcessingPhases

    • 方式1: Project Settings => Mass => Module Settings

      16969249382791696924937785.png

    • 方式2:Config/DefaultMass.ini

      1
      2
      
      [/Script/MassRepresentation.MassRepresentationProcessor]
      bAutoRegisterWithProcessingPhases=True
      
  • ExecutionFlags
  • ExecuteAfter、ExecuteBefore
  • ExecuteInGroup
  • bRequiresGameThreadExecution 是否需要在主线程执行,Processer默认是多线程的
1
2
3
4
5
6
7
8
9
10
11
12
UBoidsBoundsProcessor::UBoidsBoundsProcessor(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	bAutoRegisterWithProcessingPhases = true;
	ExecutionFlags = (int32)EProcessorExecutionFlags::All;
    ProcessingPhase = EMassProcessingPhase::PrePhysics;
	ExecutionOrder.ExecuteAfter.Add(UBoidsRuleProcessor::StaticClass()->GetFName());
	ExecutionOrder.ExecuteBefore.Add(UBoidsRenderProcessor::StaticClass()->GetFName());
	ExecutionOrder.ExecuteInGroup = MassBoidsGame::ProcessorGroupNames::Boids;
    // This processor should not be multithreaded
    bRequiresGameThreadExecution = true;
}
1
2
3
4
5
6
7
8
9
10
11
UENUM()
enum class EMassProcessingPhase : uint8
{
	PrePhysics,
	StartPhysics,
	DuringPhysics,
	EndPhysics,
	PostPhysics,
	FrameEnd,
	MAX,
};
1
2
3
4
5
6
7
8
9
10
11
12
UENUM(meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
enum class EProcessorExecutionFlags : uint8
{
	None = 0 UMETA(Hidden),
	Standalone = 1 << 0,
	Server = 1 << 1,
	Client = 1 << 2,
	Editor = 1 << 3,
	AllNetModes = Standalone | Server | Client UMETA(Hidden),
	All = Standalone | Server | Client | Editor UMETA(Hidden)
};
ENUM_CLASS_FLAGS(EProcessorExecutionFlags);

如何配置Processer处理的 Entity 查询集合

  • EMassFragmentAccess - 指定数据的访问方式
  • EMassFragmentPresence - 指定 fragment 在 Entity 的筛选规则
  • 可以使用 Tag 来设计一些业务层面的查询隔离
1
2
3
4
5
6
7
8
9
void UBoidsBoundsProcessor::ConfigureQueries()
{
    // <BoidsBoundsProcessor.h>  FMassEntityQuery Entities ;
	Entities
		.AddRequirement<FBoidsLocationFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::All)
		.AddRequirement<FMassVelocityFragment>(EMassFragmentAccess::ReadWrite, EMassFragmentPresence::All)
        .AddTagRequirement<FBoidsSpawnTag>(EMassFragmentPresence::Optional);
	Entities.RegisterWithProcessor(*this);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
UENUM()
enum class EMassFragmentAccess : uint8
{
	/** no binding required */
	None, 

	/** We want to read the data for the fragment */
	ReadOnly,

	/** We want to read and write the data for the fragment */
	ReadWrite,

	MAX
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UENUM()
enum class EMassFragmentPresence : uint8
{
	/** All of the required fragments must be present */
	All,

	/** One of the required fragments must be present */
	Any,

	/** None of the required fragments can be present */
	None,

	/** If fragment is present we'll use it, but it missing stop processing of a given archetype */
	Optional,

	MAX
};

对查询后的Entity执行业务逻辑

在 UE 5.2 版本中 Mass 的框架废弃了 ParallelForEachEntityChunk 的 支持,如有并行话的需求需要自己处理好数据的关联,并使用 ParallelFor;

  • 此时被处理的这些 Entity在内存中是连续的
  • 注意 : 返回的数据顺序并不是稳定的,因此不能缓存 idx的方式来记录某个特征
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void UBoidsBoundsProcessor::Execute(FMassEntityManager& EntitySubsystem, FMassExecutionContext& Context)
{
	QUICK_SCOPE_CYCLE_COUNTER(STAT_BoidsBoundsProcessor);
	
	Entities.ForEachEntityChunk(EntitySubsystem, Context, [this] (FMassExecutionContext& Context)
	{
		const TArrayView<FMassVelocityFragment> Velocities = Context.GetMutableFragmentView<FMassVelocityFragment>();
		const TConstArrayView<FBoidsLocationFragment> Locations = Context.GetFragmentView<FBoidsLocationFragment>();

		const int32 NumEntities = Context.GetNumEntities();
		const float& TurnRate = BoidsSettings->TurnBackRate;

		ParallelFor(NumEntities, [this, &Velocities, &Locations, &TurnRate](int32 Ndx)
			{
				const FVector& Location = Locations[Ndx].Location;
				FVector& Velocity = Velocities[Ndx].Value;

				// ...
			});
	});
}

MassGamePlay Framework

相关的插件

  • Mass Entity
  • Mass AI
  • Mass GamePlay
  • Mass Crowd

image-20231009112245039

Mass是如何设计 数据导向 的架构?

  • Mass 会管理 Entity 的不同代理;
  • Entity 会有 若干个 Trait (特征)
    • 可以把这些 Entity 看成是轻量级的Actor
    • Trait 看成是轻量级的 Component
    • 以上说法不严谨,因为 Entity 和Trait 没有函数,只用定义
  • 一系列的Trait 组成的 Entity 被称为 Archetype (原型)
  • 实际的数据存放在 Fragment (片段)
  • Fragment 数据处理业务在 Processor 的函数中被执行

16884637104601688463709522.png

如何使用 Mass GamePlay框架

  • MassSpawner
    • Entity Types
      • MassEntityConfigAsset
        • MassEntityTraitBase
    • Spawn Data Generators
      • MassEntitySpawnDataGeneratorBase

Trait

蓝图配置

MassEntityConfigAsset

在 Mass 提供的许多内置特征中

我们可以找到该 Assorted Fragments 特征

它包含一个数组,FInstancedStruct可以从编辑器向该特征添加片段,而无需创建新的 C++ Trait 类。

Assorted Fragments

C++ 新增 Trait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// ---- .h
UCLASS(BlueprintType)
class MASSBOIDSGAME_API UBoidsTrait : public UMassEntityTraitBase
{
	GENERATED_BODY()
	
	UPROPERTY(Category="Boids", EditAnywhere)
	FBoidsSpeedFragment Speed;

	UPROPERTY(Category="Boids", EditAnywhere)
	FBoidsMeshFragment Mesh;
	
	// ~ begin UMassEntityTraitBase interface
	//virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, UWorld& World) const override;
	virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const override;
    virtual void ValidateTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World)const override;
	// ~ end UMassEntityTraitBase interface
};

//----- .cpp

void UBoidsTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const
{
	//UMassEntitySubsystem* EntitySubsystem = UWorld::GetSubsystem<UMassEntitySubsystem>(&World);
	//check(EntitySubsystem);

	TSharedPtr<FMassEntityManager> EntityManager = World.GetSubsystem<UMassEntitySubsystem>()->GetMutableEntityManager().AsShared();

	BuildContext.AddTag<FBoidsSpawnTag>();
	BuildContext.AddFragment<FBoidsLocationFragment>();
	BuildContext.AddFragment<FMassVelocityFragment>();
	BuildContext.AddFragment(FConstStructView::Make(Speed));

	// Mesh Shared Fragment
	{
		const uint32 SharedHash = UE::StructUtils::GetStructCrc32(FConstStructView::Make(Mesh));
		//const FConstSharedStruct SharedFragment = EntitySubsystem->GetOrCreateConstSharedFragment(SharedHash, Mesh);
		const FConstSharedStruct SharedFragment = EntityManager->GetOrCreateConstSharedFragmentByHash(SharedHash, Mesh);

		BuildContext.AddConstSharedFragment(SharedFragment);
	}
}
//为特征提供自定义验证代码
void UBoidsTrait::ValidateTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const
{
	// If BoidMesh is null, show an error!
	if (!Mesh.BoidMesh)
	{
		UE_VLOG(&World, LogMass, Error, TEXT("BoidMesh is null!"));
		return;
	}
}

Mass Debugger

相对于 Unity的DebuggerWindows 提供的信息较为简陋,若架构设计中重度依赖需要扩展相关信息的展示于收集便于性能分析

Archetypes

  • EntitiesCount
  • EntitiesCountPerChunk
    • 每个Chunk可以容纳的Entity数量
  • ChunksCount
  • Allocated memory
  • Fragments、Tags、Shared Fragments
  • 总原型数量

16969113972291696911396373.png

Processors

  • Query的相关信息
    • 绿色表示 要求 fragment 是读写访问,灰色为只读
  • Processer 间的依赖关系

16969116132311696911612426.png

Processing Graphs

  • Processer 运行的时机分组
  • 当前Processer的运行路径

16969114352361696911435181.png

Replication

这部分的资料相对较少

主要通过 CalculateClientReplication 函数传入 负责CRUD的回调函数 (Create、Read、Update、Delete)

相关源码参照

  • UMassReplicatorBase
    • UMassCrowdReplicator

16969108782291696910877372.png

MassReplication相关文件

img


编程范式 DOD vs OOP

 DOD(数据驱动设计,Data-Oriented Design)OOP(面向对象编程,Object-Oriented Programming)
研发思路1. 分析问题,确定需要处理的数据和数据之间的关系。

2. 设计数据结构,以适应硬件特性,如处理器缓存和内存访问模式。

3. 编写操作数据的函数,实现高效的数据处理。

4. 将数据和函数组织成模块,实现程序的模块化。

5. 测试和优化代码,确保程序性能达到预期。
1. 分析现实世界中的实体和它们之间的关系。
2. 定义类和对象,将现实世界中的实体映射到程序中。
3. 设计类的属性和方法,实现封装、继承和多态等概念。
4. 编写代码,实现类和对象之间的交互。
5. 测试和维护代码,确保程序正常运行。
关注点数据间的关系
优化数据的存储和访问
将问题分解为数据结构和操作
对现实世界中的事物进行建模
通过类和对象来表示现实世界中的实体
强调封装、继承、多态、代码复用
数据组织数据和操作数据的方法分离
数据以紧凑的、连续的内存块存储
数据和方法是紧密耦合 » 可读性和可维护性
性能数据被按照访问模式进行分组,以便实现连续的内存访问和更好的缓存利用率对象创建和销毁的开销、虚拟函数调用的开销
CacheMissing、内存碎片
业务场景高性能、计算密集型,比如渲染、物理模拟等复杂业务问题的建模,比如游戏中的角色、物品等
劣势&挑战可读性和可维护性较低
业务场景不易建模
代码复用性低
性能开销
内存问题
过渡封装
本文由作者按照 CC BY 4.0 进行授权