原地跑步动画 + 代码驱动位移,是最省事的做法,但对攻击前冲、翻越、处决这类"位移节奏是动画灵魂"的动作,代码写不出动画师调出来的加速曲线。Root Motion 让动画数据自己携带位移:播放期间把根骨骼的位移提取出来,应用到角色的运动上。听起来只是把"谁驱动位移"换了个答案,实际牵涉压缩、运动学积分和网络同步一整条链。基于 UE5.3 源码拆。
提取:根轨道从动画到曲线
动画资产里根骨骼(通常是 skeleton 的 root bone 或指定 motion root)的变换不是普通骨骼数据——UAnimSequence 里它被单独抽成一条 root motion track(FTransformCurve 形态存进 CompressedTrackToSkeletonMap 之外的独立通道)。为什么要单独抽出来:
- 压缩策略不同。普通骨骼 track 按旋转/位移分量做有损压缩(误差容限按骨骼重要性分级),root track 的位移直接决定角色位置,容限必须更紧甚至无损;
- 查询模式不同。其他骨骼每帧整段解压求值,root track 需要支持任意时间窗内的累计位移查询——"从 t 播到 t+Δt,角色总共动了多少",这要求位移数据能跨压缩块累加。
提取的结果在序列求值时产出 FRootMotionMovementParams(FAnimExtractContext::RootMotionMovementParams):一组从当前求值窗口提取出的累积 root transform。提取本身发生在 UAnimSequence::GetAnimationPose 里,按当前播放窗口的 start/end time 对 root track 做段内积分:
$$\Delta T = T_{root}(t_{end}) \cdot T_{root}(t_{start})^{-1}$$
这个"末时刻乘初时刻逆"的形式有个细节:root 位移是相对序列起点记录的,窗口积分必须左右消掉,不能把窗口起点之前的位移重复累加。
回放:谁消费这段位移
UAnimInstance::RootMotionMode 决定消费策略,三档的差异是信任边界:
IgnoreRootMotion:丢弃,角色全靠代码位移(原地动画的常规用法);RootMotionFromMontagesOnly:只认 Montage 里的 root motion——这是实战最常用档,因为 Montage 有明确的起止(攻击动作从哪帧冲到哪帧是资产内可控的),而普通 Sequence 的 root motion 来源太散;RootMotionFromEverything:所有序列的 root motion 都生效,Sequence 直接扔进 default slot 也能驱动位移——灵活但行为难审计。
消费链是:动画求值产出 FRootMotionMovementParams → AnimInstance 按 mode 过滤后挂到 USkeletalMeshComponent::RootMotionParams → CharacterMovement 在 UCharacterMovementComponent::ApplyRootMotionToVelocity 里把它变成速度增量:
$$v_{root} = \frac{\Delta x_{root}}{\Delta t}$$
这里有个运动学语义转换:动画给的是位移,移动组件要的是速度(因为 CharacterMovement 的权威表示是速度 + 碰撞扫掠)。Δt 是动画窗口对应的实际帧时间,Montage 被代码暂停或改播放速率时,root motion 的物理节奏跟着变——暂停 Montage 时角色会被"定"在半空冲拳姿势,因为位移增量随 Δt→∞ 为零。
还有一个坑:root motion 位移经过碰撞扫掠(PerformMovement 的 Sweep)。动画师把前冲做得再猛,撞墙就会停——这正是 root motion 攻击比代码位移手感好的原因,碰撞一致性是免费的。代价是穿墙动画需要动画师预留缓冲,代码救不了。
网络同步:为什么服务器不拽 root
Root Motion 的网络同步是 CharacterMovement 预测模型上叠的一层。客户端预测播放 Montage、本地积分 root motion 进胶囊体;关键是Montage 本身要复制(UAnimInstance::Montage_ 系列走属性同步,ActiveMontage 在 FAnimMontageInstance 上标记 dirty),两端各自本地求 root motion。
服务器校正回的是移动数据(位置/速度/时间戳),不是动画数据。服务器跑自己的权威版本:它也在播同一个 Montage(复制保证),按服务器时间积分 root motion 算权威位置。客户端收到 ServerMove 校正后,胶囊体位置被拉回——但 Montage 播放位置不被拉,靠的是校正重放(ClientAdjustPosition → ReplayMove)时按校正后的时间戳重新积分。这就是为什么 root motion 同步对两端播放速率一致敏感:Montage 播放速率没同步好,同样的动画窗口两端积出的位移不同,服务器每次都发现客户端"多走了",表现就是频繁拽人。
时间戳对齐的公式化描述:服务器权威位移窗口 $[t_s, t_s+\Delta t]$,客户端预测窗口 $[t_c, t_c+\Delta t]$,播放位置偏差 $\delta = t_c - t_s$ 导致位移差
$$\Delta x_{err} \approx v_{root}(t_s) \cdot \delta$$
前冲峰值帧($v_{root}$ 最大处)对 $\delta$ 最敏感——网络抖动时恰好在冲刺帧丢包,校正幅度最大。实战对策:Montage 的 root motion 峰值段避开 0.2 秒内的网络敏感操作,或者干脆在 Montage 里把位移集中到起势段。
落地清单
- 只在 Montage 上用 root motion(
RootMotionFromMontagesOnly),行为可审计; - 位移峰值段不要太长,给网络校正留缓冲帧;
p.RootMotionDebug可视化 root 位移向量,排查"动画在走角色不走"时先看 root track 是否真的抽出来了(有些 DCC 导出的 root 层级不对,提取结果是恒等变换);- 多段 root motion(连击第二段换方向)用 Montage section + root motion 时,确保 section 切换时上一段位移已完整积分进胶囊体,否则切换瞬间会跳变。
整条链的时序: