Unity动画控制器的原理就是状态机。

传统的状态机需要在代码里写一个很大的Switch…Case来处理状态,Unity以可视化的形式简化了。而且还提供了子状态机和动画混合功能。由于状态机的原理是同一时刻只能有一个状态,所以Unity还提供了层的概念来将动画分成两个层来同时编辑

系统状态

动画控制器默认提供Any State、Exit、Entry三种状态,我们无法删除默认的三种状态。除了Exit状态以外,自定义的动画状态无法连接到Any State和Entry状态,只能反向连接。

首先是entry状态,他表示当前控制器的初始状态,右击该状态,选择Make Transition命令,即可连接新的状态。状态机会按照连线的状态一次切换动画。橘黄色的状态表示默认状态,如果想切换默认状态,选择另一个状态,然后右键——Set as Layer Default State

接着是Any State状态,比如角色死亡一类的,需要从现有状态切换到另一个动画。可以从Any State状态连线到立刻播放的状态,等它的状态处理完后,再连线回到默认状态,继续原有逻辑

Any State动画状态

最后是Exit状态,状态机可以创建子状态。如果子状态需要回到父状态Base Layer。就需要把子状态连线到Exit状态

切换条件

状态机互相切换需要条件,一个状态机一共支持4种条件:Float、Int、Bool和Trigger。Trigger就像Bool一样,设置true后需要立即设置False。

设置状态机条件

单击两个状态机之间的连线,就可以在inspector面板下方就可以添加满足的条件。

状态机在同一时刻只能执行一个状态,即使两个状态的条件都满足了,也只能进入其中一个。Solo复选框表示即使当前即使别的条件达成了,也只能进入选中Solo状态的动画。Mute表示即使当前条件达成了,也不能进入选中Mute的状态。

Has Exit Time复选框表示不同动画切换时是否启动动画过度,可以调节蓝色半透明区域来设置过渡时间

设置动画的代码

1
2
3
4
5
6
Animator animator = GetComponent<Animator>();
int intID = Animator.StringToHash("New Int");//使用Hash可以解决某些设备编译代码的问题
animator.SetFloat("New Float",1f);
animator.SetInteger(intID,1);
animator.SetBool("New Bool",true);
animator.SetTrigger("New Trigger");

状态机脚本

我们是可以给每个状态添加自己的脚本的。我们可以给每个状态添加脚本来监听一些状态事件,比如状态开启、状态更新和状态退出等。

选中一个状态,然后单击Add Behaviour即可添加脚本。此外,也可以序列化常用数据,如int、String、bool和object等,然后在面板中输入参数即可。例如进入某个状态,播放一个特定的音乐或者做一些特殊的逻辑等。

点击生成的StateMachineBehaviour代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NewMachineBehaviour : StateMachineBehaviour
{
// OnStateEnter 在进入当前状态前调用
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{

}

// OnStateUpdate 在OnStateEnter后、OnStateExit之前每帧调用
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{

}

// OnStateExit 在Transition变化并且当前状态播放结束后调用
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{

}

// OnStateMove 此方法在Animator.OnAnimatorMove()之后调用,这里可以处理骨骼根节点的位移
override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// Implement code that processes and affects root motion
}

// OnStateIK 此方法在Animator.OnAnimatorIK()之后调用,这里可以处理IK动画
override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// Implement code that sets up animation IK (inverse kinematics)
}
}

IK动画

Inverse Kinematics,子骨骼节点带动父骨骼节点运动

如果想要控制人物IK,首先需要在具有骨骼的角色的——Animator面板上的——Layer(Base Layer)点击小齿轮——勾选IK Pass,这样的话我们下面的脚本的OnAnimatorIK才会起效

勾选IK Pass

我们在角色的左手右手分别绑定一个球体,通过移动球体来控制IK影响手部位的动画

球体位置

新建MoveHandIK脚本,在Playing模式下,就可以通过移动圆球来改变胳臂位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MoveHandIK : MonoBehaviour
{
public Animator animator;
public Transform rightHandObj;
public Transform leftHandObj;
private void OnAnimatorIK(int layerIndex)
{
if (animator)
{
//设置动画权重 0是完全依照原动画,1是完全按照新的IK
animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f);

if(rightHandObj != null)
{
//设置右手根据目标点而移动
animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandObj.position);
animator.SetIKRotation(AvatarIKGoal.RightHand, rightHandObj.rotation);
}
if (leftHandObj != null)
{
//设置左手根据目标点而移动
animator.SetIKPosition(AvatarIKGoal.RightHand, leftHandObj.position);
animator.SetIKRotation(AvatarIKGoal.RightHand, leftHandObj.rotation);
}
}
}
}

Root Motion

3维角色的某些动画本身可能就是带位移的,比如挥砍的时候前进几步,为了能在Unity中也能更新Transform信息,需要打开Apply Root Motion复选框。

勾选Apply Root Motion

在Project窗口选择带动画的模型,然后在Inspector面板勾选Bake Into Pose复选框,这表示动画播放完毕后才同步位移信息,不选中表示位移随着动画同时改变

Bake Into Pose

然后我们在脚本中就可以监听Animator的移动更新事件。注意,位移移动事件是在Update()方法之后执行的。在OnAnimatorMove()中控制位移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;

public class CharactorMove : MonoBehaviour
{
public Animator animator;
private void OnAnimatorMove()
{
if (animator)
{
Vector3 newPosition = transform.position;
newPosition.z += 1f * Time.deltaTime;
transform.position = newPosition;
}
}
}

如果不挂载此脚本,人物会根据原动画的运动方式叠加位移

Avatar Mask

Avatar Mask可以限制某些骨骼不播放动画。在Project视图中选择Create→Avatar Mask命令,可以创建它。

如果是人形动画,那么可以直接设置人形遮罩骨骼,其中红色的部分表示禁止这部分骨骼播放动作

如果是使用Generic动画,需要单独选中需要禁止播放动画的骨骼节点

Avatar Mask

想要应用Mask,在Animator面板中的Base Layer中设置(下图为IK Pass的图)

设置Mask

层是用来做动画融合的,同一套骨骼上的两个动画同时播放,例如FPS类游戏或者篮球类游戏。下半身跑动的过程中,上半身还可以旋转投篮等。

为了让上下部分的骨骼相互不影响,可以设置他们的Avatar Mask。

在Animator窗口中,点击Layer面板右上角的加号,即可添加层。可以让Base Layer来处理整体逻辑,而让New Layer专门用来做动画融合,Weight可以设置融合的权重,Mask就是遮罩文件了,Blending设置的Override表示直接覆盖掉其它层的动画。

动画Layer

Blend Tree

Blend Tree用来做动画混合。动画混合和动画融合是不同的概念。

动画混合是指两个动画切换的时候,为了避免太过生硬而混合在一起的过程,比较经典的例子就是控制角色四向跑动时的切换。

在Layer中单击鼠标右键,从弹出的快捷菜单中选择Create State——From New Blend Tree命令即可创建它。

创建混合树

创建完成后,双击打开,点击生成的Blend Tree主干,在Inspector面板中,我们可以自定义BlendType,添加需要混合的motion

一个Blend Tree内部

motion里面就是animation clip,点击添加进去即可,我们关闭Automate Thresholds的勾选,就可以自己编辑不同动画片段的Threshold参数了。

通过下面的代码,我们就可以在运行时改变自己设置的TreeValue参数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SetTreeValue : MonoBehaviour
{
public Animator animator;
private int blendTreeID;
private void Start()
{
blendTreeID = Animator.StringToHash("TreeValue");
}
private void Update()
{
animator.SetFloat(blendTreeID, Input.GetAxis("Horizontal") * 96.0f);
}
}

非运行播放动画

通常,在做编辑器的时候,需要在非运行模式下也能播放动画,我们使用下面的代码来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Animations;
using System.Linq;
#endif
[RequireComponent(typeof(Animator))]
public class EditorPreviewAnim : MonoBehaviour{ }

#if UNITY_EDITOR
[CustomEditor(typeof(EditorPreviewAnim))]
public class EditorPreviewAnimEditor : Editor
{
private AnimationClip[] m_Clips = null;
private EditorPreviewAnim m_Script = null;

private void OnEnable()
{
m_Script = (target as EditorPreviewAnim);
Animator animator = m_Script.GetComponent<Animator>();
AnimatorController controller = (AnimatorController)animator.runtimeAnimatorController;
m_Clips = controller.animationClips;
}

private int m_SelectIndex = 0;
private float m_SliderValue = 0;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();

EditorGUI.BeginChangeCheck();
m_SelectIndex = EditorGUILayout.Popup("AnimationClip", m_SelectIndex, m_Clips.Select(pkg => pkg.name).ToArray());//使用Linq返回动画片段的名称字符串数组

m_SliderValue = EditorGUILayout.Slider("Schedule", m_SliderValue, 0f, 1f);
if (EditorGUI.EndChangeCheck())
{
AnimationClip clip = m_Clips[m_SelectIndex];
float time = clip.length * m_SliderValue;
clip.SampleAnimation(m_Script.gameObject, time);
}
}
}
#endif

Animator Override Controller

前面我们介绍了Animator Controller可以编辑动画之间的切换状态。在游戏中,很多模型动画的切换事件的逻辑可能都是一样的,比如游戏中的很多怪物,它们之间的区别可能就是动画文件不一样,总不能每一个怪物都编辑一套相同的Animator Controller控制行为吧,此时就需要使用Animator Override Controller了

我们首先直接在Project窗口创建Animator Override Controller

创建Animator Override Controller

然后在Inspector面板中,选定切换逻辑一致的Animator,将需要的Animation clip放进去即可

Animator Override Controller Inspector

RuntimeAnimatorController

RuntimeAnimatorController是用来处理Animator Controller动态更新的。

如果我们想在运行时挂载Controller,可以先将Controller放在Resources文件夹里

文件夹

然后将此脚本挂载在需要运行时添加Animator的人物身上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Animator))]
public class Script_07_07 : MonoBehaviour {

public Animator animator;

void OnGUI()
{
if (GUILayout.Button ("<size=50>读取</size>")) {
RuntimeAnimatorController controller =
Resources.Load<RuntimeAnimatorController> ("New Animator Controller");
animator.runtimeAnimatorController = controller;
}
if (GUILayout.Button ("<size=50>删除</size>")) {
animator.runtimeAnimatorController = null;
}
}
}

脚本挂载

运行就可以点击按钮挂载Animator

运行时读取Animator