总览
Task 是 StateTree 里真正“做事”的单元。你写自定义 Task 时,最重要的是知道它什么时候开始、什么时候每帧推进、什么时候结束、什么时候清理。否则你会遇到这些问题:一进入状态就跳走、任务永远 Running、Move To 没清理、动画重复播放、状态切换后旧逻辑还在跑。
生命周期顺序
最常见顺序是:
- State 被选中。
- Task 的绑定属性被复制。
- 调用
EnterState。 - 如果返回 Running,后续 Tick 调用
Tick。 - Tick 返回 Succeeded 或 Failed 后,状态完成。
- Transition 选择下一个状态。
- 离开旧状态时调用
ExitState。 - 完成通知时可能调用
StateCompleted。
EnterState 返回 Succeeded 的意思是:我一进来就做完了。比如“设置一个变量”可以这样。Move To 这类需要等待的任务通常返回 Running,然后在 Tick 或回调里完成。
Running、Succeeded、Failed
| 返回值 | 意思 | 守卫例子 |
|---|---|---|
| Running | 还没做完,下帧继续 | 正在走向巡逻点 |
| Succeeded | 做完且成功 | 到达巡逻点 |
| Failed | 做完但失败 | 巡逻点不可达 |
不要害怕 Failed。Failed 很有用,它让你能写“如果 Move To 不可达,就去 Search 或 Return”的兜底。
写一个等待 Task
示例结构:
USTRUCT()
struct FGuardWaitTaskInstanceData
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category="Parameter")
float Duration = 1.0f;
UPROPERTY(Transient)
float RemainingTime = 0.0f;
};
USTRUCT(meta=(DisplayName="Guard Wait"))
struct FGuardWaitTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
using FInstanceDataType = FGuardWaitTaskInstanceData;
virtual const UStruct* GetInstanceDataType() const override
{
return FInstanceDataType::StaticStruct();
}
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override
{
FInstanceDataType& Data = Context.GetInstanceData(*this);
Data.RemainingTime = Data.Duration;
return EStateTreeRunStatus::Running;
}
virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override
{
FInstanceDataType& Data = Context.GetInstanceData(*this);
Data.RemainingTime -= DeltaTime;
return Data.RemainingTime <= 0.0f ? EStateTreeRunStatus::Succeeded : EStateTreeRunStatus::Running;
}
};
喂饭解释:Duration 是可配置参数,RemainingTime 是运行时临时数据。进入状态时把剩余时间设好,Tick 每帧扣,扣完返回 Succeeded。
源码依据
FStateTreeTaskBase 注释明确:EnterState 在新状态进入时调用,返回 Succeeded/Failed 会立即结束状态并触发新状态选择,Running 会继续 Tick。Tick 只有在 bShouldCallTick 或 bShouldCallTickOnlyOnEvents 符合时调用。ExitState 在当前状态退出时调用。StateCompleted 在状态完成后、新状态选择前以反序调用。
架构分析
Task 是最容易产生副作用的地方,所以要把生命周期当成资源管理边界:Enter 申请或启动,Tick 推进,Exit 释放或取消。启动 GameplayTask、绑定委托、播放 Montage、开 Timer、Claim SmartObject,都应该能在 Exit 里找到对应的清理策略。这样状态被打断时也不会留下脏状态。
使用案例
守卫 Patrol 状态:
PickNextPatrolPointTask:EnterState 里选点,返回 Succeeded。Move ToTask:EnterState 发起 AI Move,返回 Running。- Move 成功后返回 Succeeded。
- Transition 到 Idle。
这说明一个状态可以有多个 Task,但要注意任务完成策略。新手阶段建议一个状态先放一个主要 Task,等理解完成策略后再组合多个。
项目落地
每个自定义 Task 都写清楚三件事:Enter 做什么,Tick 做什么,Exit 清理什么。需要注册委托、启动 GameplayTask、播放 Montage、开启 Timer 的任务,Exit 里必须考虑取消或解绑。否则状态切走后旧回调可能回来改新状态的数据。
常见坑
不要在 EnterState 返回 Succeeded 后还期待 Tick 会被调用。不要把运行时变化写进共享的节点模板数据,应该写 InstanceData。不要忘记 bShouldCallTick,如果关闭 Tick,绑定 Tick 拷贝也不会按你预期发生。不要在 ExitState 里启动新行为,Exit 主要做清理。
源码路径索引
StateTreeModule/Public/StateTreeTaskBase.hStateTreeModule/Public/StateTreeExecutionContext.hStateTreeModule/Public/StateTreeExecutionTypes.hGameplayStateTreeModule/Public/Tasks/StateTreeMoveToTask.h