总览
Mass 里的 StateTree 是最容易把新手劝退的一块。普通 Actor StateTree 是“一个 Actor 上挂一个组件,然后组件 Tick 它”。Mass 不是这样。Mass 关心的是成百上千个 Entity,不能给每个 Entity 都挂 Actor 组件,于是它用 Fragment 存状态树实例句柄,再由 Mass Processor 批量推进。
先别背类名,先记住一句话:普通 StateTree 像“每个 NPC 自己带一本任务清单”,Mass StateTree 像“调度员拿着一批人的任务清单,一批一批推进”。
普通 Actor 和 Mass 的差异
| 问题 | Actor StateTree | Mass StateTree |
|---|---|---|
| 运行入口 | UStateTreeComponent / UStateTreeAIComponent |
UMassStateTreeProcessor |
| 实例数据放哪 | 组件里的 FStateTreeInstanceData |
FMassStateTreeInstanceFragment 持有句柄 |
| 外部数据 | Actor、AIController、Component | Fragment、Subsystem、Entity View |
| Tick 粒度 | 一个组件一个组件 Tick | 按 Chunk/Query 批量 Tick |
| 适合对象 | 少量重要 NPC | 大量群众、车辆、简单 AI |
所以 Mass StateTree 不是“给 Mass Entity 挂了一个 StateTreeComponent”。它没有那个组件。它走的是 Mass 的 ECS 数据通道。
源码依据
UMassStateTreeTrait 是把 StateTree 加进 Mass Entity 配置的入口,属性上要求使用 MassStateTreeSchema。FMassStateTreeInstanceFragment 保存 FMassStateTreeInstanceHandle 和上次更新时间。FMassStateTreeSharedFragment 保存共享的 UStateTree 资产。FMassStateTreeExecutionContext 继承 FStateTreeExecutionContext,额外提供 Mass Entity、Mass ExecutionContext 和 EntityManager。UMassStateTreeProcessor 才是真正批量 Tick StateTree 的处理器。
架构分析
Mass StateTree 有四个关键角色:
| 角色 | 作用 | 新手理解 |
|---|---|---|
| Trait | 在 Entity Config 里声明“这类实体有 StateTree 行为” | 配方 |
| Shared Fragment | 多个实体共享同一个 StateTree 资产 | 公共剧本 |
| Instance Fragment | 每个实体自己的运行状态句柄 | 当前演到哪一页 |
| Processor | 每帧或被 Signal 唤醒后推进一批实体 | 调度员 |
为什么要这么绕?因为 Mass 的核心目标是批量。共享数据只存一份,个体差异放 Fragment,处理器按 Query 遍历 Chunk。这样 1000 个行人可以共享一套行为资产,但每个人仍然有自己的“当前状态”和“实例数据”。
使用案例
案例:城市里有 500 个路人。你想让他们做三件事:沿路走、发现座椅就坐下、坐一会儿继续走。
不要给 500 个路人都生成完整 AIController。可以用 Mass:
- Entity Config 加移动相关 Trait。
- 加 SmartObject 相关 Trait。
- 加
Mass StateTree Trait。 - StateTree 使用 Mass Schema。
- 状态设计为
Walk、FindSeat、UseSeat、ResumeWalk。
状态表:
| State | 输入数据 | Task | Transition |
|---|---|---|---|
| Walk | 当前位置、ZoneGraph lane | 沿 ZoneGraph 前进 | 看到可用座椅 -> FindSeat |
| FindSeat | SmartObject 查询结果 | Claim SmartObject | 成功 -> UseSeat,失败 -> Walk |
| UseSeat | Claim Handle、等待时间 | Use SmartObject / 等待 | 完成 -> ResumeWalk |
| ResumeWalk | 目标 lane | 释放交互并继续移动 | 成功 -> Walk |
数据怎么喂给 StateTree
普通 AI 里,你可能会从 Pawn 上读 HealthComponent、从 AIController 上读黑板。Mass 里要换脑子:数据应当来自 Fragment 或 Subsystem。
| 需求 | Mass 推荐数据来源 |
|---|---|
| 当前位置 | Transform Fragment |
| 速度/移动意图 | Movement Fragment |
| 可交互目标 | SmartObject Fragment 或 Subsystem 查询 |
| 当前行为状态 | StateTree Instance Fragment |
| 个体配置 | Shared Fragment / Const Shared Fragment |
这也是为什么 Mass StateTree 的 Task 不能随便假设自己能拿到 Actor。低 LOD 群众可能根本没有 Actor 表现体。
代码演示
Mass Processor 内部大致会做这样的事:拿到每个实体的 StateTree 实例数据,构造 FMassStateTreeExecutionContext,设置当前 Entity,然后 Tick。
UE::MassBehavior::ForEachEntityInChunk(Context, MassStateTreeSubsystem,
[TimeInSeconds](FMassStateTreeExecutionContext& StateTreeContext,
FMassStateTreeInstanceFragment& StateTreeFragment)
{
const float DeltaTime = FloatCastChecked<float>(
TimeInSeconds - StateTreeFragment.LastUpdateTimeInSeconds,
/* Precision */ 1.0 / 256.0);
StateTreeFragment.LastUpdateTimeInSeconds = TimeInSeconds;
StateTreeContext.Tick(DeltaTime);
});
这段不是让你照抄到项目里,而是帮助理解:Mass StateTree 的执行上下文里同时有 StateTree 的上下文,也有 Mass 的 Entity 上下文。写 Mass Task 时要从这个上下文取 Fragment 或 Subsystem,而不是去找组件。
项目落地
哪些逻辑适合 Mass StateTree:
- 大量单位共享相似行为,例如行人、车辆、动物群、战场小兵。
- 行为可以拆成简单状态,不依赖复杂蓝图 Actor。
- 低 LOD 时也能运行,不需要每个实体都有 SkeletalMesh Actor。
- 数据可以 Fragment 化,比如目标位置、速度、等待时间、交互句柄。
哪些逻辑不适合一开始就上 Mass StateTree:
- Boss AI。
- 强依赖复杂动画蓝图、Montage、剧情事件的角色。
- 每个单位有大量独特蓝图组件逻辑。
- 需要频繁和 UI、任务系统、背包系统强耦合的少量角色。
常见坑
不要把 Actor StateTree 的 Task 原封不动搬到 Mass。它可能依赖 AIController、Pawn、Component,而 Mass Entity 不一定有这些。不要在 Mass Task 里强制 Spawn Actor 做逻辑主体,表现层可以 Actor 化,但决策层要尽量留在 Fragment。不要忽视 Signal 和 Tick 预算,大量实体每帧全量 Tick 会把 Mass 的优势吃掉。不要把所有个体差异都塞进 StateTree 参数,能放 Fragment 的长期状态就放 Fragment。
源码路径索引
MassAI/Source/MassAIBehavior/Public/MassStateTreeTrait.hMassAI/Source/MassAIBehavior/Public/MassStateTreeFragments.hMassAI/Source/MassAIBehavior/Public/MassStateTreeExecutionContext.hMassAI/Source/MassAIBehavior/Public/MassStateTreeSchema.h