一、3D 动画的使用
使用导入的 3D 动画:
- 将模型拖入场景中
- 为模型对象添加 Animator 脚本
- 为其创建 Animator Controller 动画控制器(状态机)
- 将想要使用的相关动作,拖入 Animator Controller 动画控制器(状态机)窗口
- 在 Animator Controller 动画控制器(状态机)窗口编辑动画关系(使用之前学习的状态机相关知识)
- 代码控制状态切换
(一)状态设置相关参数
我们可以选中状态机窗口中的某一个状态为其设置相关参数,我们可以称之为动画状态设置
主要设置的是当前状态的播放速度等等细节
在 Animator 窗口中选择其中一个状态,可以看见 Inspector 窗口中如下参数:
-
Motion:分配给此状态的动画剪辑
-
Speed:动画的默认速度
-
Multiplier:控制速度的乘数,如果要使用需要勾选的 Parameter 选中配合的参数 float 类型
-
Motion Time:运动的时间,如果要使用需要勾选的 Parameter 选中配合的参数 float 类型
-
Mirror:是否为状态生成镜像,仅适用于人形动画,如果要配合参数使用选中旁边的 Parameter 关联参数,参数是 bool 类型
-
Cycle Offset:循环偏移时间,如果要配合参数使用选中旁边的 Parameter 关联参数,参数是 float 类型
-
Foot IK:是否遵循 Foot IK,适用于人形人形动画
-
Write Defaults:AnimatorStates 是否为其运动执行未动画化的属性写回默认值。
-
Solo / Mute:仅播放该过渡 / 禁用过渡
Solo 和 Mute 如果一起选择,Mute 优先执行
-
Add Behaviour:添加状态机行为脚本,见 八、状态机行为脚本
(二)连线设置相关参数
我们可以选中状态机窗口中的某一条箭头为其设置相关参数,我们可以称之为动画过渡设置
主要设置的是从一个状态切换到另一个状态时 的表现效果和切换条件
动作和动作之间的连线:
-
Has Exit Time:是否有退出时间,如果勾选,当切换动画时,动画一定是播放到下方的 Exit Time(百分比)的时间时才过渡到下一个动画
-
Exit Time:退出时间
当选择上方的 Has Exit Time 时,该值决定了过渡生效的确切时间,该值可以大于 1
如果小于 1,比如 0.85,表示当动画播放到了 85% 的动画时,就会过渡。
如果大于 1,比如 4.5,那么动画将循环 4.5 次后过渡到下一个动画
-
Fixed Duration:选中后,下方的 Trnaition Duration 过渡持续时间将以秒为单位解读过渡时间,如果不选中,则以百分比解读过渡时间
-
Transition Duration(s / %):过渡持续时间,相当于从该状态切换到下一状态的过渡动画持续的时间,对应下方两个蓝色箭头包裹区域
-
Transition Offset:过渡到目标状态的起始播放的时间偏移。如果是 0 则从目标状态开头开始播放,如果是 0.5 则从目标状态的一半开始播放
你可以理解为切入下一个状态的切入点
-
Interruption Source:该过渡中断的情况
- None:不在添加任何过渡
- Current State:将当前状态过渡排队
- Next State:使下一个状态的过渡进行排队
- Current State Then Next State:将当前状态的过渡和下一个状态的过渡都依次排队
- Next State Then Current State:将下一状态的过渡和当前状态的过渡依次排队
-
Ordered Interruption:当前过渡是否可在不考虑顺序的情况下被其它过渡中断
选中时,找到有效过渡或当前过渡时,会中断
不选中时,找到有效过渡,会中断
-
Conditions:过渡条件
如果没有过渡条件,只会考虑 Exit Time
AnyState 和动作之间的连线:
多出如下参数:
-
Can Transition To self:是否可以过渡到自己
-
Preview source state:预览各种过渡状态
可以查看从任意状态切换到当前状态的过渡效果
注意点:
-
Has Exit Time 是否启用。
如果希望瞬间切换动画不需过多等待,取消该选项
-
Can Transition To self 是否启用。
如果希望自己不要打断自己,取消该选项
二、动画分层和遮罩
动画分层的作用:
游戏中会有这样的需求,人物健康状态时播放正常动画、人物非健康状态时播放特殊动画
比如血量低于一定界限,人物的大部分动作将表现为虚弱状态。我们可以利用动画分层来快速实现这样的功能
动画分层和动画遮罩结合使用:
3D 游戏中我们常常会面对这样的需求,人物站立时会有开枪动作、人物跑动时会有开枪动作、人物蹲下时会有开枪动作
从表现上来看光是开枪动作可能就有 3 种,如果要让美术同学做 3 种开枪动作费时又费资源
我们是否可以这样做,比如开枪动画只影响上半身,下半身根据实际情况播放站立,跑动,蹲下动作,通过上下半身播放不同的动画就可以达到动画的组合播放
动画分层的主要就是达到这两个目的:
- 两套不同层动作的切换
- 结合动画遮罩让两个动画叠加在一起播放,提升动画多样性,节约资源
(一)如何使用动画分层
- 新建一个动画层
- 设置动画层参数
- 在该层中设置状态机(注意:结合遮罩使用时默认状态一般为 Null 状态)
- 根据需求创建动画遮罩
在 Animator 窗口的 Layer 面板中创建一个新的层级。新层级与之前的层级共同控制动画的播放。
-
Weight:权重
当动画同时播放时,如果选择的是叠加状态,会根据权重决叠加的比例
-
Mask:动画遮罩,该层所有动画都只会作用在遮罩上,遮罩外面的部分不受该层动画影响
创建 Avatar 遮罩:
选择遮罩部分:
其中绿色表示该层将动画应用于此区域,红色部分不会受该层动画的影响
-
Blending:混合方式
- Override:覆盖方式,播放该层动画时忽略其他所有层的播放
- Additive:叠加方式,会和其他层动画叠加播放
-
Sync:是否同步其他层
主要用于直接从另一个层复制状态过来,在该层中进行修改,另一个层的设置信息都将保留,我们只需要替换状态对应的动画即可
适用于比如正常状态下有待机走路跑步等动作,但是受伤状态下动作会改变,可以利用同步层方便我们进行编辑
选择后会多一个 Source Layer 表示你要复制哪一层的状态
创建后,New Layer 的状态和状态之间的转换条件将和 MyLayer1 完全相同,只是该层每个状态对应的动画都为 None,自己对应替换需要设置的新状态即可:
-
Timing:当选中 Sync 同步其他层时,该参数激活
选中,会采用折中方案调整同步层上的动画时长(基于权重计算)
不选中,动画时长将使用原始层作为模板
-
IK Pass:反向动力学,之后讲解 IK 的时候再讲
代码控制:
private Animator animator;
animator = this.GetComponent<Animator>();
animator.SetLayerWeight(animator.GetLayerIndex("MyLayer2"), 1);
三、动画 1D 混合
游戏动画中常见的功能就是在两个或者多个相似运动之间进行混合,比如:
-
根据角色的速度来混合行走和奔跑动画
-
根据角色的转向来混合向左或向右倾斜的动作
你可以理解是高级版的动画过渡
之前我们学习的动画过渡是处理两个不同类型动作之间切换的过渡效果,而动画混合是允许合并多个动画来使动画平滑混合
在 Animator Controller 窗口,右键 ->
Create State ->
From New Blend Tree
创建好后,双击 Blend Tree 进入混合树,点击后显示右侧的 Inspector 面板。
这里需要在 Motion 列表中点击 + 号添加至少两个动作字段才有如下的参数界面,混合树 Blend Tree 也可以包含混合树:
-
Parameter:参数,用于控制混合的参数,在参数列表中的参数
-
蓝色图像:可以在这里控制 n 个动画的阈值
-
Motion:关联的动画列表,可以用鼠标改变顺序
-
Threshold:对应动作的临界阈值 当等于这个值时动作权重最大(完全播放该动作)
这个值可以完全自由控制,数值范围不定
-
:控制动作的播放速度
一般默认为 1,做好的动作速度一般不需要修改
-
:是否镜像动作
-
Automate Thresholds:是否自动设置阈值,它会在取值范围内平均分
一般可以取消勾选我们手动控制更准确
-
Compute Thresholds:计算阈值的方式
会从动画剪辑的根运动中获去数据
- Speed:速度
- Velocity X、Y、Z:xyz 上的分速度
- Angular Speed(Rad、Deg):角度或者弧度表示的角速度
举例:
比如你的动画剪辑行走动画时 Speed 速度是 1.5 个单位每秒,慢跑是 2.3 个单位每秒,快跑是 4 个单位每秒,阈值就会根据这些值来进行设置进行混合
-
Adjust Time Scale:调整时间刻度
- Homogeneous Speed:均匀速度
- Reset Time Scale:重置时间刻度
1D 混合就是通过一个参数来混合子运动
注意:往混合树里面加入动作时需要找到动画文件进行关联
四、动画 2D 混合
1D 混合是用一个参数控制动画的混合,之所以叫 1D 是因为一个参数可以看做是 1 维线性的
2D 混合可以简单理解是用两个参数控制动画的混合,之所以叫 2D 是因为两个参数可以看做是 2 维平面 xy 轴的感觉
在之前的 Inspector 窗口中下拉 Blend Type 列表,可以看到有多种不同的混合类型:
-
2D Simple Directional:2D 简单定向模式。运动表示不同方向时使用,比如向前、后、左、右走
-
2D Freeform Directional:2D 自由形式定向模式。同上 运动表示不同方向时使用,但是可以在同一方向上有多个运动,比如向前跑和走
-
2D Freeform Cartesian:2D 自由形式笛卡尔坐标模式。运动不表示不同方向时使用,比如向前走不拐弯、向前跑不拐弯、向前走右转、向前跑右转
-
Direct:直接模式。自由控制每个节点权重,一般做表情动作等
在 Animator 的 Parameters 中创建两个 Float 变量,并在 Blend Tree 中进行关联
我们设置的 x、y 变量对应于 PosX、PosY,相当于二维平面内的坐标。红色点表示当前 x、y 的值对应的坐标,调整红色点的位置可以在下方窗口预览当前对应的动画:
前三种方式只是针对动作的不同采用不同的算法来进行混合的,第四种可以用多个参数进行融合
混合树中还可以再嵌入混合树,使用上是一致的,根据实际情况选择性使用
五、动画子状态机
子状态机顾名思义就是在状态机里还有一个状态机,它的主要作用就是某一个状态是由多个动作状态组合而成的复杂状态
比如某一个技能它是由 3 段动作组合而成的,跳起,攻击,落下
当我们释放这个技能时会连续播放这 3 个动作,那么我们完全可以把他们放到一个子状态机中
在 Animator Controller 窗口中,右键 ->
Create Sub-State Machine
创建出来后,是个类似菱形的图标,我们双击进入子状态机
子状态机的界面和状态机类似。不同的是有一个上层的图标(Up Base Layer),通过连接该状态来转移到上层状态机的某个状态
在这里选择上一层的某个状态
在这里选择回到哪一层,将播放该层的默认动画
六、动画 IK 控制
在状态机的层级设置中,开启 IK 通道
继承 MonoBehavior 的类中,Unity 定义了一个 IK 回调函数:OnAnimatorIK
我们可以在该函数中调用 Unity 提供的 IK 相关 API 来控制 IK
Animator 中的 IK 相关 API:
- SetLookAtWeight:设置头部 IK 权重
- SetLookAtPosition:设置头部 IK 看向位置
- SetIKPositionWeight:设置 IK 位置权重
- SetIKRotationWeight:设置 IK 旋转权重
- SetIKPosition:设置 IK 对应的位置
- SetIKRotation:设置 IK 对应的角度
- AvatarIKGoal 枚举:四肢末端 IK 枚举
private Animator animator;
public Transform pos;
public Transform pos2;
private void OnAnimatorIK(int layerIndex)
{
// 头部 IK 相关
// weight: LookAt全局权重 0 ~ 1
// bodyWeight: LookAt 时身体的权重 0 ~ 1
// headWeight: LookAt 时头部的权重 0 ~ 1
// eyesWeight: LookAt 时眼镜的权重 0 ~ 1
// clampWeight: 0 表示角色运动时不受限制,1 表示角色完全固定无法执行 LookAt,0.5 表示只能够移动范围的一半
animator.SetLookAtWeight(1, 1f, 1f);
animator.SetLookAtPosition(pos.position);
// animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1);
animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 1);
// animator.SetIKPosition(AvatarIKGoal.RightFoot, pos2.position);
animator.SetIKRotation(AvatarIKGoal.RightFoot, pos2.rotation);
}
private void OnAnimatorMove()
{
// 如果动画本身有移动,还需要自己添加代码移动,建议在这里添加
}
IK 在游戏开发中的应用
- 拾取某一件物品
- 持枪或持弓箭瞄准某一个对象
关于 OnAnimatorIK 和 OnAnimatorMove 两个函数的理解:
我们可以简单理解这两个函数是两个和动画相关的特殊生命周期函数,他们在 Update 之后 LateUpdate 之前调用
他们会在每帧的状态机和动画处理完后调用,OnAnimatorIK 在 OnAnimatorMove 之前调用
OnAnimatorIK 中主要处理 IK 运动相关逻辑
OnAnimatorMove 主要处理动画移动以修改根运动的回调逻辑
他们存在的目的只是多了一个调用时机,当每帧的动画和状态机逻辑处理完后再调用
七、动画目标匹配
动画目标匹配主要指的是:当游戏中角色要以某种动作移动,该动作播放完毕后,人物的手或者脚必须落在某一个地方
比如:角色需要跳过踏脚石或者跳跃并抓住房梁,那么这时我们就需要动作目标匹配来达到想要的效果
Unity 中的 Animator 提供了对应的函数来完成该功能,使用步骤是
-
找到动作关键点位置信息(比如起跳点,落地点,简单理解就是真正可能产生位移的动画表现部分)
-
将关键信息传入 MatchTargetAPI 中
private void MatchTarget()
{
// 参数一:目标位置
// 参数二:目标角度
// 参数三:匹配的骨骼位置
// 参数四:位置角度权重
// 参数五:开始位移动作的百分比
// 参数六:结束位移动作的百分比
animator.MatchTarget(targetPos.position, targetPos.rotation, AvatarTarget.RightFoot, new MatchTargetWeightMask(Vector3.one, 1), 0.4f, 0.64f);
}
其中,位移动作百分比在这里查看:
调用匹配动画的时机有一些限制:
-
必须保证动画已经切换到了目标动画上
-
必须保证调用时动画并不是处于过度阶段而真正在播放目标动画:
如果发现匹配不正确,往往都是这两个原因造成的
-
需要开启 Apply Root Motion
通常的做法是,为动画添加事件进行触发,事件选在播放动画后的某一点添加
八、状态机行为脚本
状态机行为脚本时一类特殊的脚本,继承指定的基类,它主要用于关联到状态机中的状态矩形上
我们可以按照一定规则编写脚本,当进入、退出、保持在某一个特定状态时我们可以进行一些逻辑处理
简单解释就是为 Animator Controller 状态机窗口中的某一个状态添加一个脚本,利用这个脚本我们可以做一些特殊功能。比如:
-
进入或退出某一状态时播放声音
-
仅在某些状态下检测一些逻辑,比如是否接触地面等等
-
激活和控制某些状态相关的特效
-
新建一个脚本继承 StateMachineBehaviour 基类
-
实现其中的特定方法进行状态行为监听
- OnStateEnter:进入状态时,第一个 Update 中调用
- OnStateExit:退出状态时,最后一个 Update 中调用
- OnStateIK:OnAnimatorIK 后调用
- OnStateMove:OnAnimatorMove 后调用
- OnStateUpdate:除第一帧和最后一帧,每个 Update 上调用
- OnStateMachineEnter:子状态机进入时调用,第一个 Update 中调用
- OnStateMachineExit:子状态机退出时调用,最后一个 Update 中调用
-
处理对应逻辑
public class Lesson57_StateMachineBehaviour : StateMachineBehaviour
{
public string stateName; // 可以配置字段实现脚本的通用性
public string musicName;
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
if (stateInfo.IsName(stateName))
Debug.Log("进入HumanoidIdle状态");
}
public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
if (stateInfo.IsName("HumanoidIdle"))
Debug.Log("退出HumanoidIdle状态");
}
public override void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
base.OnStateIK(animator, stateInfo, layerIndex);
}
public override void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
base.OnStateMove(animator, stateInfo, layerIndex);
}
public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
base.OnStateUpdate(animator, stateInfo, layerIndex);
if (stateInfo.IsName("HumanoidIdle"))
Debug.Log("处于HumanoidIdle状态");
}
public override void OnStateMachineEnter(Animator animator, int stateMachinePathHash) {
base.OnStateMachineEnter(animator, stateMachinePathHash);
}
public override void OnStateMachineExit(Animator animator, int stateMachinePathHash) {
base.OnStateMachineExit(animator, stateMachinePathHash);
}
}
状态机行为脚本和动画事件如何选择:
- 状态机行为脚本相对动画事件来说更准确,但是使用起来稍微麻烦一些
- 根据实际需求选择使用
九、状态机复用
游戏开发时经常遇到这样的情况:
有 n 个玩家和 n 个怪物,他们的动画状态机行为都是一致的,只是对应的动作不同而已
这时如果我们为他们每一个对象都创建一个状态机进行状态设置和过渡设置无疑是浪费时间的
所以状态机复用就是解决这一问题的方案,主要用于为不同对象使用共同的状态机行为,减少工作量,提升开发效率
-
在 Project 窗口,右键 Create
->
Animator Override Controller -
为 Animator Override Controller 文件在 Inspector 窗口关联基础的 Animator Controller 文件
-
关联需要的动画
十、角色控制器
角色控制器是让角色可以受制于碰撞,但是不会被刚体所牵制
如果我们对角色使用刚体判断碰撞,可能会出现一些奇怪的表现,比如:
-
在斜坡上往下滑动
-
不加约束的情况碰撞可能让自己被撞飞
-
等等
而角色控制器会让角色表现的更加稳定,Unity 提供了角色控制器脚本专门用于控制角色
注意:添加角色控制器后,不用再添加刚体
- 能检测碰撞函数
- 能检测触发器函数
- 能被射线检测
选中角色模型,在 Inspector 窗口中添加如下组件:
- Slope Limit:坡度度数限制,大于该值的斜坡上不去
- Step Offset:台阶偏移值,单位为米,低于这个值的台阶才能上去,该值不能大于角色控制器的高度
- Skin Width:皮肤的宽度,两个碰撞体可以穿透彼此最大的皮肤宽度,较大的值可以减少抖动,较小的值可能导致角色卡住,建议设置为半径的 10%
- MinMoveDistance:最小移动距离,大多数情况下为 0,可以用来减少抖动
- Center:胶囊体相对于模型的中心偏移位置
- Radius:胶囊体半径
- Height:胶囊体高度
代码控制:
public class Lesson59 : MonoBehaviour
{
private CharacterController cc;
private Animator animator;
// Start is called before the first frame update
void Start() {
cc = this.GetComponent<CharacterController>();
animator = this.GetComponent<Animator>();
// 是否接触了地面
if (cc.isGrounded) print("接触地面了");
// 受重力作用的移动
cc.SimpleMove(Vector3.forward * 10 * Time.deltaTime);
// 不受重力作用的移动
cc.Move(Vector3.forward * 10 * Time.deltaTime);
}
// Update is called once per frame
void Update() {
animator.SetInteger("Speed", (int)Input.GetAxisRaw("Vertical"));
cc.Move(this.transform.forward * (80 * Time.deltaTime * Input.GetAxisRaw("Vertical")));
if (cc.isGrounded) print("接触地面了");
}
// 当角色控制器想要判断和别的碰撞器产生碰撞时,使用该函数,相当于替换了 OnCollisionEnter 函数
private void OnControllerColliderHit(ControllerColliderHit hit) {
print(hit.collider.gameObject.name);
}
// 对角色控制器没用
//private void OnCollisionEnter(Collision collision)
//{
// print("碰撞触发");
//}
//可以检测触发器
private void OnTriggerEnter(Collider other) {
print("触发器触发");
}
}