AnimBP 编译篇说了节点图怎么变成求值栈。状态机(AnimGraph 里的 State Machine 节点)是 AnimBP 里语义最重的一个节点:它看起来是"图里的图",编译后和别的节点一样被压进同一个节点数组,但转移(Transition)的求值时机有自己独立的时钟。这篇把三个常被误解的点钉死:转移规则何时求值、跨状态混合的权重结构、打断机制的成本。
状态机的编译形态
State Machine 节点编译成 FAnimNode_StateMachine,内部再持有:
FAnimNode_State数组:每个状态就是一个小 AnimGraph(自己的求值子栈);FAnimationTransitionNode数组:转移边,每条持有转移规则(一个布尔表达式子图)和跨状态混合参数(Blend 时长/曲线);- 运行时状态:
CurrentState/MostRecentlyEvaluatedState/ 活动转移列表ActiveTransitions。
关键认知:状态机节点本身在 Evaluate 阶段就像一个普通节点——下游要 pose 它就算当前状态(或转移混合)的 pose。它的特殊逻辑全在 Update 阶段驱动:状态时钟推进、转移求值、活动转移的混合权重更新。
转移规则:何时求值,求值几次
这是最常被误解的机制。直觉上"条件满足就转移",但代码里的实际语义是:
- 求值发生在源状态的 Update 阶段,且只有
bCanTransition检查的采样时机才跑规则图——具体是状态机节点 Update 时逐个检查出边(FAnimNode_StateMachine::Update→UpdateTransitions); - 每条出边的规则每帧最多求值一次,结果缓存在转移节点上(
bLastTickEvaluated之类),同一帧内多个消费方共用; - 混合窗口内(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分支,优先级比较就在那一小段。
机制全景: