UE5.8 Mass Framework 专题系列

UE5.8 Mass 专题(二):EntityManager、CommandBuffer 与结构变更

讲 FMassEntityManager 的实体生命周期、Archetype 查找、Create/Destroy、批量构建、Defer、FMassCommandBuffer 和结构变更时机。

总览

FMassEntityManager 是 Mass 实体世界的核心账本。它负责创建实体、销毁实体、管理 Archetype、批量构建、查找匹配 Archetype,并把实体从一个组成迁移到另一个组成。源码注释里明确说实体存储在 chunked array 中,每个有效实体会被分配到一个 Archetype,Fragment 数据由该 Archetype 管理。

日常项目里最容易踩坑的是结构变更:加 Fragment、删 Fragment、加 Tag、删 Tag 都可能改变实体所属 Archetype。如果你正在 Query 遍历某个 Chunk,同时立刻迁移实体,就会破坏当前遍历的稳定性。因此 Mass 提供 FMassCommandBufferDefer() 模式,把结构变更延迟到安全点统一执行。

UE5.8 Mass 专题(二):EntityManager、CommandBuffer 与结构变更 配图
结构变更会改变实体所在 Archetype,生产代码应尽量通过 Defer 命令缓冲在安全点批量提交。

源码依据

MassEntityManager.h 提供 CreateArchetypeGetOrCreateSuitableArchetypeGetArchetypeForEntityGetMatchingArchetypes 等接口。MassCommandBuffer.h 的注释强调同一个 command buffer 只能由 owner thread push command,flush 时不能继续 push。UE5.8 里部分 RuntimeCheck 便利接口已 deprecated,命令执行接口也提示未来使用 Run 替代旧 Execute,说明结构变更 API 正在继续收紧边界。

生命周期心智模型

操作 是否结构变更 推荐路径
创建实体 Spawner、Template、EntityManager 批量创建
销毁实体 Defer destroy 或安全点批量销毁
写 Fragment 字段 Query 中取得 MutableFragmentView 后直接写
添加 Tag Context.Defer().AddTag<T>()
移除 Fragment Context.Defer().RemoveFragment<T>()
设置 Shared Fragment 可能改变 Archetype 构建模板时配置,运行时谨慎

架构分析

EntityManager 的成本分两类:字段写入成本和组成迁移成本。字段写入通常只是写当前 Chunk 的数组元素;组成迁移则要把实体从旧 Archetype 搬到新 Archetype,并维护内部索引、观察者通知和缓存失效。项目里要尽量让实体长时间停留在少数稳定 Archetype 中,把短期状态做成字段,而不是频繁加减 Tag。

CommandBuffer 的价值不只是“晚点执行”,还包括批处理、顺序一致性和观察者触发。比如一个 Processor 标记“到达柜台”的实体时,只 Defer 添加 FNeedsCheckoutTag;当前阶段结束后统一变更,Observer 再负责初始化结账数据。这样比 Processor 内直接创建对象、改 Tag、发信号更可控。

使用案例

下面的 Processor 在顾客到达目标后延迟添加 Tag,让后续 Observer 或 Processor 接管排队逻辑:

void UShopperArrivalProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
    EntityQuery.ForEachEntityChunk(EntityManager, Context, [this](FMassExecutionContext& Context)
    {
        const TConstArrayView<FTransformFragment> Transforms = Context.GetFragmentView<FTransformFragment>();
        const TArrayView<FShopperIntentFragment> Intents = Context.GetMutableFragmentView<FShopperIntentFragment>();

        for (int32 Index = 0; Index < Context.GetNumEntities(); ++Index)
        {
            const float DistSq = FVector::DistSquared(
                Transforms[Index].GetTransform().GetLocation(),
                Intents[Index].TargetLocation);

            if (DistSq < FMath::Square(80.0f))
            {
                Intents[Index].Intent = 1;
                Context.Defer().AddTag<FShopperNeedsQueueTag>(Context.GetEntity(Index));
            }
        }
    });
}

注意代码只直接写普通 Fragment 字段,结构变更通过 Context.Defer() 走命令缓冲。这样当前 Query 的 Chunk 视图不会被立刻破坏。

项目落地

建议把结构变更集中到少数 Processor 或 Observer。比如“生命周期类 Processor”负责生成、销毁、进入/退出状态;“模拟类 Processor”只写高频字段;“表现类 Processor”只根据 LOD 和表示类型请求 Actor 或 ISM。团队调试时可以先查结构变更数量,再查普通 Tick 耗时,很多 Mass 抖动问题都来自过于频繁的加减 Tag。

常见坑

不要在每帧给大量实体 Add/Remove 同一个 Tag 表示瞬时条件。不要把 Defer 当作无限制消息队列,命令太多仍然会在 flush 时集中还债。不要在多个线程随意 Push 到同一个 CommandBuffer。不要忘记结构变更会影响 Observer,Observer 写得过宽会把一次批量迁移放大成更多工作。

源码路径索引

  • MassEntity/Public/MassEntityManager.h
  • MassEntity/Public/MassCommandBuffer.h
  • MassEntity/Public/MassCommands.h
  • MassEntity/Public/MassEntityBuilder.h