RootMotion源码简单解析

前言

由于平时经常使用RootMotion动画, 经常遇到一些问题, 所以还是凑空看了一下源码, 大概整理了一下RootMotion动画的运行机制和关键问题

伪代码

先用伪代码简单的描述一下RootMotion的大概运行逻辑, 以单机蒙太奇动画示例

1
2
3
4
5
6
7
8
st=>start: 移动组件::PerformMovement
cd1=>condition: 是否有RootMotion?
c1=>operation: TickCharacterPose获取RootMotionParam
c2=>operation: 通过RootMotionParam计算速度
e=>end: 刷新位置

st->cd1(yes)->c1->c2->e

这当中涉及到的内容非常多, 下面慢慢整理

动画实例部分

上面伪代码中需要的参数RootMotionParams来自于动画资源, 也就是来自动画实例部分

1
2
3
4
5
6
7
8
9
10
11
12
USTRUCT()
struct FRootMotionMovementParams
{
//..............
UPROPERTY()
bool bHasRootMotion;
UPROPERTY()
float BlendWeight;
UPROPERTY()
FTransform RootMotionTransform;
//..........
}

参数主要就上面3个属性


这里先讨论多数情况下的RootMotion, 一般都是由蒙太奇触发, 那么起点就是动画实例(AnimInstance)内的UAnimInstance::Montage_Play()

播放蒙太奇的时候, 会创建一个FAnimMontageInstance*对象来真正的播放蒙太奇动画

image-20210802110915641

也就是是说, 播放蒙太奇会创建一个对象来管理这个蒙太奇动画, 同时也会存放在动画实例里面的数组和Map中


下面要去看动画实例中的函数UAnimInstance::UpdateAnimation

image-20210802111137556

看调用栈, 就是来自PerformMovement()(稍后再看)的调用

然后就调用到UpdateMontage(DeltaSeconds);

image-20210802111258219

Montage_UpdateWeight();这个刷新混合值

UAnimInstance::Montage_Advance() 中重点做了这个事

1
MontageInstance->Advance(DeltaSeconds, RootMotionParams, bUsingBlendedRootMotion);

然后在FAnimMontageInstance::Advance()中重点做了下面的事情

image-20210802111520573

ExtractRootMotionFromTrackRange()函数获取了蒙太奇的位置插值信息, 这个计算内容也比较复杂, 会有片段以及slot等检测, 这里不展开. 得到的信息是本地空间的, 即是一个位移而不是位置, 最后计算的时候会经过转换到世界空间等计算;

那么动画实例这一部分的任务就是计算出RootMotionParams, 等待着移动组件的使用

移动组件部分

UCharacterMovementComponent::PerformMovement()开始

先判断是否开启了RootMotion, 如果开启那么就TickCharacterPose(DeltaSeconds);,同时清理掉当前的RootMotionParams参数

RootMotionParams参数都是用完就清理, 包括在SkeletalMesh中也有类似操作

然后是TickCharacterPose()

image-20210802113219463

这里对Mesh的Tick会调用到之前动画实例部分的代码

下面通过ConsumeRootMotion()获取参数并清理了Mesh中的参数

Mesh中保存里AnimScriptInstance(就是当前的AnimInstance), AnimScriptInstance::ConsumeExtractedRootMotion()可以获取并清理ExtractedRootMotion

然后设置缩放以后设置到当前的RootMotionParams

1
2
UPROPERTY(Replicated)
float AnimRootMotionTranslationScale;

这个缩放参数 目前没有扩展出方便蓝图调用的API, 如果有需要可以手动设置此变量来动态的控制蒙太奇的运动, 参考UE5的WarpMotion组件的思路


接下来跳过一大堆代码,转到下面这里

image-20210802113830692

Velocity变量直接决定下一次渲染我们角色的偏移位置, 那么实际上这里就可以简单理解为之前计算出来的RootMotionParams参数在这里经过了空间转换, 然后通过若干计算转换成了Velocity

偷懒具体计算就不去看了

动画序列RootMotion

用动画序列来执行根骨个运动, 即开启RootMotion From Everything

开启此模式以后, PerformMovement()会一直调用TickCharacterPose()

随后会执行如下代码

image-20210802152027895

这里就要提到动画实例里面的一个多线程辅助类FAnimInstanceProxy,如果当前的RootMotionModeRootMotion From Everything,那么我们在主线程Tick的时候就会立刻去更新FAnimInstanceProxy::TickAssetPlayerInstances(),这样是为了能及时获取到每一帧的Rootmotion信息

image-20210802152320194

随后通过FAnimInstanceProxy类来一直获取RootMotionMovementParams参数

总结

  • 其实RootMotion本质上走的还是移动组件的处理流程,只不过其移动数据是从动画里面提取的。
  • Rootmotion只支持Montage的同步; UE4的状态机太复杂不容易同步
  • 除非是常规的线性运动的Rootmotion,其他的不规则的运动几乎无法预测, 导致同步效果不理想
  • 从性能上说,减少数据的同步和校验可以减少服务器的CPU和内存的压力,所以Rootmotion在网络游戏中的使用要慎重考虑。