AnimationTree 上手很容易:AnimationNodeStateMachine 画几个状态、连几条过渡线,就能跑。但它内部"每帧到底发生了什么"值得拆开看——尤其是 blend tree 模式下,图不是命令式执行的,而是被排序后从叶子向根一次性求值。理解这个模型,才能解释为什么有些连线方式性能差、为什么有的节点要求"同步"。
节点是函数:输入状态,输出状态
AnimationNode 的接口就一对(scene/animation/animation_node.h):
AnimationState process(time, seek, is_external_seeking)
AnimationState 是一组带权重的活动动画提案——Animation { resource, time, weight, ... } 的列表,外加时间和速度信息。每个节点接收子节点的输出状态作为输入,做自己的混合运算,把结果往父节点传。根节点(StateMachine / BlendTree 的 Output 节点)的最终状态交给 AnimationMixer(九-1),mixer 负责真正采样和写回。
这个抽象的关键在于:节点本身不产生采样值,只搬运和重组"谁在播、权重多少、时间走到哪"。真正的逐帧属性求值只发生在 mixer 一处——所以图里有十个节点也不意味着十套插值,图的输出可能被塌缩成两条活动动画。
求值顺序:拓扑排序,不是遍历
BlendTree 里每个节点有输入端口(AnimationNodeBlendTree 里表现为 input_count),连线定义依赖:Output 依赖它的子节点,子节点再依赖各自的子节点。每帧求值前,引擎按拓扑排序(Kahn 算法的 DFS 变体)排出一个从叶子到根的线性序列,然后依次 process 一遍。两个工程推论:
- 求值次数 = 节点数:每帧每个节点恰好 process 一次,与图的形状无关。同样三个动画,串成三层 Blend2 和直接接到一个 Blend3,成本几乎一样。
- 环是不允许的:拓扑排序的前提是 DAG,BlendTree 的编辑器连线本身就拒绝成环;StateMachine 的过渡环则是另一回事(状态机靠 transition 驱动,不是数据流图)。
StateMachine 更直白:它没有"每帧全图求值",只有当前状态的子树被 process,过渡发生时从源状态 fade out、目标状态 fade in 地交接提案权重——这正是 mixer 活动动画列表(九-1)里"两条动画权重此消彼长"的来源。
BlendSpace2D:重心坐标插值
BlendSpace2D 把动画放在二维坐标系里(常见用法:横轴速度、纵轴朝向),查询点落在图上时,取最近的三个顶点做重心坐标(barycentric)插值。设三个顶点权重初值由面积比给出:对查询点 $p$ 和顶点 $v_1, v_2, v_3$ 构成的三角形,
$$w_1 = \frac{\|(p - v_2) \times (v_3 - v_2)\|}{\|(v_1 - v_2) \times (v_3 - v_2)\|}, \quad w_2, w_3 \text{ 轮换对称}$$
叉积比给出的是子三角形面积比,三者之和为 1。查询点落在凸包外时,引擎退化为最近边界线段的两点插值。落在恰好两个顶点等距的位置时,三角形选择会抖动——这会造成动画权重在两个合法解之间跳变,工程上的标准缓解是顶点别太稀疏,让重心移动时权重变化平滑。
同步节点:共享时钟
不加处理的混合有个问题:walk 动画长 1.0s、run 动画长 0.6s, Blend2 把它们 50/50 混合时,两条动画的本地时间各自走,脚步相位根本对不上。AnimationNodeSync(Blend2/BlendSpace 等可开启的 sync 属性)解决的就是这个:开启后子动画的播放进度被归一化到 0..1 相位再混合——walk 走到 30%,run 也按 30% 走。归一化的公式很朴素:$\phi_i = (t_i \bmod T_i) / T_i$,混合时所有子节点锁定同一 $\phi$,输出时间再反解回父节点的时钟。代价是动画必须有语义对齐的循环点:walk 和 run 的左脚落地帧都应在相位 0 附近,否则同步了相位反而对齐了错误姿势。这是美术资源问题不是引擎问题——但排查"同步了还是滑步"时,第一反应应该是检查两条动画的循环相位。
实战建议
一,监控活动动画数而不是节点数:图很复杂但输出始终塌缩成两三条动画,CPU 花在 mixer 采样上的才是大头(九-14 的性能账按这个思路算)。二,能用 StateMachine 表达的不要套 BlendTree——状态机只 process 当前状态,BlendTree 是全图求值。三,sync 只在相位语义对齐的动画组上开,乱开比不开更糟。四,过滤(filter)跳层动画时记得 Blend2 的 filter 是按 track 路径剔除的,路径写错会静默失效——表现为"跳跃动画全身都在播"。
本文基于 Godot 4.2–4.4 源码(scene/animation/animation_blend_tree.cpp、animation_node_state_machine.cpp);求值顺序与 sync 语义在 4.x 各 minor 版本稳定。