总览
Mass 的第一课是忘掉“一个对象一棵组件树”。FMassEntityHandle 只是一对索引和序列号,负责标识实体;实体有什么数据,由 Fragment 和 Tag 决定;拥有相同组成的实体会进入同一个 Archetype;Archetype 再把数据按 Chunk 存储。Processor 遍历时拿到的是当前 Chunk 的列视图,而不是一个个 UObject。
这种设计适合批处理,因为同一批实体的数据布局一致。例如所有“会移动且有 Transform”的实体可以共享 FTransformFragment、FMassVelocityFragment、FMassDesiredMovementFragment 的数组;一个移动 Processor 只需要遍历这些数组,不关心实体来自哪个 Spawner,也不关心近处是否有 Actor 表现。
源码依据
MassEntityFragments.h 定义了 FMassFragment、FMassTag、FMassChunkFragment、FMassSharedFragment 和 FMassConstSharedFragment 的基础类型。MassEntityTypes.h 中的 FMassArchetypeCompositionDescriptor 在 UE5.8 使用 FMassElementBitSet 管理组成信息,并提供 HasAll、Append、Remove、CountStoredTypes 等操作。源码里很多旧的 subset getter 已被标记为 UE5.8 deprecated,说明组合判断的推荐路径已经转向统一 bitset。
类型怎么选
| 类型 | 适合放什么 | 典型例子 |
|---|---|---|
| Fragment | 每个实体不同、Processor 经常读写的数据 | 位置、速度、目标、生命值、状态句柄 |
| Tag | 只有有/无,没有实例数据的状态 | FMassCodeDrivenMovementTag、FMassOffLODTag |
| Chunk Fragment | 同一个 Chunk 共享、用于批处理过滤的数据 | 可见性、变量 Tick、LOD Chunk 状态 |
| Shared Fragment | 多实体共享、但可能运行时引用对象的数据 | Subsystem、表现管理器引用 |
| Const Shared Fragment | 配置参数,不希望每个实体复制一份 | 移动参数、LOD 距离、复制参数 |
经验规则很直接:如果每个实体每帧都要变,放 Fragment;如果只是开启/关闭一类 Processor,放 Tag;如果很多实体完全共用一份配置,放 Const Shared Fragment;如果它包含 UObject 或只允许游戏线程访问,要检查对应 Traits 和线程边界。
架构分析
Archetype 是 Mass 的“数据表结构”。加入或移除 Fragment/Tag 并不是改一个字段,而是改变实体组成,实体可能要迁移到另一个 Archetype。Chunk 是 Archetype 内部的批量存储单位,它让 Query 可以一次处理一组连续数据。这样的代价是结构变更比普通字段写入更贵,收益是稳定组成的大批量实体能很快。
一个常见错误是为每个状态都创建 Tag,然后在行为里频繁切换。比如“正在走路”“正在排队”“正在看手机”“正在避让”都做成 Tag,会让实体在多个 Archetype 间跳来跳去。更稳的方式是把短期状态放进一个枚举 Fragment,只把会显著改变处理器集合的状态做成 Tag,例如“需要智能物体交互”“暂停模拟”“自定义移动”。
使用案例
假设要做商场顾客,可以这样拆数据:
USTRUCT()
struct FShopperIntentFragment : public FMassFragment
{
GENERATED_BODY()
FVector TargetLocation = FVector::ZeroVector;
uint8 Intent = 0; // 0 Wander, 1 Queue, 2 Browse, 3 Exit
};
USTRUCT()
struct FShopperBudgetFragment : public FMassFragment
{
GENERATED_BODY()
float RemainingBudget = 100.0f;
float Patience = 1.0f;
};
USTRUCT()
struct FShopperCustomerTag : public FMassTag
{
GENERATED_BODY()
};
这里 Intent 是频繁变化的运行时状态,适合 Fragment;“这是顾客类型”才适合 Tag。预算和耐心不该放 Shared Fragment,因为每个顾客不同。移动速度配置反而可以放 FMassMovementParameters 这类 Const Shared Fragment。
项目落地
设计 Fragment 前先列 Processor。每个 Processor 需要读什么、写什么、是否能并行,反推数据边界。比如 ShopperChooseDestinationProcessor 写 FShopperIntentFragment,ShopperMovementIntentProcessor 读它并写 FMassDesiredMovementFragment,ShopperCheckoutProcessor 读写预算和排队状态。这样的拆分比先写一堆字段再找地方使用更稳。
常见坑
不要把 UObject、TArray、大型结构随便塞进高频 Fragment。Mass 不是不能存复杂类型,但普通高频 Fragment 越小越好。不要用 Entity Handle 当稳定存档 ID,Handle 会随世界和生命周期变化。不要默认所有 Fragment 都线程安全,访问 World、Subsystem、Actor、Component 前要确认 Query 是否要求游戏线程,以及 Subsystem traits 是否允许并行读写。
源码路径索引
MassEntity/Public/MassEntityHandle.hMassEntity/Public/MassEntityFragments.hMassEntity/Public/MassEntityTypes.hMassEntity/Public/MassArchetypeTypes.h