转载引用请注明出处:🔗https://blog.csdn.net/weixin_44013533/article/details/132081959
作者:CSDN@|Ringleader|
如果本文对你有帮助,不妨点赞收藏关注一下,你的鼓励是我前进最大的动力!ヾ(≧▽≦*)o
主要参考:
注:本文使用的unity版本是2021.3.25f
注:带?的小节是重点或难点
本章主要学习Unity动画基础知识,主要包含:动画片段、Animation编辑器、动画状态机、混合树 blendTree、Root Motion等内容,IK和Playable将在后续博客总结。
本文主要是一些动画基础知识介绍和editor编辑器操作示范,包含较多的案例。
值得一提的是对Root Motion和Bake into Pose进行了深入的剖析,感兴趣的可以直接看结论:root motion个人理解。
动画片段文件其实是一个描述物体随时间变化(如移动、旋转、缩放等)的yaml文本文件
。我们在unity中创建的资源文件,其实大多都是由yaml语言编写的文本文件,这些文本文件包括但不限于这里的“动画片段”,“动画控制器”(状态机),预制件prefeb,甚至是“场景”文件。
以创建开门动画为例,以下为创建过程:
在Project窗口点击动画片段,拖入模型还能预览动画,表明同一动画能在不同对象上复用。
但是不是意味着动画可以随意复用呢?我们创建一个更复杂的动画:双开门实验下。
对于同一个双开门动画文件,当更改子物体名称后,比如将left_pivot改成left_pivotxxxx,左半扇门动画将失效,但更改gate名称和子物体模型,动画仍正常使用。
根本原因还是得看动画文件yaml:
发现文件规定了特定名称子对象的变化曲线,这里的path就是相对父对象的路径,所以如果子对象名称改变,动画就会失效。同样道理,如果父子对象遵循yaml文件描述的路径,动画就能复用。
那么是否能将动画做得更复杂些呢,比如人形动画?当然可以的,上面的pivot就可以类比人体中各个关节,实现人体各个肢体动作动画。
可问题又来了,如果引入的动画和模型中关节名称不一致,岂不是要手动一个一个改?嘿嘿不用,Unity引入了Avatar替身系统,可以帮助我们快速实现人形动画的复用,后面会介绍,先介绍如何引用第三方模型和动画。
常用的动画制作工具有:3ds Max、Maya、Zbrush、Blender等。
还可以使用fuse cc + mixamo 傻瓜式制作模型和添加动作。
steam下载fuse(Adobe现已不支持fuse),并制作模型(类似游戏中的捏脸)
导出为obj,并将文件中所有内容压缩为zip格式
????????????
导入模型
????????????
骨骼自动绑定
????????????
选择动作并浏览
下载动画
例如下载单一动画idle,格式选择FBX for Unity,Skin选择是否包含模型,可以只有动作;fps选择帧率,帧率越大动作精度越高但文件越大,30就可以,unity有插帧算法;keyframe reduction帧压缩,可以设置一个阈值,如果两帧之间的变化小于这个阈值的话就把其中一帧删除掉,这里直接选择none。
也可以下载动作包。pose选项如果动画需要模型,可以选择T pose,或者选择original pose即这个FBX被上传时的姿态。
????????????
7. 导入动画。解压动作包后直接拖入Project窗口
Unity商城下载新的角色模型(比如chan)
并导入,给jump动画添加chan这个角色,发现摆出T pose,动画无法复用,原因就是骨骼结构不一致导致。
所谓的动画,就是对于当前节点,以及当前节点的子节点,一系列属性对于时间的差值控制。
能够应用动画片段的前提就是该节点以及该节点的子节点层级结构,以及所拥有的动画属性与参与动画片段的属性能够对应。
Avatar的存在主要是为了解决人物动画的复用问题,不同来源的FBX中,对于骨骼节点的层级结构,以及命名可能不尽相同,因此Unity以Avatar作为一个中介。不同的FBX都对Avatar中的标准人物骨骼结构去创建映射关系,将动画片段中的人物骨骼节点,映射到标准人物骨骼结构中,建立对标准人物骨骼结构的映射关系。
在Animator状态机进行动画控制时,依照人物模型创建的Avatar,将人物动画片段对标准人物骨骼中的节点控制,映射到当前的人物模型当中,从而就实现了对不同来源FBX中,人物动画的复用。
下面就利用Unity Avatar实现动画复用。
Animation窗口可以手动为对象创建动画和添加动画事件。
Dopesheet是关键帧清单,横轴代表对象的各种可动画属性,纵轴代表时间或者帧,可以通过滚轮调整尺度,按A重置尺度,帧率可以通过右侧···设置和显示。
Animation窗口有两种模式:录制模式和预览模式。
快捷操作:
K Key All Animated,将记录当前属性列表中选中属性的关键帧,如果当前没有选中任何属性,则会记录所有属性。
Shift + K Key All Modified,将动画属性列表中所有已修改的属性的数值记录为关键帧。(我不知道是不是快捷键冲突,这个操作没效果)
下面就可以利用所学尝试K动画了:
除了关键帧清单,还可以用Curves曲线模式查看动画关键点。
一个关键点有两条切线:一条在左侧用于向内的斜坡,另一条在右侧用于向外的斜坡。切线可控制关键点之间的曲线形状。可从许多不同的切线类型中进行选择一种类型,用于控制曲线离开一个关键点并到达下一个关键点的方式。右键单击一个关键点可以选择该关键点的切线类型。
?????
要使动画值在通过关键点时实现平滑变化,左右切线必须共线。以下切线类型可确保平滑:
Clamped Auto
模式下手动调整关键点的切线,则会切换到 Free Smooth
模式。Clamped Auto
。当关键点设置为此模式时,系统会自动设置切线,使曲线通过关键点时保持平滑。但是,与 Clamped Auto
模式相比有两个不同之处:
Free Smooth
的特例)。有时可能不希望曲线在通过关键点时是平滑的。要在曲线中产生急剧变化,请选择 Broken 切线模式之一。
Animation窗口其他操作可以参见:
动画事件允许您在时间轴中的指定点调用对象脚本中的函数。
由动画事件调用的函数也可以接受一个参数。该参数可以是 float、string、int 或 object 引用或 AnimationEvent 对象。AnimationEvent 对象具有一些成员变量,通过这些变量可将浮点、字符串、整数和对象引用以及有关触发函数调用的事件的其他信息一次性传递给该函数。
要将动画事件添加到当前播放头位置的剪辑,请单击 Event 按钮。具体操作如下:
脚本:
// 此 C# 函数可由动画事件调用
using UnityEngine;
using System.Collections;
public class ExampleClass : MonoBehaviour
{
public void PrintEvent(string s)
{
Debug.Log("PrintEvent: " + s + " called at: " + Time.time);
}
}
保存新的空动画剪辑时,Unity 会执行以下操作:
? 创建新的 Animator Controller 资源
? 将新剪辑以默认状态添加到 Animator Controller 中
? 将 Animator 组件添加到要应用动画的游戏对象
? 为 Animator 组件分配新的 Animator Controller
下图以 Animation 窗口中创建新动画剪辑为起点,展示了 Unity 如何分配这些部件:
在大多数情况下,拥有多个动画并在满足某些游戏条件时在这些动画之间切换是很常见的。例如,只要按下空格键,就可以从行走动画切换到跳跃动画。但是,即使您只有一个动画剪辑,仍需要将其放入 Animator Controller 以便将其用于游戏对象。
控制器使用所谓的状态机来管理各种动画状态和它们之间的过渡;状态机可视为一种流程图,或者是在 Unity 中使用可视化编程语言编写的简单程序。
创建、查看和修改 Animator Controller 资源则在 Animator 窗口中操作。
Animator 窗口有两个主要部分:主要网格化布局区域以及左侧 Layers 和 Parameters 面板。
左侧面板:
充当状态机的输入
。主网格区包含四种节点:
Entry 入口。动画状态机会从这个节点开始,根据Transition进入一个默认State。
Any State 任意状态。用于从任意状态转换到特定状态。比如射击类游戏中,如果被子弹打中后,不管当前处于什么状态,都会倒地死亡。
Exit 退出状态机。一般用于嵌套的状态机的退出。
Custom Status 自定义状态节点。可以在空白处右键添加Empty State,也可以将Animation Clip文件拖到Animator窗口中添加一个State。第一个创建的State默认是橘黄色的,代表是默认状态。有一条黄色的箭头从Entry指向橘黄色的State。
参考:
选中一个自定义状态State时,在Inspector中可以看到如下内容:
Motion 可以设置一个Animation Clip,如果是从Animation Clip创建的动画,这里应该已经有动画了,你也可以从工程中选择动画。
Speed 动画的播放速度,负数为倒放,无法通过api修改,需结合下面的multiplier使用
Multiplier 乘数,可以使用一个参数来控制动画的播放速度,动画最终的播放速度会是Speed * Multiplier。
如下是API测试Tag、speed的简单应用(记得给对象添加下面的script,一开始我按键没起作用我还以为是InputSystem问题,忙了半天,发现压根就没添加脚本,尴尬-_-||):
代码:
using UnityEngine;
using UnityEngine.InputSystem;
public class StateMachine : MonoBehaviour
{
private Animator _animator;
private AnimatorStateInfo _animatorStateInfo;
private float animatorScalar = 1f;
void Start()
{
_animator = GetComponent<Animator>();
_animator.SetFloat("Scalar", animatorScalar);
}
void Update()
{
if (Keyboard.current.wKey.isPressed)
{
_animatorStateInfo = _animator.GetCurrentAnimatorStateInfo(0);
//_animator.GetCurrentAnimatorStateInfo(_animator.GetLayerIndex("Base Layer"));
if (_animatorStateInfo.IsTag("不能动"))
{
Debug.Log("不能操作");
return;
}
Debug.Log("可以操作");
animatorScalar += 0.1f;
_animator.SetFloat("Scalar", animatorScalar);
}
if (Keyboard.current.sKey.isPressed)
{
animatorScalar -= 0.1f;
_animator.SetFloat("Scalar", animatorScalar);
}
}
}
Mirror 镜像动画,可以人形动画左右镜像变化。也可以使用一个参数控制。
Cycle Offset 循环动画初始播放的偏移量。偏移量使用的是单位化时间,范围是0-1。也可以使用参数来控制。
Foot IK 只用于人形动画。角色的脚是否使用反向动力学。
简单介绍一下IK。
- 我们常见的由美术制作或者由动作捕捉出来的固定动画,一般都是由骨骼的根节点(对于人性角色来说就是屁股)到末梢骨骼节点依次计算其旋转、位移和缩放 ,来决定每一块骨骼的最终位置,种被称为正向动力学****forward Kinematics。
- 但是在很多时候我们需要反过来计,比如当我们希望角色的手和脚放在一个特定的位置上,举个例子就是爬山的时候,我们需要手和脚接触在岩壁上,此时我们没有现成的动画片段来调用,我们就只能先确定手和脚的位置,再通过手和脚的这个位置,反向计算他们的各个父节点的旋转、位移和缩放了。这种就叫做反向动力学****inverse Kinematics。
那么回到我们这里的Foot IK,Unit使用了Avatar技术来为人形动画提供复用功能。
这种技术很好用但也有些不足,比如当我们把骨骼系统转化为肌肉系统之后,人形角色的双手和双脚的位置会出现一定的偏移。unit为了解决这个问题,提前为我们保存了骨骼系统下,手和脚的正确位置。并把这些位置放置在了4个IK goal上。
Foot IK的作用就是通过反向动力学,把我们脚部的实际位置,向这里的IK Goal的位置拉近一点。
IK Goal位置可以通过脚本修改,同时更改对应的权重。注意,如果权重为1,则完全用IK的位置和旋转;如果权重为0,则完全用原来动画中的位置和旋转。使用脚本使用IK时,我们需要先开启动画层的IKPass,之后在OnAnimatorIK方法中,对IK进行设置。
代码:
public class AnimatorIKFirstTest : MonoBehaviour
{
[Range(0,1)]
public float IK_weight = 0f;
public GameObject ik_object;
private Animator _animator;
private void Start()
{
_animator = GetComponent<Animator>();
}
private void OnAnimatorIK(int layerIndex)
{
_animator.SetIKPosition(AvatarIKGoal.LeftHand, ik_object.transform.position);
// 这个方法用来设置IK的权重,这个IK会和原来的动画进行混合。如果权重为1,则完全用IK的位置旋转;如果权重为0,则完全用原来动画中的位置和旋转
_animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, IK_weight);
}
}
效果:
这里只是对IK简单进行应用,如果能结合射线检测的话,我们就可以根据它开发出脚步适应地形的效果,这个后续深入学习IK的章节再说。
以电梯开门动画为例,演示write default参数对动画的影响。
如下gif所示,电梯能正常升降以及在一楼开闭,但发现只要在二楼开门,电梯会闪现到一楼才能打开,这就很迷惑。
原因就在这个writeDefaults,因为电梯结构如下,电梯的上升和下降控制A对象,电梯的开闭控制BC对象,那么就会出现电梯开门动画没有对A对象属性赋值,如果此时开门动画的writeDefault被勾选,Unity会自动为A对象赋予默认值,而这个值就是在一楼的状态值。所以才会出现在二楼开门会闪现到一楼的情况。
何为“默认值”?(参考:Write Defaults的作用)
当动画机Enable时,Unity会遍历此动画机包含的所有Clip修改了哪些属性,并将OnEnable时这些属性的值作为默认值。
当将open和close的write default勾去掉后,动画逻辑则变回正常。
状态间通过右键添加Transition,同一方向可以设置多个Transition。
一个状态可以转换为多个状态,怎么决定这些变化呢?点击一个状态,其Transition栏列出了从这个状态发出的所有变化,如下:
下面展示Transition顺序对执行的影响。
下面再详细看一下Transition的各个参数
Has Exit Time决定是否在播放完动画后才进行切换,先勾去,先看下面的Conditions。
Conditions依赖Parameter参数
案例:上一节我们用电梯的例子其实有个bug,就是当处于close状态时,在一楼触发down会有个闪现到二楼再下降的动画,在二楼触发up也会有类似bug。
这就需要添加对楼层的判断。我们这里利用float height记录电梯高度,int floor记录电梯层数,bool 1st floor记录是否是1楼。具体代码如下:
public class StateTransition : MonoBehaviour
{
private Animator _animator;
// Start is called before the first frame update
void Start()
{
_animator = GetComponent<Animator>();
}
// Update is called once per frame
void Update()
{
float elevatorHeight = this.transform.position.y;
if (elevatorHeight > 2)
{
_animator.SetBool("1st floor", false);
_animator.SetInteger("floor", 2);
}
else
{
_animator.SetBool("1st floor", true);
_animator.SetInteger("floor", 1);
}
_animator.SetFloat("height", elevatorHeight);
}
}
通过对condition的额外判断,就能正常上下电梯。
Has Exit Time 是否有退出时间条件。注意以下几点:
Exit Time 如果勾选了Has Exit Time,该参数是可以设置的,设置动画退出的单位化时间。例如设置为0.75,代表动画播放到75%时为true,如果没有其他条件,会直接切换到下一个State。
如果exit time小于1,那么state每次循环到对应位置的时候(不管动画是否设置为循环,state总是循环的),该条件都会为true。比如第一次播放到75%,第二次播放到75%……时退出条件都会为true。
如果exit time大于1,该条件只会检测一次。比如exit time为3.5,state的动画会在循环3次后,在播放到第4次的50%时为true。
Fixed Duration 勾选时,下方Transition Duration参数的单位是秒,不勾选时,参数会作为一个百分比。
Transition Duration transition的过渡时间。两个状态在转换时,一般不会瞬间从一个状态转换到另一个状态,而是会经过平滑混合,这个属性就是设置了平滑混合的时间。可以从下图的两个蓝色箭头看出转换的时间。
Transition Offset 目标状态开始播放的时间偏移。比如设置为0.5,则转换到下一个State时,会从50%的位置开始播放。
上面的各个参数都可以用下面的Transition图可视化表示:
Interruption Source和Ordered Interruption 这两个参数可以用来控制transition的打断。下面会进行详解。
在默认的情况下,动画转换时不能被打断的(注意:不是状态State不能打断,是状态转换Transition无法打断)。但是如果你需要对transition进行更多控制,可以通过配置Interruption Source和Order Interruption来满足需求。
Interruption Source有以下五种:None、Current State、Next State、Current State Then Next State、Next State Then Current State。
这五种方式决定此状态转换如何被中断。
这个Interruption理解文字比较麻烦,推荐直接看视频 Unity动画系统详解 十二 动画状态过渡中断/转换打断
参考:
因为要控制角色移动,这里介绍Unity新输入系统的部分内容。
Package Manager 安装 Input System
安装结束后会提示重启Unity,以激活新输入系统
选择角色模型,为角色对象添加Player Input组件,这是new Input System提供的处理玩家输入的组件
点击Create Actions新建玩家输入配置文件,这个文件定义了玩家的各种输入,如移动、转向、射击等,这个默认的配置已经满足目前的需求,不用动。
Player Input组件的Behavior选择Invoke Unity Events,这里的意思是当系统检测到我们的输入时就会调用一些我们写好的方法
展开下面的Events,发现里面的内容和前面的玩家输入配置里内容一一对应。点击加号就能将脚本中的方法注册其中,当系统检测到玩家输入WASD时,就会通过资源文件发现玩家的行为是 move,然后调用所注册的方法。
准备前进、后退动画,配置状态机(两个bool变量作为状态触发条件,注意勾去Has Eixt Time让动画即时切换)
编写角色控制脚本,实现按w角色前进,按s角色后退
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerMoveTest : MonoBehaviour
{
private Animator _animator;
private float thredshold = 0.01f;//防止摇杆漂移或误触
private void Start()
{
_animator = GetComponent<Animator>();
_animator.SetBool("wPress", false);
_animator.SetBool("sPress", false);
}
public void PlayerMove(InputAction.CallbackContext callbackContext)
{
Vector2 movement = callbackContext.ReadValue<Vector2>();
if (movement.y > thredshold)
{
_animator.SetBool("wPress", true);
}
else
{
_animator.SetBool("wPress", false);
}
if (movement.y < -thredshold)
{
_animator.SetBool("sPress", true);
}
else
{
_animator.SetBool("sPress", false);
}
}
}
动画无法循环问题:
有时会发现动画莫名卡住,如下:
后退状态无法循环,查看动画片段LoopTime循环播放没打开,原来是我编辑后没有Apply导致的。(一定要注意编辑后要应用生效!)
手动控制移速:
为了精细化控制移动,关闭角色对象Animator组件的Apply Root Motion选项(这样同时也能解决动画自带位移导致的角色自动偏航和升降问题)
同时希望在使用手柄时,能根据摇杆操作幅度控制不同的移速。于是代码如下:
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerMoveTest : MonoBehaviour
{
private Animator _animator;
private float thredshold = 0.01f;//防止鼠标漂移或误触
public float forward_speed = 2f;
public float backward_speed = 1.5f;
private float current_speed;
private void Start()
{
_animator = GetComponent<Animator>();
_animator.SetBool("wPress", false);
_animator.SetBool("sPress", false);
}
private void Update()
{
Move();
}
private void Move()
{
transform.Translate(Vector3.forward * (Time.deltaTime * current_speed), Space.Self);
}
public void PlayerMove(InputAction.CallbackContext callbackContext)
{
Vector2 movement = callbackContext.ReadValue<Vector2>();
current_speed = 0;
if (movement.y > thredshold)
{
_animator.SetBool("wPress", true);
current_speed = forward_speed * movement.y; // 乘以movement.y用于摇杆,希望根据摇杆幅度进行不同速度的移动
}
else
{
_animator.SetBool("wPress", false);
}
if (movement.y < -thredshold)
{
_animator.SetBool("sPress", true);
current_speed = backward_speed * movement.y;// 注意y∈[-1,1],后退已有负号,backward_speed取正值即可
}
else
{
_animator.SetBool("sPress", false);
}
}
}
效果:
从上面的动画能看出来,当移速低时,动画依旧是大步跨越,导致出现滑步现象。我们希望有办法让角色速度低时,小跨步,速度大时大跨步。但如果是让动画师给不同速度做不同的动画,来解决这个问题,可想而知并不是明智的做法。
这就引出了Unity的BlendTree,利用BlendTree能方便地解决上面的问题,能根据角色移速融合不同的动画片段,来实现角色实际移速和角色移动幅度相一致的效果。
2. 双击进入Blend Tree,查看Blend Tree参数
Blend Type可以选择混合方式,1D方式关联单个float变量来控制动画混合
点击添加按钮,依次将后退、待机、前进三个动画加入
滑动轴进行预览,可以看到多个动画分配不同权重下混合后的效果
3. 关闭Automate Thresholds 并设置对应动画片段的Threshold
4. 添加speed 参数接收角色current_speed,修改状态机,并修改脚本
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerMoveTest : MonoBehaviour
{
private Animator _animator;
private float thredshold = 0.01f;//防止鼠标漂移或误触
public float forward_speed = 2f;
public float backward_speed = 1.5f;
private float current_speed;
private void Start()
{
_animator = GetComponent<Animator>();
_animator.SetFloat("speed", 0f);
}
private void Update()
{
Move();
}
private void Move()
{
transform.Translate(Vector3.forward * (Time.deltaTime * current_speed), Space.Self);
_animator.SetFloat("speed", current_speed);
}
public void PlayerMove(InputAction.CallbackContext callbackContext)
{
Vector2 movement = callbackContext.ReadValue<Vector2>();
current_speed = 0;
if (movement.y > thredshold)
{
current_speed = forward_speed * movement.y; // 乘以movement.y用于摇杆,希望根据摇杆幅度进行不同速度的移动
}
else if (movement.y < -thredshold)
{
current_speed = backward_speed * movement.y;// 注意y∈[-1,1],后退已有负号,backward_speed取正值即可
}
}
}
角色移速和脚步动画的一致性问题已解决,但滑步现象依旧存在,这个后续解决
在游戏中,其实只要有前进和转向即可控制角色移动,那为何需要八个方向的移动呢?因为游戏中通常还有瞄准或锁定功能,在锁定敌人状态下,人物永远面向敌人,只有前进是不够的,所以会有诸如平移/侧步(strafe )、后退等动画。
如《只狼:影逝二度》中两者状态下移动的差异:
下面利用Blend Tree的1D、2D大概复现下类似的移动控制。
主要参考:IGBeginner0116 :在Unity中如何利用Root Motion、Input System和Cinemachine制作一个简单的角色控制器_教程
与前面的BlendTree初始案例类似
输入控制添加冲刺方案(应该叫run的,算了不改了)
Blend Tree设置如下如下
关闭root motion,用脚本控制角色移动。
添加刚体和碰撞体,注意要Freeze X、Z轴的rotation
否则可能出现下面的情况:
4. 为了更好追踪角色,添加cinemachine,Body选择Transposer 的World Space,Aim选择hard look at,具体含义可以参看我cinemachine章节 Unity基础(九)【cinemachine基础(body、aim参数详解)】(多fig动图示范)
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerMoveTest2D : MonoBehaviour
{
private Animator _animator;
private float thredshold = 0.01f;//防止鼠标漂移或误触
public float walk_speed = 1.5f;
public float run_speed = 3.7f;
private bool isRunning = false;
private float current_speed;
private Vector2 movement;
private Quaternion _quaternion;
private void Start()
{
_animator = GetComponent<Animator>();
_animator.SetFloat("speed", 0f);
}
private void Update()
{
Rotate();
Move();
}
private void Move()
{
current_speed = 0;
if (movement.magnitude > thredshold)
{
current_speed = (isRunning ? run_speed : walk_speed) * movement.magnitude;
}
transform.Translate(Vector3.forward * (Time.deltaTime * current_speed), Space.Self);
_animator.SetFloat("speed", current_speed);
}
private void Rotate()
{
if (movement.magnitude > thredshold)
{
Quaternion targetQuaternion = Quaternion.LookRotation(new Vector3(movement.x,0,movement.y), Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetQuaternion, 1000 * Time.deltaTime);
}
}
public void PlayerMove(InputAction.CallbackContext callbackContext)
{
movement = callbackContext.ReadValue<Vector2>();
}
/*用新输入系统添加冲刺sprint按键绑定(键盘的shift键,xbox的B键),Player Input组件监听冲刺按键调用下面的PlayerRun方法*/
public void PlayerRun(InputAction.CallbackContext ctx)
{
float shiftValue = ctx.ReadValue<float>();
Debug.Log(shiftValue);
isRunning = ctx.ReadValue<float>() > 0;
}
}
LockView
的变量作为锁定视角的触发条件2D Simple Directional
,放置各方向行走动画using System;
using UnityEngine;
using UnityEngine.InputSystem;
using Object = System.Object;
public class PlayerMoveTest2D : MonoBehaviour
{
private Animator _animator;
private float thredshold = 0.01f;//防止鼠标漂移或误触
public float walk_speed = 1.5f;
public float walk_speed_with_locked_view = 1f;
public float run_speed = 3.7f;
private bool isRunning = false;
private float current_speed;
private Vector2 movement;
private Quaternion _quaternion;
public float rotateSpeed = 1000f;
/*锁定目标*/
private bool isLockedView = false;
public GameObject lockedObject;// 这里待锁定目标就简单用外部赋值代替了
private void Start()
{
_animator = GetComponent<Animator>();
_animator.SetFloat("speed", 0f);
_animator.SetBool("LockView", false);
_animator.SetFloat("speedX", 0f);
_animator.SetFloat("speedY", 0f);
}
private void Update()
{
LockAim();
Rotate();
Move();
}
private void Move()
{
if (isLockedView)
{
_animator.SetFloat("speedX", movement.x);
_animator.SetFloat("speedY", movement.y);
transform.Translate(new Vector3(movement.x, 0, movement.y) * Time.deltaTime, Space.Self);
}
else if (movement.magnitude > thredshold)
{
current_speed = (isRunning ? run_speed : walk_speed) * movement.magnitude;
_animator.SetFloat("speed", current_speed);
transform.Translate(Vector3.forward * (Time.deltaTime * current_speed), Space.Self);
}
else
{
current_speed = 0;
_animator.SetFloat("speed", current_speed);
}
}
private void Rotate()
{
/*锁定视角情况下方向键不控制方向*/
if (!isLockedView && movement.magnitude > thredshold)
{
Quaternion targetQuaternion = Quaternion.LookRotation(new Vector3(movement.x,0,movement.y), Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetQuaternion, rotateSpeed * Time.deltaTime);
}
}
public void PlayerMove(InputAction.CallbackContext callbackContext)
{
movement = callbackContext.ReadValue<Vector2>();
}
/*用新输入系统添加冲刺sprint按键绑定(键盘的shift键,xbox的B键),Player Input组件监听冲刺按键调用下面的PlayerRun方法*/
public void PlayerRun(InputAction.CallbackContext ctx)
{
isRunning = ctx.ReadValue<float>() > 0;
}
public void PlayerLockedView(InputAction.CallbackContext ctx)
{
isLockedView = !isLockedView;
_animator.SetBool("LockView", isLockedView);
}
/*锁定目标*/
private void LockAim()
{
if (isLockedView)
{
Quaternion targetQuaternion = Quaternion.LookRotation(this.lockedObject.transform.position - transform.position, Vector3.up);
transform.rotation =
Quaternion.RotateTowards(transform.rotation, targetQuaternion, rotateSpeed * Time.deltaTime);
}
}
}
PlayerLockedView
方法这里效果还需要优化,一个是瞄准方向问题(我四元数没学好之后再研究-_-),另一个是镜头问题,锁定模式下镜头应该始终在角色背后,涉及镜头切换问题,同时角色和锁定的对象作为目标组,后面一并研究。
通过案例大概了解了这个Blend Tree的用法,接下来详细了解这个BlendTree。
我理解的Blend Type就是根据所混合的多个动画间差异属性个数和特点来选择的。比如走和跑差异在线速度,左转、右转差异在角速度,所以这种就适合用1D BlendTree来混合。
至于“向前走”,“向后走”,“向左走”,“向右走”多个动画,差异在方向上,而方向需要两个参数来表示,所以适合用2D Blend Tree。2D算法有多个,分别适合不同场合。至于选择何种算法,需要先了解不同算法的原理,详细内容参看后面的双变量混合——权重分配算法浅析章节。
前两类都是通过算法计算各动画权重,如果想手动精确控制各权重就用Direct混合。
1D:1D 混合根据单个参数来混合子运动。
2D Simple Directional(2D简单方向):当你的运动代表不同的方向,如“向前走”,“向后走”,“向左走”,“向右走”,或“向上瞄准”,“向下瞄准”,“左瞄“和”右瞄“。当然了,可以在(0,0)处包含一个默认动作类似“空闲站立”或“直线瞄准”。与1D混合树不同的是,2D Simple Directional不是在同一个方向上的多个动作,比如“走”和“跑”。
2D Freeform Directional(2D自由方向):动画运用有不同的方向时,也可以使用这种混合类型:可以在同一个方向上有多个运动,例如“走”和“跑”。在Freeform Directional类型中,(0,0)位置必须包含一个默认动作,如“空闲站立”。
2D Freeform Cartesian(2D自由笛卡儿):当混合的2个参数不代表不同的方向时使用。使用Freeform Cartesian,参数X和Y可以表示不同的概念类型,例如角速度和线速度。举个例子:“向前走不转向”,“向前跑不转向”,“向前走并右转”,“向前跑并右转”等动作。
Direct:此类型的混合树让用户直接控制每个节点的权重。适用于面部形状或随机空闲混合。
单变量混合树中可以拖动蓝色三角形来更改motion的阈值。如果未启用“Automate Thresholds”开关,则可以手动更改Threshold 数值来改变motion阈值。
双变量混合中可以拖动混合图中蓝点来更改motion的位置,或者手动改变 Pos X 和 Pos Y 的值。
Compute Thresholds或Compute Positions 下拉框中,可以根据动画中的数据,自动计算阈值或位置。
1d:
2d:
二维混合的Compute Positions中,我们可以对X轴/Y轴应用一维混合中计算模式,也可以通过另外两种计算模式来同时计算XY的位置坐标.
Velocity XZ 即X轴对应动画中根节点的VelocityX,Y轴对应动画中根节点的VelocityZ,按照动画在XZ平面的运动方向来进行坐标分配
Speed And AngelSpeed 则是Y轴按照动画根节点的速度计算坐标,而X轴按照根节点的角速度计算坐标。
通过动画速度这一列可以调节动画的播放速度,比如你想让跑步的动画播放速度变为原来的2倍,可以设置为2。
Adjust Time Scale > Homogeneous Speed按钮可以将动画的播放速度调整到动画列表中所有动画速度的平均值。先将所有动画的平均速度算出来,然后通过调节动画的speed让所有动画的速度都一致。
上面复选框可以左右镜像一个humanoid类型的动画Clip。这个功能可以使用同一个动画创建出来两个方向的动画,可以节省一倍的存储空间和内存。
比如一个向左走的动画,通过镜像可以创建出一个向右走的动画。
申明:以下权重计算原理整理自up IGBeginner0116 的视频。
范例动画
(example motion):混合树中这些动画,我们称之为具体动画或者范例动画参数空间
(parameter space):由这两个参数构成的坐标空间,我们称之为参数空间范例点
(example point):范例动画在参数空间上的位置,也就是这里的蓝点,我们称之为范例点。蓝圈儿表示当前状态下相关动画片段在这个混合树中所占的权重,权重越大蓝圈就越大采样点
:而这里的红点就是游戏中实际的参数的值,我们称之为采样点目标:在确定了采样点后,我们需要给参数空间上的每一个范例点所对应的范例动画计算出一个权重值。
权重计算原则:关于如何计算权重值,在Johansen(Mecanim动画系统主持开发工程师)的硕士论文第六章提到了七个基本原则:
三角形顶点坐标V1、V2、V3,对于没加权的重心 V =(V1+V2+V3)/3,加上权重λ后变为V = (λ1V1+λ2V2+λ3V3)/(λ1+λ2+λ3),代入各顶点和采样点坐标值后,外加已知条件λ1+λ2+λ3=1,可得λ1 λ2 λ3的三元一次方程,即可求得各顶点的权重值。
2D Simple Directional混合树的优点显而易见,它性能好,我们要做的只是遍历一遍所有的动画片段,然后再计算一下重心就可以了。缺点也是显而易见的,它不允许从原点出发的同一个方向上出现两个动画片段,因为这样就可能会找不出合适的三角形;同样它也不允许原点的某个方向上的180度以内都没有顶点,因为这样也会找不到合适的三角形。
考虑到它的性能比较好,当我们不需要在某个方向上混合多个动画时,我们应该首选2D Simple Directional混合树。但是我们往往需要在同一个方向上混合多个动画片段,比如我们不只需要走还需要跑的时候,此时我们就需要寻找别的替代方案了。
这个算法背后就是Johansen硕士论文中的梯度带插值算法(Gradient Band Interpolation)
首先第i个范例动画,它在参数空间下有一个坐标pi,这个范例动画对参数空间中的任意一点p都有一个影响值hi,这个hi如何求呢?首先我们遍历一下除i点外的其他所有范例点,针对每一个范例点Pj,求出PiP在PiPj上的投影长度,以及它和PiPj长度的比例。这个比值在一定程度上反映了Pi点Pj点它们和P点的距离关系。
现在我们可以观察到这个比值越大,Pi的影响值就越小,这样是有一些反直观的,所以我们用1减去这个比值,这样就得到了范例点i相对于范例点j的相对影响力。垂直于PiPj可以画出两条线,两线外就是影响力为1或0的区域,中间则是介于0和1的区域。
我们在参数空间里观察一下这个算法,可以看到P1点和P2点的相对影响力关系,这里构成了一条递减的梯度带,这也就是该算法被称为梯度带算法的原因。
我们接看用同样的方式遍历所有的范例点,计算出Pi与它们之间的相对影响力,然后我们在所有的这些相对影响力中求它们的最小值,这样就得到了Pi在整个参数空间下的影响力hi。
在参数空间的示意图上可以看出它大概长这样:
那么把Pi点的影响力hi,除以所有范例点的影响力的和,也就是我们对它做了归一化之后,就可以得到Pi点的权重Wi了。这就是2D Freeform Cartesian的权重分配原理。
当前的梯度代算法,基本符合我们之前提到的所有基本原则,但是在实际应用中却存在着一定的问题。
当我们需要制作一个包括了各个方向,比如行走和奔跑的混合树,参数落在两个方向的跑中间,我们希望是两个方向跑的融合,但此时却融合了走的动画,2D Freeform Cartesian是不能够满足我们的需求的。
所以Johansen还提出了一个梯度带算法的加强版,也就是极坐标下的梯度带算法,Gradient Bands in Polar Space。对应到unity这里就是2d freeform directional混合树背后的算法。
Johansen定义了极坐标下的向量
代入上面的权重公式得到:
从公式中可以得出,当i j范例点和原点距离相等时,即|Pi|=|Pj|,则权重与∠(P,Pi)成比例,从图片上看就是梯度带沿角度平均分布。
当两个范例点相对于原点同方向,那么∠(Pi,Pj)=0,权重则与|P|-|Pi|成比例,从图片上看就是梯度带成圆环分布。
其他情况梯度带遵循阿基米德螺旋分布。
极坐标下的梯度带算法可以相对精确地对不同方向和速度的动画进行插值,其时间复杂度是O(n^2),算不上高效,所以Uniy在2D Freeform Directional混合树中对该算法进行了大量的优化。
主要参考:
如果动画行走播放速度和角色实际位移速度不一致,就会出现“滑步”现象,如果仅仅这样还可以通过代码调整动画或者控制角色位移来实现同步,但对于一些速度非线性的复杂动画是很难模拟的,不如将移动交给动画师决定,也就是让动画来驱动位移,引擎计算动画的根运动数据,将其应用在角色上,驱动角色移动,就能让角色的移动与动画完美匹配。
Root Motion直译就是“根骨骼运动”,在Unity中Root Motion的确切含义和作用定义比较模糊,说法众说不一(或者是角度不一样?),这里简单罗列一些说法:
[Unity3D]什么是Apply Root Motion?什么是Bake into Pose?
Apply Root Motion:应用根部动画。
作用:当你使用的骨骼动画会使得整个对象发生位移或偏转的时候,勾选Animator下的Apply Root Motion选项,会将位移和偏转实时附加到父物体上。
Bake into Pose:烘焙成姿势。
作用:将整个骨骼动画对角色产生的位移和偏转,转化为姿势,或者说BodyPose,接下来无论你是否勾选Apply Root Motion,都将不会使得父级的Transform发生变化。
Randy
所谓根节点就是一个角色的最高父节点,这个最高父节点在虚拟世界中的位置将决定角色的位置,根节点运动时,整个角色就会开始运动(走动,跑动)。
在Animator组件中我们可以选定是否要引入rootMotion,即将动画中根节点的位移,植入到Animator所在的物体上。 根节点的位移植入,可以理解为,是否要让动画师来决定角色的位移。
在自制的非骨骼动画中,root motion会把动画文件中描述的对游戏对象的坐标和角度值,转换为相对位移和相对转角,并以此来移动游戏对象。
而在generic动画中root motion会把动画文件中描述的根骨骼坐标值和角度值转换为相对位移和相对转角,并以此来移动游戏对象。
那么到了humanoid动画里,由于使用了Avatar系统,动画文件不再包含对具体骨骼的描述,我们自然也就无法通过指定根骨骼来应用root motion,unity为了解决这个问题,在humanoid动画中通过分析骨骼的结构,计算出模型的重心center of mass,这个重心也可以被称为body transform。(在预览动画这里激活这个选项,大家可以在脚本中通过animator.bodyPosition和bodyRotation来访问它的坐标和方向)。接下来unity会根据具体动画计算重心在水平平面的投影,并把这个投影当做root motion的“根骨骼节点”来对待,这个点被称作root transform。(在脚本中我们可以通过animator.rootPosition和rootRotation来访问它的坐标和指向)这个投影并不是直着投射下来的,这中间经历了一些其他的计算。
大家也可以看到游戏对象所处的位置其实就在这个 root transform上,也就是说在应用root motion时,unity会把root transform上的位移应用到游戏对象上。简单的来说,我们可以把这个unity计算出来的root transform,当做humanoid动画下代表rootmotion的根骨骼节点。
总结来说 humanoid动画下root motion的原理就是:在humanoid动画中,Unity会计算出一个Root Transform,RootMotion会把动画文件中描述的RootTransform的坐标和角度值,转换为相对位移和相对转角,并以此来移动游戏对象。 通过这种方式,我们就可以在不同的骨骼结构上复用同一个rootmotion动画。
那到底这个Root Motion到底什么含义呢,我们一步步探究。先从基础概念看起。
动画分含root motion动画和in place动画
mixamo可以选择动画为in place
root motion动画转in place动画原理大概就是将角色重心的移动轨迹压缩到一个点上,具体参见:Stay In Place Animation
Body Transform
、Root transform
、root.q
、root.t
相信大家在学习root motion时早被上面这些名词弄得晕头转向,它们分别是什么含义,有什么区别?
我们先看一下官网怎么说的:
身体变换(Body Transform)
身体变换是角色的质心。它用于 Mecanim 的重定向引擎,并提供最稳定的移位模型。身体方向是相对于 Avatar T 形姿势的下身和上身方向的平均值。
身体变换和方向存储在动画剪辑中(使用 Avatar 中设置的肌肉定义)。它们是动画剪辑中存储的唯一世界空间曲线。所有其他:肌肉曲线和 IK(反向动力学)目标(手和脚)都是相对于身体变换进行存储的。
根变换(Root Transform)
根变换是身体变换在 Y 平面上的投影,并在运行时计算。在每一帧都会计算根变换的变化。变换的此变化随后应用于游戏对象以使其移动。
根据我的理解,官网所说的是针对humanoid动画而言,body transform对generic动画也同样使用,但它就不一定是角色重心,而是开发者所指定的root node。
其次上面所说的“动画剪辑中存储的唯一世界空间曲线”应该就是指root.t
和root.q
,这个说法跟Yene大佬的说法“RootMotionCurve是程序内部生成的对象,它的特殊之处在于储存的值是相对整个模型空间的变换,而不是相对父节点的局部变换”一致。
我的理解:当使用Humanoid动画或者Generic动画添加root node后,动画clip会根据重心或者所指定的root node计算得到RootMotionCurve,这个“根运动曲线”在动画clip中表现形式就是root.t
和root.q
。而Root transform估计是根据上面的RootMotionCurve和具体root transform三个维度配置计算得到,然后根据开发者设置,将各维度root transform分量,选择bake into pose还是应用到父对象。
其中root.t
和root.q
可以在运行时更改(不过要把clip拷贝出来)
下图是Humanoid动画Apply root motion 且都Non-baked情况下,Root Transform各参数对比。
这里再放一个骨骼与父节点错位情况下,root motion对动画的影响
可以看到动画并不是从骨骼初始位置开始播放,还是对齐到模型原点,这可能就是Based Upon(at Start)的含义。
Root Motion总共有三种状态,勾选,不勾选,以及Handled by Script(脚本实现OnAnimatorMove()方法)
勾选Apply root motion和OnAnimatorMove()方法中使用animator.ApplyBuiltinRootMotion()
等效。
根据Animator组件所在的物体,(Apply Root Motion时)位移/角位移的植入有三种不同的表现形式:
- 物体没有Rigdbody,也没有character controller组件,则位移/角位移将被植入到Transform变换中
- 物体拥有Rigdbody组件,则位移/角位移将被植入到Rigdbody的速度和角速度中,根节点的位移和角位移将借助物理系统的刚体速度,刚体角速度来体现
- 物体拥有Character Controller组件,则位移/角位移将被植入到Character Controller的速度和角速度中,根节点的位移和角位移,将借助Character Controller的运动来实现
如上所说,当没有Rigdbody和character controller组件时,在方法中更新对象的Transform transform.position += _animator.deltaPosition; transform.rotation *= _animator.deltaRotation;
也能达到同样的效果。(注意Apply root motion是考虑到对象缩放值的,对于同样动画,scale小的移动幅度也会比例缩小。)
如下是对legacy动画应用Root Motion的对比图:
legacy动画的Root Motion比较简单,应用(或等效应用)Root Motion,对象就能连续运动而不出现“闪回”现象。
但对于Mecanim动画,情况就会变复杂,因为影响对象运动除了Apply Root Motion,还有Bake into pose。
注意:generic 没设置avatar和root node时正常不会有root motion相关的设置的,但如果先设置avatar 和root node,然后再改为no avatar,animation clip的root transform相关配置依然可见,动画的root.q,root.t也保留,这可能是bug。
可以看到bake into pose和apply root motion一定程度上是互斥的,一个分量的root transform选择baked into pose,那这个分量就不作用于父节点。
humanoid动画,root motion呈现多种状态:
其实2、3、4是同类型,5与6也是同类型,都是将root transform对应分量应用到body中,只是因为xz分量无法loop match,所以闪回效果很明显,如果换成旋转类的动画,勾选旋转bake一样会产生闪回效果。
所以上面主要分四类:
对比Humanoid和Generic动画发现,其差异在第一种情况,apply root motion和bake into pose都不作用情况下,generic的效果等同三个维度都baked,而humanoid呈现原地运动现象。
我们知道root motion除了Apply和Non-Apply还有第三种状态Handle by script,就是在脚本添加OnAnimatorMove()方法,在此方法中添加 transform.position += _animator.deltaPosition; transform.rotation *= _animator.deltaRotation;
可以做到等效ApplyRootMotion的效果,那么结合bake into pose会有什么效果呢?
public class HandleByScriptEqualMethod : MonoBehaviour
{
private Animator _animator;
void Start()
{
_animator = GetComponent<Animator>();
}
private void OnAnimatorMove()
{
transform.position += _animator.deltaPosition;
transform.rotation *= _animator.deltaRotation;
}
private void Update()
{
Debug.Log("deltaPosition = " + _animator.deltaPosition*100f);
}
}
如下是Humanoid动画各参数对比效果:
可以看到,handle by script 的Root Motion控制和Apply Root Motion效果是一样的。
但比较奇怪的是transform.position += _animator.deltaPosition;
这一步明明更新了父节点的transform了,上图中父节点依然无法移动?除非这个_animator.deltaPosition
值为0。
打印对比baked和Non-Baked的deltaPosition发现,zx平面baked into pose时deltaPosition的xz分量为0,这就是同是父节点transform更新情况下,bake时父节点无法移动的原因。
经过上面的探究,我们可以试着总结下,Bake into pose
和Apply root motion
到底做了什么:
当我们使用humanoid
动画或者generic
动画添加root node
节点后,Mecanim就会启用root motion
相关功能,系统会根据动画片段计算出包含三个维度的root transform
,这个root transform
就是原动画的根节点或者重心运动轨迹数据。开发者可以根据需求将各root transform
分量bake到body tranform
中,指导Avatar系统模型的运动(这也许就是上面generic和humanoid动画在未apply root motion且未bake into pose情况存在差异的原因所在)。
如果对骨骼模型的父对象使用apply root motion
,系统会将未bake into pose
的root transform
分量作用到父对象的transform
中,然后骨骼节点将跟随父对象移动。所以这也是为什么在apply root motion和未bake into pose情况下,模型能连续运动的原因,其实这可以看作“被父节点的移动带动的原地动画”。
transform.position += animator.deltaPosition;
勾选bake into pose的维度其对应的deltaPosition和deltaRotation分量就为0,所以就产生父对象运动和不运动的差异。至于如何计算delta的估计和Avatar系统有关,目前不作考究。简单配置:
简单的运动控制代码如下,但其中还存在一些问题,可以继续优化:
public class PlayerController : MonoBehaviour
{
private Animator _animator;
private static readonly int Speed = Animator.StringToHash("speed");
private Vector2 movement;
private bool isRunning = false;
private float deadzone = 0.01f;
public float walkSpeed;
public float runSpeed;
void Start()
{
_animator = GetComponent<Animator>();
_animator.SetFloat(Speed,0);
var humanScale = _animator.humanScale;
walkSpeed = 1.34f;
runSpeed = 3.74f;
}
void Update()
{
Rotate();
Move();
}
// 处理转向
private void Rotate()
{
// 按键释放时会读取值为0,导致角色方向重置,过滤掉
if (movement.magnitude > deadzone)
{
Quaternion targetQuaternion = Quaternion.LookRotation(new Vector3(movement.x,0,movement.y));
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetQuaternion, 1000f*Time.deltaTime);
}
}
// 处理移动, 这里直接更新状态机的Parameter就行,具体移动交由root motion
private void Move()
{
float currentSpeed = (isRunning ? runSpeed : walkSpeed) * movement.magnitude;
_animator.SetFloat(Speed,currentSpeed);
}
// 角色移动事件回调方法
public void PlayerMove(InputAction.CallbackContext ctx)
{
movement = ctx.ReadValue<Vector2>();
}
// 角色奔跑事件回调方法
public void PlayerRun(InputAction.CallbackContext ctx)
{
switch (ctx.phase )
{
case InputActionPhase.Performed:
isRunning = true;
break;
case InputActionPhase.Canceled:
isRunning = false;
break;
}
}
}
可能存在的问题:
角色与其他物体碰撞后不受控旋转
衔接丝滑问题,角色反应不灵敏等:关闭Has Exit Time,调整transition duration等
角色启停过快动画不丝滑,以及频繁切换动画导致鬼畜动作:给运动加差值函数。
多个角色速度不一致。
_animator.speed /= _animator.humanScale;
使用root motion后最大移动速度已被动画所默认,那么如果想调整这个移动速度,就可以通过调整animation speed来实现。动画播放速度= 角色目标移速 / 动画模型运动速度。
新发现: 状态机还分编辑态和运行态,运行态也可以分不同副本,使用animator controller时需要注意
因为同一个状态机可以给不同对象使用,但运行却可以分别调参,但更新状态机会同步到所有编辑态和运行态。(这里编辑态和运行态是我自己的理解)
优化后的代码:
public class PlayerController : MonoBehaviour
{
private Animator _animator;
private static readonly int Speed = Animator.StringToHash("speed");
private Vector2 movement;
private bool isRunning = false;
public float walkSpeed;
public float runSpeed;
public float accelerateDamping = 10f;//速度切换的阻尼感
public float decelerateDamping = 5f;//速度切换的阻尼感
private float currentSpeed;
void Start()
{
_animator = GetComponent<Animator>();
_animator.SetFloat(Speed,0);
var humanScale = _animator.humanScale;
walkSpeed = 1.34f;
runSpeed = 3.74f;
_animator.speed /= humanScale;
}
void Update()
{
Rotate();
Move();
}
// 处理转向
private void Rotate()
{
// 按键释放时会读取值为0,导致角色方向重置,先过滤
if (!Mathf.Approximately(movement.magnitude , 0))
{
Quaternion targetQuaternion = Quaternion.LookRotation(new Vector3(movement.x,0,movement.y));
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetQuaternion, 1000f*Time.deltaTime);
}
}
// 处理移动, 这里直接更新状态机的Parameter就行,具体移动交由root motion
private void Move()
{
var targetSpeed = (isRunning ? runSpeed : walkSpeed) * movement.magnitude;
// 减速时加个阻尼
if (targetSpeed < currentSpeed)
{
currentSpeed = Mathf.Lerp(currentSpeed, targetSpeed, decelerateDamping * Time.deltaTime);
}
else
{
currentSpeed = Mathf.Lerp(currentSpeed, targetSpeed, accelerateDamping * Time.deltaTime);
}
if (Mathf.Approximately(currentSpeed,0))
{
currentSpeed = 0;
}
_animator.SetFloat(Speed,currentSpeed);
}
// 角色移动事件回调方法
public void PlayerMove(InputAction.CallbackContext ctx)
{
movement = ctx.ReadValue<Vector2>();
}
// 角色奔跑事件回调方法
public void PlayerRun(InputAction.CallbackContext ctx)
{
switch (ctx.phase )
{
case InputActionPhase.Performed:
isRunning = true;
break;
case InputActionPhase.Canceled:
isRunning = false;
break;
}
}
}
优化前后对比如下,可以看启停加入差值后,角色移动变得更加丝滑真实。
通过实践,初步了解了Unity 的Animation、状态机、root motion、bake into pose、blendTree等知识,但要在实际项目中使用动画,还需要结合IK甚至是Playable等内容,抑或是第三方动画插件,当然也涉及刚体、碰撞体等知识,还有跳跃等复杂动画判断,需要一步一步来。