总览
FMassEntityManager 是 Mass 实体世界的核心账本。它负责创建实体、销毁实体、管理 Archetype、批量构建、查找匹配 Archetype,并把实体从一个组成迁移到另一个组成。源码注释里明确说实体存储在 chunked array 中,每个有效实体会被分配到一个 Archetype,Fragment 数据由该 Archetype 管理。
日常项目里最容易踩坑的是结构变更:加 Fragment、删 Fragment、加 Tag、删 Tag 都可能改变实体所属 Archetype。如果你正在 Query 遍历某个 Chunk,同时立刻迁移实体,就会破坏当前遍历的稳定性。因此 Mass 提供 FMassCommandBuffer 和 Defer() 模式,把结构变更延迟到安全点统一执行。
源码依据
MassEntityManager.h 提供 CreateArchetype、GetOrCreateSuitableArchetype、GetArchetypeForEntity、GetMatchingArchetypes 等接口。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.hMassEntity/Public/MassCommandBuffer.hMassEntity/Public/MassCommands.hMassEntity/Public/MassEntityBuilder.h