【unity实战】OnAnimatorMove+Root Motion+Blend Tree+CharacterController解决Animator动画和位移不同步问题,完美实现移动动画动作匹配
前言
实际项目中,我们肯定会使用Blend Tree动画混合树做动画的切换,而不是像之前一样直接使用Root Motion进行动画的切换,没看过的推荐先去看看:【unity实战】Animator启用root motion根运动动画,实现完美的动画动作匹配

如果你还了解
Blend Tree动画混合树的知识,可以先去查看:【unity游戏开发入门到精通——动画篇】动画混合(Blend Tree)
一、在blend tree中使用root motion
1、新增1D混合树
这里我们新增一个1D混合树做演示,并将待机、前进、后退的动画拖入进混合树。


2、如何填写这里的Threshold阈值?
这里的Threshold阈值表示对应动作的临界阈值,当等于这个值时动作权重最大(完全播放该动作)。

Threshold最合理的参数值应该是每个动画自己的移动速度,这样在我们代码里的参数就可以直接反映出root motion的最终移动速度。
3、那么我们怎么知道这里的root motion的实际位移速度是多少呢?
我们可以看下面的compute thresholds自动计算阈值,它的意思就是根据root motion中某些数值自动计算这里的阈值,打开之后,我们可以看见六个选项。

speed是root motion的移动速度或者说它的速度的绝对值,也就相当于下面这个三维向量velocity的长度。Velocity这三个分别代表了root motion在xyz三个方向上的位移速度。angular speed则是root motion的旋转速度。第一个单位是弧度每秒,第二个单位是角度每秒,如果你的角色需要使用root motion控制转身的话,会用到他们。
4、自动计算出阈值
我们这里用的动画是待机、前进以及后退,所以我们只关心Z方向,也就是“前后”方向上的位移速度。所以我们可以选择第四个velocity Z自动计算出阈值。效果如下:

我们已经知道这里的阈值其实就是这个动画在z方向上的速度,那么也就是说这个前进动画root motion的速度大约是1.53,后退大约是-1.267,待机是零。
其实这个速度我们也可以在动画文件中去查看

5、代码控制播放动画
在代码里,我们只需要将前进速度设置到1.53f,后退速度设置成1.267f,那么理论上root motion就会以相应的速度带着我们的角色前进或者后退。
新增PlayerController 角色控制代码
using UnityEngine;
public class PlayerController : MonoBehaviour
{
InputController inputController;
Animator animator;
public float forwardSpeed = 1.53f;//前进速度
public float backwardSpeed = 1.267f;//后退速度
float targetSpeed;//目标速度
float currentSpeed;//当前速度
void Awake(){
inputController = GetComponent<InputController>();
animator = GetComponent<Animator>();
}
void Update(){
//计算目标速度
targetSpeed = inputController.moveVector2.y > 0 ? forwardSpeed * inputController.moveVector2.y : backwardSpeed * inputController.moveVector2.y;
// 平滑输入值,避免动画突变
currentSpeed = Mathf.MoveTowards(currentSpeed, targetSpeed, 0.1f * Time.deltaTime);
animator.SetFloat("Speed", currentSpeed);
}
}
注意:这里使用了InputSystem实现输入控制,如果你还不了解如何使用。可以先去看看:【unity游戏开发——InputSystem】
角色绑定脚本组件,Aniamtor开启Root Motion根运动。

运行效果,现在我们已经可以顺利的在blend tree中使用root motion了

二、实现前进的速度和后退的速度一致
前面我们知道前进和后退速度是有一定不一样的。但是实际项目,我们可能希望前进的速度和后退的速度一样,要怎么做呢?
我们可以调整后面的这个动画播放速度。比如现在后退的移动速度比较慢,那么我们就把它的播放速度调快一点,但是我们需要挨个计算这里的播放速度应该设置为多少,比如:
- 计算两个动画的平均速度:
(1.267+1.53) / 2 = 1.3985 - 计算后退速度要达到1.3985,动画的播放速度:
(1.3985 - 1.267) / 1.267 + 1 = 1.104 - 计算前进速度要达到1.3985,动画的播放速度:
(1.3985 - 1.53) / 1.53 + 1 = 0.914
很明显,这很麻烦。但是好在unity在下面为我们提供了自动的计算功能。

homogeneous speed均匀速度:unity就会自动为我们计算出这里的播放速度。Reset Time Scale重置时间比例:可以把播放速度都调回1,也就是重置
如果我们直接点击homogeneous speed均匀速度,会发现播放速度和我们前面计算不太对,明显有问题。

这里需要注意,因为待机动画因为不会位移的,所以是不需要计算这个数值的。我们应该先把待机动画从blend tree里面删掉,再计算一次,然后把它拖回来。这样排除了原地不动的idle对计算的影响,可以看到数据就是我们前面计算的数值了。

运行游戏测试,这时候前进和后退的速度就都一致了,为1.3985。

三、控制角色移动速度
前面我们角色的移动速度都不是由我们决定的,而是由root motion动画决定,那如果我们希望前进的速度是1,那我们该怎么做呢?
1、通过调整动画播放速度控制移动速度
我们可以通过调整这里的播放速度来控制移动速度。比如,如果我希望前进的速度是1,那么只需要把这个播放速度改成1除以它的移动速度,也就是1.53

那么当我把状态机里的speed参数设置为1.53时,角色就会以1的速度前进。计算结果

运行查看效果

2、human scale人体的缩放导致人物移速不一样
很遗憾,问题要比我们想象的更复杂。我们先把播放速度调回1。

实际上比如当这里前进动画的Threshold阈值参数设置为1.53时,root motion的移动速度也极有可能不是1.53。为什么这么说呢?
因为humanoid动画是在不同的人物骨骼上复用动画的一种机制,同样的行走动画在小一点的角色身上自然是要走的慢一点,再大一点的角色身上就会走的快一点。
比如我们这里再放一个大小不一样的角色,他们使用相同的状态机和同样的控制脚本,并且把他们都放在相同位置重叠。

运行游戏,让角色一直往前走。

可以看到,如前面所说大一点的角色会走的快一点。那么如何解决这个问题呢?
我们先来找到这两个人速度不同的原因,在animator下有一个human scale属性,root motion会考虑物体的缩放值Scale,Scale越小,root motion的位移就会越小,前面的问题也是由于两者的大小不同或者说缩放值不同而导致的。human scale记录了Avatar在制作骨骼映射时,对人体或者说骨骼的缩放。我们可以打印这个值来看:
//打印humanScale看看
Debug.Log(animator.name + ":" + animator.humanScale);

那么为了解决这个问题,我们只需要把它们的动画播放速度除以相应的值就可以了。修改代码
void Awake(){
inputController = GetComponent<InputController>();
animator = GetComponent<Animator>();
animator.speed /= animator.humanScale;
}
再次运行查看效果,可以看出通过human scale修正动画的播放速度之后,现在两人的移动速度已经一致了。

3、root motion的移动不一定是匀速的
还有一点需要注意,角色的移动速度并不稳定,这是因为root motion的移动不一定是匀速的。我们之前看到的1.53,其实是这个移动速度的平均值。

这也就是说明在使用某些root motion动画时,我们是无法保证角色匀速移动的。当然大多数开发者并不要求百分百的精准的移动,这种程度已经可以足够满足我们大家的需求了,适当的不精准,反而可以被当做是有灵魂的。
四、把移动的控制权从root motion手中拿回来
1、问题分析
前面我们已经实现了Animator动画和位移的同步,但是假设有这样的需求:
- 动画的移动速度或方向需要动态调整(例如加速、减速、受环境影响)
- 角色需要与环境交互(如碰撞、重力、跳跃)或与其他系统(如载具、导航系统)结合。
既然如此,我们就应该把移动的控制权从root motion手中拿回来了。
2、OnAnimatorMove生命周期方法
那么如何操作呢?关键其实就在OnAnimatorMove函数。
我们查看官方文档:事件函数的执行顺序,可以知道OnAnimatorMove其实是一个特殊的生命周期函数,他会在每个更新帧中为每个 Animator 组件调用一次此函数来修改根运动 (Root Motion)。

如果我们在带有Aniamtor组件的游戏对象上,添加的脚本里使用了OnAnimatorMove方法,unity就不会用动画来驱动游戏对象的移动,转而把控制权交给我们的脚本。
public class RootMotion : MonoBehaviour
{
private void OnAnimatorMove()
{
//代码控制根运动
}
}

如果我们在回调中不执行任何功能,那么此时会发现人物不动了。因为虽然此时我们有根运动信息,但是每帧在执行该函数的回调时没有任何逻辑告知根运动信息该应用到何处,控制权完全交给了自定义的代码。

这时候我们在回调内添加unity提供的一些方法调用,比如:ApplyBuiltinRootMotion(应用默认的跟运动,这和直接勾选ApplyRootMotion的效果一样),此时相当于告诉程序我要把这些根变换信息应用到游戏对象的根节点上,此时人物将再次移动。
public class RootMotion : MonoBehaviour
{
private Animator animator;
private void Start()
{
//获取到动画控制器
animator = gameObject.GetComponent<Animator>();
}
private void OnAnimatorMove()
{
//应用默认根运动
animator.ApplyBuiltinRootMotion();
}
}
但是我们通常不使用ApplyBuiltinRootMotion,因为这就和直接勾选ApplyRootMotion没什么区别。
比如我们想要控制带CharacterController的角色移动,通常都是通animator.deltaPosition位移增量和animator.deltaRotation旋转增量这两个变量控制角色位移和旋转。
//每个更新帧中为每个 Animator 组件调用一次此函数来修改根运动
void OnAnimatorMove()
{
// Vector3 deltaPosition = animator.deltaPosition;//当前帧相对于上一帧的位移增量
// Quaternion deltaRotation = animator.deltaRotation;//当前帧相对于上一帧的旋转增量
// 移动 CharacterController
characterController.Move(animator.deltaPosition);
}
总结
-
自定义根运动处理:当 Animator 组件启用根运动(Root Motion)时,Unity 默认会根据动画的根骨骼位移自动更新 GameObject 的 Transform。通过重写
OnAnimatorMove,开发者可以拦截并自定义这一过程。 -
调用时机:在
fixed update和动画系统的各回调方法之后,在物理计算之前。也就是在 Animator 计算完动画状态后、应用根运动到 Transform 前调用。 -
位移和旋转控制:通常都是通
animator.deltaPosition位移增量和animator.deltaRotation旋转增量这两个变量控制角色位移和旋转
3、实战
3.1 这里我们先给角色添加CharacterController组件来控制角色的移动。
注意,如果对
Character Controller还不了解的,可以查看我这篇文章:【零基础入门unity游戏开发——unity3D篇】3D角色控制器CharacterController的使用、实现俯视角、第三人称角色控制
CharacterController的参数可以根据自己的项目去调节,这里我基本就保持默认了。(如果你发现你的角色脚离地了,可以把中心玩那个往上挪一挪)

3.2 修改动画混合树的配置参数
动画混合树的配置参数也不需要那么复杂了,直接设置前进动画阈值为1,播放速度为1/1.53=0.65;后退动画阈值为-1,播放速度为1/1.267=0.79。其实也就相当于前进速度是1,后退速度也是1。

3.3 修改PlayerController人物控制代码
using UnityEngine;
[RequireComponent(typeof(Animator), typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
InputController inputController;
Animator animator;
CharacterController characterController;
float currentInput; // 当前输入
[SerializeField] private float moveSpeed = 1.0f; // 移动速度
[SerializeField] private float smoothInputSpeed = 0.1f; //平滑输入速度
void Awake(){
inputController = GetComponent<InputController>();
animator = GetComponent<Animator>();
characterController = GetComponent<CharacterController>();
//设置动画缩放因子。排除人体的缩放对速度的影响
animator.speed = 1 / animator.humanScale;
}
//每个更新帧中为每个 Animator 组件调用一次此函数来修改根运动
void OnAnimatorMove()
{
Vector3 deltaPosition = animator.deltaPosition;//当前帧相对于上一帧的位移增量
// Quaternion deltaRotation = animator.deltaRotation;//当前帧相对于上一帧的旋转增量
// 移动 CharacterController
characterController.Move(deltaPosition);
}
void Update(){
// 平滑线性插值,避免动画突变
currentInput = Mathf.Lerp(currentInput, inputController.moveVector2.y, Time.deltaTime * smoothInputSpeed);
// 更新Speed参数
animator.SetFloat("Speed", currentInput);
}
}
3.4 开启Animator的AnimatePhysics动画物理
不要忘记了,去Animator开启AnimatePhysics动画物理,让动画与物理引擎同步刷新。

3.4 运行效果
现在我们就是实现了代码控制角色Root Motion位移,且保持很好的动画和位移同步。

3.5 实现人物速度控制
想要对人物速度的控制,也非常简单,只需要修改animator.speed动画的播放速度即可。
// animator.speed = moveSpeed;//设置动画播放速度
//别忘记加上前面排除人体的缩放对速度的影响
animator.speed = 1 / animator.humanScale * moveSpeed;
但是如果你不想影响整个Animator的播放速度,那么我们就可以在动画只进入这个blend tree之后才改变speed就可以了,动画状态有一个multiplayer属性,可以控制动画的播放速度。

修改代码
//设置动画播放速度
animator.SetFloat("ScaleFactor", 1 / animator.humanScale * moveSpeed);
这样,就只有这个blend tree播放的速度会受到影响,其他的动画则正常播放。运行效果看看。

我们现在就可以很方便的通过控制代码的moveSpeed参数,修改角色的移动速度,比如我把它设置成0.5,看看效果,一切正常。

3.6 CharacterController最小移动距离问题
本来到这里就应该结束了,但是我在测试时发现了一个新的问题,那就是当我们设moveSpeed = 0.1时,人物移动有出现了滑步的问题。

原因就是收CharacterController的这个Min Move Distance最小移动距离的影响,他指的是低于这个数值的移动会被系统忽略掉,由于我们的玩家移动速度太小了,导致被忽略了,所以这个值我们一般设置为0。

结果就没问题了。

参考
https://zhuanlan.zhihu.com/p/428251577
https://www.bilibili.com/video/BV1kZ4y1B7Tc
专栏推荐
完结
好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

更多推荐
所有评论(0)