总览
Mass 里真正写逻辑的地方通常是 Query 和 Processor。FMassEntityQuery 声明“我要哪些 Fragment/Tag/Shared Fragment,以及读写权限”;调度和缓存系统用这些 Requirements 找到匹配 Archetype;执行时 FMassExecutionContext 提供当前 Chunk 的数据视图、实体列表、DeltaTime、DeferredCommandBuffer 和 Subsystem 访问。
你可以把 Query 想象成 SQL 的 WHERE 加 SELECT,但它面向的是 Archetype 和 Chunk,不是对象列表。一次 Query 至少需要 All、Any 或 Optional 之一才有意义;源码注释也说明有效查询需要描述需求,并缓存有效 Archetype。
源码依据
MassEntityQuery.h 提供 ForEachEntityChunk、ParallelForEachEntityChunk、CacheArchetypes、DirtyCachedData、SetChunkFilter、GroupBy、RequireMutatingWorldAccess 和匹配实体计数等接口。MassExecutionContext.h 维护 FragmentViews、ChunkFragmentViews、Shared/ConstShared 视图、Subsystem 访问、DeferredCommandBuffer、EntityListView 和 DeltaTime。MassRequirements.h 则定义访问模式和存在性需求。
Query 设计规则
| 规则 | 做法 | 目的 |
|---|---|---|
| 写权限要少 | 只把真的要改的 Fragment 标成 ReadWrite | 给依赖求解器更多并行空间 |
| Optional 要克制 | 只用于兼容少数分支 | 避免 Processor 内部分支过多 |
| Chunk Filter 用于预算 | 按 LOD、可见性、变量 Tick 过滤 Chunk | 减少无意义遍历 |
| Subsystem 要声明 | 通过 Query 声明读写 Subsystem | 保证线程和依赖正确 |
| 结构变更走 Defer | Query 中只写字段,组成变化延迟 | 保持 Chunk 视图稳定 |
架构分析
Requirements 同时影响“能不能匹配”和“怎么调度”。如果两个 Processor 都只读同一组 Fragment,它们理论上可以并行;如果一个写 FMassDesiredMovementFragment,另一个读它,那么调度器需要保证顺序或至少不能并发冲突。手动 ExecuteBefore/After 可以补充业务顺序,但不能替代正确的读写声明。
FMassExecutionContext 的视图只在当前 Chunk 有效,不要把 TArrayView、引用或指针保存到异步任务外面。要跨帧保存信息,保存到 Fragment 或 Subsystem;要跨实体关联,保存 FMassEntityHandle 并在使用前验证实体仍有效。
使用案例
下面是一个典型查询配置:读 Transform 和 Intent,写 DesiredMovement,并要求实体有顾客 Tag、没有暂停 Tag。
void UShopperDesiredMovementProcessor::ConfigureQueries(const TSharedRef<FMassEntityManager>& EntityManager)
{
EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadOnly);
EntityQuery.AddRequirement<FShopperIntentFragment>(EMassFragmentAccess::ReadOnly);
EntityQuery.AddRequirement<FMassDesiredMovementFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddTagRequirement<FShopperCustomerTag>(EMassFragmentPresence::All);
EntityQuery.AddTagRequirement<FMassSimulationPausedTag>(EMassFragmentPresence::None);
EntityQuery.RegisterWithProcessor(*this);
}
void UShopperDesiredMovementProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntityManager, Context, [](FMassExecutionContext& Context)
{
const auto Transforms = Context.GetFragmentView<FTransformFragment>();
const auto Intents = Context.GetFragmentView<FShopperIntentFragment>();
auto Desired = Context.GetMutableFragmentView<FMassDesiredMovementFragment>();
for (int32 Index = 0; Index < Context.GetNumEntities(); ++Index)
{
const FVector Location = Transforms[Index].GetTransform().GetLocation();
const FVector ToTarget = Intents[Index].TargetLocation - Location;
Desired[Index].DesiredVelocity = ToTarget.GetSafeNormal2D() * 140.0f;
Desired[Index].DesiredFacing = Desired[Index].DesiredVelocity.ToOrientationQuat();
}
});
}
项目落地
给每个 Processor 写一句“输入/输出契约”很有帮助。例如“输入 Transform、Intent;输出 DesiredMovement;不做结构变更”。代码评审时先看契约,再看 Query 是否吻合。上线前可以把高频 Processor 的 Optional Requirement 清掉或拆成多个更窄 Processor,减少分支和 cache miss。
常见坑
不要在 Query 中误把只读字段声明为 ReadWrite,这会让调度器认为存在写冲突。不要在 ParallelForEach 中访问非线程安全 Subsystem 或 Actor。不要把 Optional 当继承体系用,Mass 更偏向组合和多 Processor 分工。不要忘记 Query 缓存需要在实体组成变化后变脏,框架会处理常见路径,自定义缓存时要谨慎。
源码路径索引
MassEntity/Public/MassEntityQuery.hMassEntity/Public/MassRequirements.hMassEntity/Public/MassExecutionContext.hMassEntity/Public/MassExecutor.h