资产篇讲了数据存哪,这篇讲执行从哪来。AnimBP 的特别之处在于:节点图不是运行时解释执行的,编译期就被压扁成一个 C++ 类(生成的 FAnimInstance 子类)加一个纯数据驱动的事件图。理解这次编译做了什么,是读懂动画性能剖析(为什么一个 AnimBP 有几百个节点却只花 0.1ms)的前提。
编译产物:一个类 + 一个求值栈
AnimBP 编译(FAnimBlueprintCompilerContext,继承自 FKismetCompilerContext)输出两件东西:
- 生成的 UAnimInstance 子类:继承用户选定的 Native Parent,用户写的 EventGraph(Update、Blueprint Update Animation 等)编译成覆盖函数;
- FAnimInstanceProxy 子类:持有节点数组。AnimGraph 里的每个节点编译成一个
FAnimNode_*实例,按依赖拓扑序塞进Nodes数组——数组顺序就是求值顺序,运行时没有任何图遍历。
求值本身在 Evaluate_AnyThread:从输出节点(最终 Pose)出发递归触发输入节点的求值(每个节点缓存自己的输出 pose,被多个下游引用时只算一次,拓扑序保证了缓存有效)。这就是"图执行"的真相:一个静态排序的数组 + 递归缓存,没有解释器、没有虚表分发图结构。
编译期的三次图变换
用户连的图和最终节点数组之间隔着三次变换,调试 AnimBP 奇怪行为时要知道它们存在:
- Pin 展平:UK2Node_AnimGraphNode 壳被剥掉,只留 FAnimNode 核心字段,BP 变量的绑定转成对变量的直接引用;
- 折叠纯函数节点:无状态的转换节点(如部分数学节点)就地内联;
- 求值拆分(Update/Evaluate 分离):节点里"依赖时间的累积逻辑"(Update 部分)和"纯姿态计算"(Evaluate 部分)被拆到两个函数。这是动画系统双阶段模型的编译期落地。
第三次变换值得展开,因为它解释了 AnimBP 最大的心智负担:Update 在 GameThread 跑,Evaluate 在 WorkerThread 跑(5.x 默认并行求值)。节点作者必须保证 Update 里不做 Evaluate 的数据竞争——编译器帮你拆,但拆不干净的情况(节点自己在 Update 里改 Evaluate 要读的缓存)要靠 bNeedsUpdate 之类的守卫手动处理。自定义 AnimNode 的第一课就是分清这两半。
FastPath:消掉属性反射
AnimGraph 节点属性绑定到 BP 变量的默认路径是反射读写:Evaluate 阶段 GetPropertyValue / SetPropertyValue 走 FProperty 系统,几十个绑定就是几十次反射调用,在 worker 线程上还是 cache 灾难。
FastPath(FExposedValueHandler 系)是编译期的第二条路:当编译器能证明某个绑定的来源是纯数据(变量直连、无蓝图逻辑),它生成一个直接内存偏移读写的 handler,跳过整个反射层。判断自己的绑定走没走 FastPath 的方法很直白:Details 面板里绑定行显示蓝色(FastPath)还是黄色(慢路径),或者用 a.AnimBlueprint.FastPath 系列 log 看 fallback 原因。
常见 fallback 原因按出现频率排:绑定源头有蓝图逻辑(Get 节点后面接了计算)、绑定目标是 struct 成员的深层路径、绑定的变量没有 BlueprintVisible 且编译器拿不到稳定偏移。修复手法不是玄学——把计算挪进 Update 阶段写回一个纯变量,FastPath 立刻回来。一个 50+ 绑定的复杂 AnimBP,全量 FastPath 和全量慢路径的 Evaluate 开销能差一个量级,这是动画优化清单的第一项。
线程模型一图收束
自定义 AnimNode 的落地清单
写自定义节点(继承 FAnimNode_Base)时对一遍这张表:
Initialize_AnyThread只做一次性分配,Evaluate 里禁止 allocate;Update_AnyThread收输入、算权重,写自己的缓存,不读其他节点的输出;Evaluate_AnyThread纯函数化:同输入同输出,只读 Update 写好的缓存和输入 pose;- 需要蓝图绑定的属性,设计时就想好 FastPath 友好性(平铺标量,不要嵌套 struct);
- 并行求值下不碰 UObject(
UAnimSequence之外的 UObject 访问都危险),资产数据提前在 Initialize 里拷进节点。
源码入口
- 编译上下文:
Engine/Source/Runtime/Engine/Private/Animation/AnimBlueprintCompilerContext.cpp,看ProcessEventGraph之后对 AnimGraph 的三趟变换; - 求值执行:
FAnimInstanceProxy::Evaluate_AnyThread(Animation/AnimInstanceProxy.cpp),看输出节点递归如何被拓扑序短路; - FastPath:
Animation/AnimBlueprintGeneratedClass.cpp的FExposedValueHandler生成逻辑,fallback 原因在ValidatePropertyBindings; - 节点基类:
FAnimNode_Base(Animation/AnimNodeBase.h),三线程函数签名就在这里。