UE5.8 StateTree 状态树专题系列

UE5.8 StateTree 专题(七):Task 生命周期,Enter、Tick、Exit 到底什么时候调用

从 FStateTreeTaskBase 源码讲 EnterState、Tick、ExitState、StateCompleted、Running/Succeeded/Failed,并写一个等待和一个自定义巡逻 Task。

总览

Task 是 StateTree 里真正“做事”的单元。你写自定义 Task 时,最重要的是知道它什么时候开始、什么时候每帧推进、什么时候结束、什么时候清理。否则你会遇到这些问题:一进入状态就跳走、任务永远 Running、Move To 没清理、动画重复播放、状态切换后旧逻辑还在跑。

UE5.8 StateTree 专题(七):Task 生命周期,Enter、Tick、Exit 到底什么时候调用 配图
Task 像一个小工作单元:进入状态时开始,Tick 中推进,返回成功/失败后触发完成跳转。

生命周期顺序

最常见顺序是:

  1. State 被选中。
  2. Task 的绑定属性被复制。
  3. 调用 EnterState
  4. 如果返回 Running,后续 Tick 调用 Tick
  5. Tick 返回 Succeeded 或 Failed 后,状态完成。
  6. Transition 选择下一个状态。
  7. 离开旧状态时调用 ExitState
  8. 完成通知时可能调用 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 只有在 bShouldCallTickbShouldCallTickOnlyOnEvents 符合时调用。ExitState 在当前状态退出时调用。StateCompleted 在状态完成后、新状态选择前以反序调用。

架构分析

Task 是最容易产生副作用的地方,所以要把生命周期当成资源管理边界:Enter 申请或启动,Tick 推进,Exit 释放或取消。启动 GameplayTask、绑定委托、播放 Montage、开 Timer、Claim SmartObject,都应该能在 Exit 里找到对应的清理策略。这样状态被打断时也不会留下脏状态。

使用案例

守卫 Patrol 状态:

  1. PickNextPatrolPoint Task:EnterState 里选点,返回 Succeeded。
  2. Move To Task:EnterState 发起 AI Move,返回 Running。
  3. Move 成功后返回 Succeeded。
  4. Transition 到 Idle。

这说明一个状态可以有多个 Task,但要注意任务完成策略。新手阶段建议一个状态先放一个主要 Task,等理解完成策略后再组合多个。

项目落地

每个自定义 Task 都写清楚三件事:Enter 做什么,Tick 做什么,Exit 清理什么。需要注册委托、启动 GameplayTask、播放 Montage、开启 Timer 的任务,Exit 里必须考虑取消或解绑。否则状态切走后旧回调可能回来改新状态的数据。

常见坑

不要在 EnterState 返回 Succeeded 后还期待 Tick 会被调用。不要把运行时变化写进共享的节点模板数据,应该写 InstanceData。不要忘记 bShouldCallTick,如果关闭 Tick,绑定 Tick 拷贝也不会按你预期发生。不要在 ExitState 里启动新行为,Exit 主要做清理。

源码路径索引

  • StateTreeModule/Public/StateTreeTaskBase.h
  • StateTreeModule/Public/StateTreeExecutionContext.h
  • StateTreeModule/Public/StateTreeExecutionTypes.h
  • GameplayStateTreeModule/Public/Tasks/StateTreeMoveToTask.h