技术博客

状态机源码:FAnimNode_StateMachine 的转移评估

第 53 / 100 篇 · 2026-07-28 · ue5 · 动画系统状态机源码分析

AnimBP 编译篇说了节点图怎么变成求值栈。状态机(AnimGraph 里的 State Machine 节点)是 AnimBP 里语义最重的一个节点:它看起来是"图里的图",编译后和别的节点一样被压进同一个节点数组,但转移(Transition)的求值时机有自己独立的时钟。这篇把三个常被误解的点钉死:转移规则何时求值、跨状态混合的权重结构、打断机制的成本。

状态机的编译形态

State Machine 节点编译成 FAnimNode_StateMachine,内部再持有:

  • FAnimNode_State 数组:每个状态就是一个小 AnimGraph(自己的求值子栈);
  • FAnimationTransitionNode 数组:转移边,每条持有转移规则(一个布尔表达式子图)和跨状态混合参数(Blend 时长/曲线);
  • 运行时状态:CurrentState / MostRecentlyEvaluatedState / 活动转移列表 ActiveTransitions。

关键认知:状态机节点本身在 Evaluate 阶段就像一个普通节点——下游要 pose 它就算当前状态(或转移混合)的 pose。它的特殊逻辑全在 Update 阶段驱动:状态时钟推进、转移求值、活动转移的混合权重更新。

转移规则:何时求值,求值几次

这是最常被误解的机制。直觉上"条件满足就转移",但代码里的实际语义是:

  1. 求值发生在源状态的 Update 阶段,且只有 bCanTransition 检查的采样时机才跑规则图——具体是状态机节点 Update 时逐个检查出边(FAnimNode_StateMachine::Update → UpdateTransitions);
  2. 每条出边的规则每帧最多求值一次,结果缓存在转移节点上(bLastTickEvaluated 之类),同一帧内多个消费方共用;
  3. 混合窗口内(Crossfade 进行中)规则继续求值——bAnimGraphEvaluated 不会冻结规则,这意味着"转移中的转移"(A→B 没混完又触发 B→C)是常态而非异常。

第三条解释了为什么"转移期间触发新转移"的行为看起来不稳定:混合中源状态还在 Update,它的出边还在被检查;同时目标状态已经 Update,它的出边也开始检查。一个帧窗口内最多两条边的规则在同时求值,谁触发取决于 Update 顺序和规则各自的缓存。设计状态机时把"互斥条件"(A→B 和 A→C 的条件必须互斥)当纪律写,不要依赖求值顺序的隐式优先级——代码里没有图结构上可见的优先级,Transition Priority(每个转移边上的数字)才是显式优先级,同帧多触发时取优先级最高的。

跨状态混合:两层权重栈

转移触发后,动画不是"切过去"而是叠加混合。数据结构上是一个两层栈:

  • 下层:源状态 pose(权重 $w_{src}$);
  • 上层:目标状态 pose(权重 $w_{dst} = 1 - w_{src}$);
  • 混合曲线由转移边的 BlendProfile / BlendOption 定义,$w_{src}$ 按曲线从 1 衰减到 0。

实现上活动转移是 FAnimationActiveTransitionEntry,持有 Alpha 和 Blend 对象,每帧 Update 推进 Alpha,Evaluate 时对两个状态的 pose 做 FAnimationRuntime::BlendTwoPosesTogether。多条活动转移共存时(打断链),栈不止两层——A→B 混到一半触发 B→C,此时求值的是 A、B、C 三个 pose 的三层叠加,每层权重由各自的 Alpha 链推出。打断越深,当帧 pose 成本线性涨,这是打断(Interruption)最实在的代价。

bInterruptByOtherTransitions 的优先级组(Priority 0 最高)决定被打断的顺序:高优先级转移可以打断低优先级的活动转移,同优先级看序号。这个机制给了"受击动画打断一切常规动作"的标准实现,但每条打断边都是潜在的栈加深,谨慎加。

状态时钟与通知

状态的 TimeRemaining / CurrentTime 在 Update 推进,驱动状态内序列的采样位置和自动转移(Automatic Rule Based on Sequence Player in State 的 "End of State")。两个坑:

  • 时间缩放后自动转移点漂移:状态内 SequencePlayer 设了 PlayRate,End of State 以动画实际剩余时间为准还是状态时钟为准,取决于 bIgnoreStateMachineTransition 之类的配置,版本间变过,别盲信;
  • Notify 跨混合丢触发:源状态衰减到 0 权重的那帧,其 Notify 触发被 NotifyWeightThreshold 过滤(默认 0.4)——权重低于阈值的 pose 不触发 Notify。混合很快时(0.1s 内的 crossfade),状态尾部的 Notify(脚步声在最后一帧)会被静默吞掉。缓解:把关键 Notify 放中间帧、或改用 AnimNotifyState 让窗口跨越混合、或调高阈值前想清楚副作用。

源码入口

  • 状态机节点:Animation/AnimNode_StateMachine.cpp,Update 看转移检查循环,Evaluate 看活动转移栈的合成;
  • 活动转移:FAnimationActiveTransitionEntry(同文件),看 Update 里 Alpha 推进和 Blend 构造;
  • 转移规则:FAnimationTransitionNode::Evaluate 的规则子图求值,注意它复用 FastPath 的绑定结果——规则里的变量绑定和 AnimGraph 绑定走同一套 FExposedValueHandler;
  • 打断逻辑:UpdateTransitions 里 bInterruptByOtherTransitions 分支,优先级比较就在那一小段。

机制全景:

转移求值时机:Update 阶段查边,Evaluate 阶段叠 pose 状态 A(源) Update 推进时钟 转移边 A→B 规则每帧求值一次 状态 B(目标) 触发后才开始 Update 混合窗口内:pose(A)·w_src(t) + pose(B)·(1−w_src(t)),w 按 Blend 曲线衰减 打断发生 → 栈加深一层,当帧求值成本线性涨 坑 1:A→B 与 A→C 条件必须显式互斥 代码里没有隐式图优先级 坑 2:快速混合吞尾部 Notify 权重低于阈值不触发