3.x 的动画系统有个怪味道:AnimationPlayer 既管"按时间播动画"又管"把值写回节点属性",AnimationTree 想做个图化混合,最后却只能在内部包一个 AnimationPlayer 来借它的播放能力——两层职责搅在一起,AnimationTree 的每个节点都隔着一层 player 的语义。Godot 4.0 的重构把公共部分抽成了 AnimationMixer(scene/animation/animation_mixer.h):播放、混合、事件分发都在基类,AnimationPlayer 只剩"手动播放"的语义,AnimationTree 则作为另一种"混合策略提供者"与它平级。这篇拆这个基类到底做了什么。
从"一个 player"到"一组活动动画"
3.x 的 AnimationPlayer 同一时刻只有一条"当前动画"的时间轴。AnimationMixer 的核心抽象改成了一张活动动画列表:play() 不再是"切换当前动画",而是往列表里加一个 ActiveAnimation 条目——
ActiveAnimation { animation, time, speed, weight, fade, loop_mode }
每个条目有自己的本地时钟和权重。_process(或物理帧回调)里,mixer 把每条活动动画的时间推进 $\Delta t \times \text{speed}$,按各自的 fade 曲线更新 weight,然后进入求值阶段:每条动画按当前时间在每条 track 上采样出值,不同活动动画对同一条 track 的采样值按 weight 混合,得到最终写回节点的值。交叉淡入淡出从"player 内部的特殊状态"变成了"两个普通 ActiveAnimation 的 weight 此消彼长"——这正是八-11 等功率曲线的动画版,只是权重归一化由 mixer 统一做。
求值与写回:track cache
混合出来的值要落到节点属性上。Mixer 维护一张 track cache:track 路径(NodePath)→ 目标对象 + 属性 setter 的缓存表。首次求值时解析路径(二-9 的 NodePath 解析那套,缓存后不再重复),之后每帧是纯粹的查表 + set()。这个缓存还有第二层收益:节点临时移出树(切场景、隐藏子树)时 cache 标记失效而不是报错,节点回来直接复用。
Track 类型决定混合方式——这是 mixer 里最要小心的正确性角落:
- 连续值(position、float):线性加权 $\bar{v} = \sum_i w_i v_i \big/ \sum_i w_i$;
- 旋转:quaternion 加权要做最近化(翻转符号使点积为正再 lerp/slerp),否则两条差 180° 的等价旋转会被插值穿过零点;
- 离散/触发 track(方法调用、离散布尔):不能加权——权重最高的那条胜出,且只在它的时间跨过时触发一次。
为什么这次重构值得
对 AnimationTree,收益是结构性的:blend tree 的每个节点不再需要"借"一个 player,它们向 mixer 输出的就是一组带权重的 ActiveAnimation 提案,真正的播放状态只有 mixer 一份(九-2 展开求值顺序)。这消掉了 3.x 一堆诡异 bug 的根源——两个 player 时钟不一致导致的偏移累积。
对写脚本的,收益在 API 面:AnimationMixer 暴露的 play/stop/set_speed/animation_started/finished 这层语义,AnimationPlayer 和 AnimationTree 完全一致,而 3.x 里 AnimationTree 上这套东西是绕路实现的。还有 mixer 级别的根运动累计(get_root_motion_position() 一族):3.x 根运动归 player 管,AnimationTree 里做根运动要靠 hack;4.x 累计发生在 mixer 的求值阶段,对树内所有动画统一生效(九-6 的 root motion 基础就是它)。
迁移时的行为差异
从 3.x 迁代码有一个语义变化要适配:3.x 的 play() 是切换当前动画,4.x 是往列表里加条目。绝大多数时候行为一致(同名动画重复 play 会复用条目),但依赖"play B 时 A 立刻彻底停止"的老代码要检查——A 的停止现在受 fade out 控制,要立即停止得显式 stop("A") 或设 fade = 0。另外 current_animation 属性没了,对应状态从 get_current_animation() 系列 API 读。这些都在官方迁移文档里,但实际项目里最常踩的是第一条:以为停了其实还在淡出,方法是 track 还在被两条动画抢着写。
本文基于 Godot 4.2–4.4 源码(scene/animation/animation_mixer.h);AnimationMixer 自 4.0 引入,后续 minor 版本在回调时机上有修补,行为以主线为准。