ue5 Mass 框架简介

UE5 Mass 框架介绍

UE5Mass 分为几个库,都是以插件的形式放在引擎源码中,默认是关闭的,目前还是试验性插件。

官方给出了示范工程:城市示例 1,大致模块组织如下:

其中 MassEntity 是这个 Mass 框架的基础,这里先从 MassEntity 库开始。

MassEntity

在介绍 MassEntity 之前,先引入几个概念:

  • Archetype:原型,可以类比面向对象编程里的类定义,包含 FragmentTag
  • Entity:使用原型创建的实例对象,可以类比类实例化后的对象
  • Fragment:每个原型的数据片段的定义
  • Tag:原型的标签,不包含数据
  • Handle:句柄,MassEntity 中有两种句柄:ArchetypeHandleEntityHandle
  • 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

FragmenetEntity 的数据部分,继承基类 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 组成成分的类结构,包括上面的:FragmentTagTrait,他们的关系如下图:

创建 Archetype

首先我们需要先创建 Archetype,通过使用 FMassEntityManager 提供的接口,我们可以根据我们现有的 FragmentTag 创建出我们需要的 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 也是返回句柄,在使用中,句柄跟 ArchetypeEntity 的对应关系如下,

原型类详细定义:

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,它用来存储 MassArchetypeFragmentTagsChunkFragmentSharedFragment 的信息,并且根据这些信息生成一个 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;
}

下面是内存结果:

生成的 ArchetypeFragment 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()));
    }
};

最后 FragmentTagSharedFragmentChunkFragment 会合并生成该 ArchetypeHash 值。

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 的倍数了。

  1. 一级缓存:有数据 Cache 跟 指令 Cache 之分,每个 CPU 各自有一个 32KB 的 Cache
  2. 二级缓存:每个核心内各自有一个 512KB 的 Cache
  3. 三级缓存:上图的三级缓存没有写数量,表示 8 个核心共享一块三级缓存,大小是 16MB(共享的三级缓存,速度会慢很多)。

Chunk 内存示意图如下:

创建实例

有了原型后,我们就可以通过原型来创建我们需要的实例对象了:

/// 通过 ArchetypeHandle 创建单个 Entity
FMassArchetypeHandle Archetype = CropArchetype;
FMassEntityHandle NewItem = EntityManager.CreateEntity(Archetype);

/// 通过 ArchetypeHandle 批量创建 100 个 Entity
TArray<FMassEntityHandle> Entities;
EntityManager->BatchCreateEntities(Archetype, 100, Entities);
  1. Entity.Index : EntityMassEntityManager.Entities 数组中的位置,Entity 移除时,Index 会放入 EntityFreeIndexList 里,下次创建时优先从 EntityFreeIndexList 拿出下一个可分配的 Index
  2. Entity.SerialNumber : MassEntityManager 维护的唯一自增 Id

下图是这几个类的关系:

查询

有了实例后,我们后续想更新,修改实例需要通过查询获取到这些 EntityUE 提供了 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;
    }
}

各个更新阶段的配置:

EMassProcessingPhaseRelated ETickingGroupDescription
PrePhysicsTG_PrePhysicsExecutes before physics simulation starts.
StartPhysicsTG_StartPhysicsSpecial tick group that starts physics simulation.
DuringPhysicsTG_DuringPhysicsExecutes in parallel with the physics simulation work.
EndPhysicsTG_EndPhysicsSpecial tick group that ends physics simulation.
PostPhysicsTG_PostPhysicsExecutes after rigid body and cloth simulation.
FrameEndTG_LastDemotableCatchall 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
};
  1. All: 全部满足
  2. Any:包含其中之一
  3. None:不能包含
  4. Optional:可有可无?

其他示例(SharedFragmentSubsystem):

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

对应的获取函数如下表:

TypeEMassFragmentAccessFunctionDescription
FragmentReadOnlyGetFragmentView返回包含 fragment 的只读数组 TConstArrayView
ReadWriteGetMutableFragmentView返回包含 fragment 的可读写数组 TArrayView
Shared FragmentReadOnlyGetConstSharedFragment返回常引用 shared fragment.
ReadWriteGetMutableSharedFragment返回引用 shared fragment.
SubsystemReadOnlyGetSubsystemChecked返回常引用 subsystem.
ReadWriteGetMutableSubsystemChecked返回引用 subsystem.

查看源码可以得知,ReadOnlyReadWrite 其实没有做强限制,定义 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 其实是由一组 FragmentTagSharedFragmentChunkedFragment 的组成的,不同的组成规则,得到的是不同的 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 数组。

上图展示了 RequirementFFarmWaterFragment 执行步骤:

  1. 首先找到了两个符合要求的 Archetype
  2. 然后遍历 ArchetypeCorp 中所有的 chunk,将 Context 中的是 FragmentView 绑定到对应的内存地址,然后执行 Processor::Excute

参考

1.城市示例

2.UE5的ECS:MASS 框架

3.《WorkWithUE5》CitySampleZoneGraph剖析

4.UE5 MassEntity简易实践

5.Community Mass Sample