本文原创版权归 博客园 阿诚de窝 所有,此处为技术收藏,如有再转,请自觉于文章醒目位置标明原文作者及出处,以示尊重!
作者:阿诚de窝
原文:http://www.cnblogs.com/hammerc/p/4825877.html
Mecanim动画系统是Unity3D4.0开始引入的一套全新的动画系统,主要提供了下面4个方面的功能:
Unity不能制作3D模型和进行骨骼绑定,这些需要在专业的建模软件中由美术进行制作,一般常用的建模软件有下面几种:
当美术制作好了资源以后,我们只需要将这些资源导入到Unity3D中使用即可。
导入到Unity3D的资源需要进行一些简单的设置,主要分为下面两种设置:
通过Unity3D Mecanim提供的各种工具对动画进行配置,使其可以正常播放,常用的Mecanim模块如下:
学习动画系统需要有具体的动画文件及资源,这里我们使用官方提供的示例场景,大家可以在Unity Asset Store中下载到,地址如下:
https://www.assetstore.unity3d.com/en/#!/content/5328
虽然Mecanim是4.0推出的系统,但是我还是使用Unity5.0来进行学习,当然对于Mecanim来说,使用4.x还是5.x都不会有太大的区别。
我们要在Unity3D中使用上模型和动画,需要经过下面几个阶段的制作,下面以一个人形的模型开发为准来介绍。
我们的美术在建模时一般会制作一个称为T-Pose(及双臂张开)的模型。
在之前制作的模型上进行骨骼的绑定,我们需要注意的是骨骼数量不能少于15根,同时要遵循Unity3D的骨骼制作标准,如下:
http://docs.unity3d.com/Manual/Preparingacharacterfromscratch.html
即刷权重的过程,即将骨骼影响的模型每块三角面的权重进行设置,这样我们的骨骼才能影响到模型,产生动画。
Unity3D支持下面几种模型格式的文件导入:
更多的信息可以参考官网的用户手册:http://docs.unity3d.com/Manual/HOWTO-importObject.html
由于类型max等文件是对应建模软件的源文件,Unity3D要支持该文件需要安装3DMax,其它文件也一样,那么比较好的文件格式是fbx,该格式是通用的3D文件格式,而虽然obj也是通用的3D文件格式,但是其不支持动画等特性,一般只用于静态物体。
这里提供Fbx的SDK下载地址:http://www.autodesk.com/products/fbx/overview
关于Fbx的模型文件该如何导出,可以参考Unity3D给出的引导:http://docs.unity3d.com/Manual/HOWTO-exportFBX.html
导入非常简单,只需要在Project面板中右击选择“Import New Asset...”,再弹出菜单中选择我们导出的fbx文件即可。
我们在Project窗口中选中导入的模型后,在Inspector窗口就会看到该模型的配置信息,一共有下面3个标签页,同时会给出官网的解说链接:
Model:http://docs.unity3d.com/Manual/FBXImporter-Model.html
这里需要额外说的就是,当我们的模型需要使用到Light Mapping烘焙系统时,需要勾选Generate Lightmap UVs选项来开启第二套uv。
Rig:http://docs.unity3d.com/Manual/FBXImporter-Rig.html
这里需要额外说的就是,骨骼的类型:None:表示没有骨骼,Legacy:表示为老版本动画系统的骨骼,Generic:表示为通用的骨骼,Humanoid:表示为人形骨骼(设置为人形骨骼可以使用Mecanim专门为人形骨骼开发的各种功能了)。
如果选择了Humanoid类型,可以点击Configura...按钮对人形骨骼和肌肉配置进行修改。
Animations:http://docs.unity3d.com/Manual/FBXImporter-Animations.html
我们先看一张图:
这里我们可以看到,我们在GameObject之上绑定的Animator组件是控制模型进行动画播放的。
而其属性Controller则对应一个Animator Controller文件,该文件可以在Animator窗口中打开,其是被设计为状态机形式的系统,多个状态之间的切换关系可以在该界面进行设置。
Animator Controller中的每个状态则对应一个Animation Clip,每个Animation Clip是一个简单的动画单元,可以在Animation窗口中打开。
如果fbx包含动画文件,则可以在其内部创建多个动画剪辑,每个Animation Clip包含一个简单的动画如Idle、run等等。
老的动画形式的文件,使用XML数据来记录动画信息。
好了,回到我们的主角Animation View中来,对于Animation Clip文件来说,虽然可以使用Animation窗口打开编辑,但是我们一般都不会进行编辑,因为太过于复杂,这部分内容应该在建模软件中由美术编辑好。
那么,Animation窗口可以用来做什么呢?答案是我们可以用它来制作一些简单的动画,比如在一些游戏中,战斗开始前,摄像机会在整个场景中来回移动最后再回到角色后方(目的是让玩家在战斗之前了解敌人所在方位及战斗场景的地形)。
1.首先我们打开Animation窗口,选择场景中的Main Camera,然后点击Animation窗口中的Create按钮,保存我们的文件,Unity会生成两个文件,一个Animator Controller文件及一个Animation文件。
2.下面我们点击Add Property两次,添加position及rotation,如下:
3.将红色线条移到最后一帧,然后点击菜单栏“GameObject”->“Align With View”,将摄像机移动到当前Scene视图的位置同时旋转使其面向Scene视图面向的方向。
4.点击播放按钮即可看到摄像机移动效果。
5.如果还要继续添加可以在后面双击时间轴添加关键帧即可。
此时我们会发现Main Camera已经被添加一个Animator的组件,同时所有的参数也已经设置好了,我们直接点击运行游戏就可以看到效果。
在我们的Animator组件中,有一个Apply Root Motion的选项,该选项没有勾选时,我们的动画会按照世界坐标来移动,即我们的Animation上的数值会直接设定到目标的position之上,而如果勾选,则是在目标的position之上添加我们的动画设定的数值。
对于一个动画,我们还可以为其任意一帧添加一个事件:
1.首先,我们为Main Camera添加一个接收Event事件的组件:
1 using UnityEngine;
2 using System.Collections;
3
4 public class TestAnimEvent : MonoBehaviour
5 {
6 public void ShowAnimMsg(string msg)
7 {
8 print(msg);
9 }
10 }
2.下面我们在Animation窗口中的任意一帧添加一个事件,并调用ShowAnimMsg方法同时传递一个字符串:
3.运行游戏就会看到输出了。
那么我们可以使用Event的功能做什么呢?试想一下,如果我们希望我们的人物在跳跃着陆时需要播放一个音效,就可以使用到事件的功能了。
我们先看看Animation Clip的一些设置:
Loop time:动画是否循环播放。
下面出现了3个大致一样的选项:
Root Transform Rotation:表示为播放动画的对象的旋转方面的信息。
Root Transform Position(Y):表示为播放动画的对象的位置 y 轴方面的信息。
Root Transform Position(XZ):表示为播放动画的对象的位置 XZ 平面方面的信息。
他们都有一致的选项,我们逐个的来看:
Bake Into Pose:不勾选表示动画产生的旋转或位移(具体看是哪个选项下面)会应用到播放动画的主体对象上,勾选则表示不会应用到播放动画的主体对象上。
具体的效果如下(我的这个动画是人物进行前空翻的动画):Root Transform Position(XZ)的Bake Into Pose没有勾选,表示人物前空翻时,主体GameObject会向前移动,如果勾选则GameObject在XZ平面则不会移动。同理,Root Transform Position(Y)勾选表示GameObject在Y轴不移动,如果不勾选,则跟随摄像机会产生抖动,因为目标GameObject在Y轴也出现了跳跃时移动。
Based Upon:基准点,有如下类型:
我们先简单的看看动画状态,点击项目中任意一个Animator Controller,打开Animator界面可以看到如下的信息:
我们先不管左面的Layers和Parameters面板,右方的显示面板是一个标准的有限状态机配置面板,其中有3个状态“Entry”、“Any State”和“Exit”是由系统生成的,其它状态则由我们自己创建和管理,需要注意的是在Unity4.x中,系统创建的状态只有“Any State”一个。
每个状态之间由带箭头的线段连接,表示这个状态可以过渡到指向的状态。
状态关系和切换我们以后再看,现在我们点击任意一个状态,在Inspector面板可以看到该状态的详细信息,如下:
下面的Transitions面板则会列出当前状态可以过渡到其它状态的列表:
Animator Controller在Unity中是作为一种单独的配置文件存在的文件类型,其后缀为controller,Animator Controller包含了以下几种功能:
下面我们通过一个图来直观的看看动画的组成结构:
Animator组件用来控制人物动画的播放,其中需要的两个最核心的内容就是控制动画播放逻辑的Animator Controller以及动画骨骼Avatar对象。
我们需要播放动画的角色都需要添加Animator组件,该组件即为我们控制动画的接口,下面我们来看看Animator组件:
下方还会显示我们的动画的一些主要信息。
我们在右键创建资源时还会发现这样的一个资源,该资源的意思是基于某个Animator Controller进行每个状态的动画的修改,其他设置不变,可以方便的复用我们已经创建好的Animator Controller。
我们在Project视图中右击菜单中可以创建Animator Controller,下面我们看看新创建的Animator Controller:
首先,我们发现的是3个默认的状态,这些状态是Unity自动帮我们创建的同时也无法删除:
1.我们可以通过右键菜单进行创建:
2.或者我们可以通过把一个Animation Clip拖拽到状态机窗口中进行创建;
我们发现我们创建的第一个状态被设置为默认的第一个状态,第一个状态会被标记为黄色,如下:
我们可以通过Inspector窗口进行该状态的设置,上篇笔记有详细的解说。
我们可以使用这两种方法来创建多个状态,同时配置好每个状态,如下:
我们的状态机已经有了状态,但是还没有指定每个状态之间的关系,下面我们来看看该如何指定状态之间的关系。
在Mecanim中,动画之间的播放不再是通过调用诸如“Play”之类的方法进行切换了,而是通过判断参数的变换来进行状态即动画的切换。
我们打开Parameters面板,这里被用来设置状态机使用到的各种参数,如下:
点击加号可以创建一个参数,在Unity中允许我们创建4种类型的参数:
Float:float类型的参数,多用于控制状态机内部的浮点型参数;
Int:int类型的参数,多用于控制状态机内部的整型参数;
Bool:bool类型参数,多用于状态切换;
Trigger:本质上也是一个bool类型的参数,但是其值默认为false,且设置为true后系统会自动将其还原为false;
下面我们创建两个简单的Bool类型的变量,如下:
我们希望,当Walk为true时人物走动,为false时人物站立,Run也是一致,只不过会过渡到跑动。
下面我们通过右击状态的菜单可以拉出一条状态转换的线条,表示当前状态转换到目标状态。
我们点击线条可以在Inspector窗口设定转换的条件,如下:
我们在条件框设置Walk为true,表示当Walk被设置为true时就从站立跳转到走路动画,同样的,走路回到站立也需要一条转换的线条,不过Walk要设置为false:
Run的设定也一致。
我们发现线条中有一个勾选项Has Exit Time,那么它是什么意思呢?
如果我们勾选了该项,在动画转换时会等待当前动画播放完毕才会转换到下一个动画,如果当前动画是循环动画会等待本次播放完毕时转换,所以对于需要立即转换动画的情况时记得要取消勾选。
还有一种情况时,当我当前的动画播放完毕后就自动转换到箭头所指的下一个状态(没有其他跳转条件),此时必须勾选该选项,否则动画播放完毕后就会卡在最后一帧,如果是循环动画就会一直循环播放。
我们在绑定了Animator组件的GameObject之上添加下面的新脚本组件,就可以实现通过按键来切换人物播放的动画了:
1 using UnityEngine;
2 using System.Collections;
3
4 public class TestAnimChange : MonoBehaviour
5 {
6 private Animator _animator;
7
8 void Start()
9 {
10 _animator = this.GetComponent();
11 }
12
13 void Update()
14 {
15 if(Input.GetKeyDown(KeyCode.W))
16 {
17 _animator.SetBool("Walk", true);
18 }
19 if(Input.GetKeyUp(KeyCode.W))
20 {
21 _animator.SetBool("Walk", false);
22 }
23 if(Input.GetKeyDown(KeyCode.R))
24 {
25 _animator.SetBool("Run", true);
26 }
27 if(Input.GetKeyUp(KeyCode.R))
28 {
29 _animator.SetBool("Run", false);
30 }
31 }
32 }
这里我重新弄了一个简单的场景和新的Animator Controller来作为示例。
下面先看看Animator Controller的配置:
人物在站立状态只能进入走路,走路只能进入奔跑或返回站立,奔跑只能进入跳跃或返回走路,跳跃则只能返回奔跑。
参数方面为两个参数:
连线直接的转换条件为:moveSpeed大于0.1进入走路,走路moveSpeed大于0.9进入奔跑小于0.1返回站立,奔跑moveSpeed小于0.9返回走路,Jump被触发则进入跳跃;
为人物添加Animator组件并绑定上面的Animator Controller文件,同时绑定下面的脚本:
1 using UnityEngine;
2 using System.Collections;
3
4 public class TestAnim : MonoBehaviour
5 {
6 //将名称转换为哈希值可以提高索引的速度
7 private int moveSpeed = Animator.StringToHash("moveSpeed");
8 private int jump = Animator.StringToHash("Jump");
9 private int runState = Animator.StringToHash("Base Layer.Run");
10
11 private Animator _animator;
12
13 void Start()
14 {
15 _animator = this.GetComponent();
16 }
17
18 void Update()
19 {
20 float speed = Input.GetAxis("Vertical");
21 _animator.SetFloat(moveSpeed, speed);
22
23 //获取动画的当前状态
24 AnimatorStateInfo info = _animator.GetCurrentAnimatorStateInfo(0);
25 //跳跃除了要按下空格键外, 还需要处于奔跑状态才行, 否则按下空格键时就会标记 Jump, 一进入奔跑就马上跳跃
26 if(Input.GetKeyDown(KeyCode.Space) && info.fullPathHash == runState)
27 {
28 _animator.SetTrigger(jump);
29 }
30 }
31 }
在Unity5.0中,我们可以给Animator Controller中的每个State添加脚本了,类似于专门用于GameObject的MonoBehavior,State可以添加State Machine Behavior,我们打开任意的Animator Controller,就可以在Inspector窗口发现添加脚本的按钮:
除了State外,我们还可以在层上添加,层可以看做包含了整个State的一个大State:
我们可以添加一个脚本看看,下面是我添加了注释的State Machine Behavior脚本的标准形式:
1 using UnityEngine;
2 using System.Collections;
3
4 public class StateScript : StateMachineBehaviour
5 {
6 // OnStateEnter is called before OnStateEnter is called on any state inside this state machine
7 override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
8 {
9 //进入当前状态时会被调用
10 }
11
12 // OnStateUpdate is called before OnStateUpdate is called on any state inside this state machine
13 override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
14 {
15 //进入当前状态的每帧会被调用
16 }
17
18 // OnStateExit is called before OnStateExit is called on any state inside this state machine
19 override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
20 {
21 //退出当前状态时会被调用
22 }
23
24 // OnStateMove is called before OnStateMove is called on any state inside this state machine
25 override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
26 {
27 }
28
29 // OnStateIK is called before OnStateIK is called on any state inside this state machine
30 override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
31 {
32 }
33
34 // OnStateMachineEnter is called when entering a statemachine via its Entry Node
35 override public void OnStateMachineEnter(Animator animator, int stateMachinePathHash)
36 {
37 }
38
39 // OnStateMachineExit is called when exiting a statemachine via its Exit Node
40 override public void OnStateMachineExit(Animator animator, int stateMachinePathHash)
41 {
42 }
43 }
通过使用State Machine Behaviour我们可以更加方便的在特定的时间点触发一些我们需要的事件,但是需要注意的是,我们一般给State Machine Behaviour赋值一些场景的对象不是直接在Inspector面板里拖拽而是通过Animator的GetBehavior方法获取到指定的State Machine Behaviour的实例后通过脚本进行赋值的。
IK(Inverse Kinematics)即反向动力学,即可以使用场景中的各种物体来控制和影响角色身体部位的运动,一般来说骨骼动画都是传统的从父节点到子节点的带动方式(即正向动力学),而IK则倒过来,由骨骼子节点带动骨骼父节点,具体情况比如人物走路踩到了石头就需要由脚的子节点来带动全身骨骼做出踩到石头的响应。
IK可以使人物和场景更加贴合,从而达到更加真实的游戏效果,如果大家玩过《波斯王子》或《刺客信条》系列,应该对主角的攀爬和飞檐走壁的能力印象深刻,这些都是应用了IK,使动画贴合到具体的场景中进行的表现。
Unity3D本身已经带有了IK的功能(http://docs.unity3d.com/Manual/InverseKinematics.html),我们接下来就对IK进行一下简单的学习和使用。
该插件是对Unity本身的IK的优化和增强,可以模拟出更加真实的效果,有兴趣可以看一看。
https://www.assetstore.unity3d.com/cn/#!/content/14290
我们直接上手一个小例子来看看Unity3D中的IK应该如何使用,我们会创建一个场景,使人物的头部始终面向一个点,同时创建四个点控制人物的手和腿的移动。
我们在场景中添加一个人物和5个小球,如下:
根据Unity官方的文档给出的资料来看,首先必须在需要使用IK动画的Animator的层上开启“IK Pass”,如下图所示:
只有开启了这个选项,系统才会调用IK相应的方法。
下面我们为这个人物添加一个脚本,如下:
1 using UnityEngine;
2 using System.Collections;
3
4 public class TestIK : MonoBehaviour
5 {
6 public Transform lookAtTarget;
7
8 public Transform leftHandTarget;
9 public Transform rightHandTarget;
10 public Transform leftFootTarget;
11 public Transform rightFootTarget;
12
13 private Animator _animator;
14
15 void Start()
16 {
17 _animator = this.GetComponent();
18 }
19
20 void OnAnimatorIK(int layerIndex)
21 {
22 if(_animator != null)
23 {
24 //仅仅是头部跟着变动
25 _animator.SetLookAtWeight(1);
26 //身体也会跟着转, 弧度变动更大
27 //_animator.SetLookAtWeight(1, 1, 1, 1);
28 if(lookAtTarget != null)
29 {
30 _animator.SetLookAtPosition(lookAtTarget.position);
31 }
32
33 _animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1);
34 _animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1);
35 if(leftHandTarget != null)
36 {
37 _animator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandTarget.position);
38 _animator.SetIKRotation(AvatarIKGoal.LeftHand, leftHandTarget.rotation);
39 }
40
41 _animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1);
42 _animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1);
43 if(leftHandTarget != null)
44 {
45 _animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandTarget.position);
46 _animator.SetIKRotation(AvatarIKGoal.RightHand, rightHandTarget.rotation);
47 }
48
49 _animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1);
50 _animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 1);
51 if(leftHandTarget != null)
52 {
53 _animator.SetIKPosition(AvatarIKGoal.LeftFoot, leftFootTarget.position);
54 _animator.SetIKRotation(AvatarIKGoal.LeftFoot, leftFootTarget.rotation);
55 }
56
57 _animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1);
58 _animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 1);
59 if(leftHandTarget != null)
60 {
61 _animator.SetIKPosition(AvatarIKGoal.RightFoot, rightFootTarget.position);
62 _animator.SetIKRotation(AvatarIKGoal.RightFoot, rightFootTarget.rotation);
63 }
64 }
65 }
66 }
需要注意的是,控制IK的脚本必须添加到OnAnimatorIK方法中才会生效,下面看下效果图:
动画分层可以用来解决什么样的问题呢?试想一下如果你要开发一款第三人称的射击游戏,那么肯定是希望身体的动画分为上下两部分,上方根据瞄准的位置和是否射击进行动画播放,下方根据移动播放动画。最好的例子就是Unity4.x自带的示例AngryBots了。
下面我们就使用Avatar Mask来实现人物在奔跑中招手的效果。
我们先添加一个人物到场景,同时为其添加一个Animator Controller并设定好跳转条件,如下:
接下来我们添加下面的脚本来控制动画的播放,对了为了不让角色因为播放动画而移动,记得把“Apply Root Motion”取消,方便我们查看动画播放的效果。
添加的脚本如下:
1 using UnityEngine;
2 using System.Collections;
3
4 public class MaskTest : MonoBehaviour
5 {
6 private Animator _animator;
7
8 void Start()
9 {
10 _animator = this.GetComponent();
11 }
12
13 void Update()
14 {
15 if(Input.GetKeyDown(KeyCode.R))
16 {
17 _animator.SetBool("run", true);
18 }
19 if(Input.GetKeyUp(KeyCode.R))
20 {
21 _animator.SetBool("run", false);
22 }
23
24 AnimatorStateInfo state = _animator.GetCurrentAnimatorStateInfo(0);
25 if(state.shortNameHash == Animator.StringToHash("Run") && Input.GetKeyDown(KeyCode.J))
26 {
27 _animator.SetTrigger("jump");
28 }
29 }
30 }
接下来我们希望按下空格时人物播放招手动画,但是跑动跳跃的动画不能停止播放。
我们需要添加一个新的Layer来管理招手动画的播放:
同时我们要配置好招手动画,这里的Idle可以不添加任何动画,仅仅表示空就可以了,同时还添加了一个Trigger“wave”表示进入招手动画的条件。
接下来我们设置一下该层,将其权重设置为1:
好了,我们修改一下代码试试看吧:
1 using UnityEngine;
2 using System.Collections;
3
4 public class MaskTest : MonoBehaviour
5 {
6 private Animator _animator;
7
8 void Start()
9 {
10 _animator = this.GetComponent();
11 }
12
13 void Update()
14 {
15 if(Input.GetKeyDown(KeyCode.R))
16 {
17 _animator.SetBool("run", true);
18 }
19 if(Input.GetKeyUp(KeyCode.R))
20 {
21 _animator.SetBool("run", false);
22 }
23
24 AnimatorStateInfo state = _animator.GetCurrentAnimatorStateInfo(0);
25 if(state.shortNameHash == Animator.StringToHash("Run") && Input.GetKeyDown(KeyCode.J))
26 {
27 _animator.SetTrigger("jump");
28 }
29
30 if(Input.GetKeyDown(KeyCode.Space))
31 {
32 _animator.SetTrigger("wave");
33 }
34 }
35 }
运行一下,发现当播放招手动画时其它动画都会停止,该怎么办呢?
答案是我们需要创建一个Avatar Mask来表示我们只希望播放动画的部分,即手部动画播放,其它部分不播放,在Project窗口右击创建一个Avatar Mask文件:
将右手部分以外的区别关闭即可:
这里说一个Transform这个选项,该选项是控制每个骨骼是否参与动画使用的,如果我们的人物有翅膀和尾巴之类的东西就要使用它了。
最后一步我们将这个Avatar Mask拖入Animator Controller的Wave层中即可:
再次运行下游戏,我们需要的效果是不是已经出来了呢。
下面我们来详解一下Layer面板的设置:
动画层的权重,默认的Base Layer必须为1。如果设置为0则当前层的动画不会播放,1则会播放,0-1之间会采用类似融合的情况来播放动画,比如之前的招手的例子,如果设置为0.5则招手动画播放时手部只会抬到脖子附近。
动画遮罩,上方已经详解就不赘述。
动画混合方式:
开启了该功能后会多出一些选项,如下:
我们可以将该功能看做复制的功能。
Source Layer:指定当前层是哪个层的副本,设定后当前层的状态会和指定层完全一致或完全同步,但是我们可以修改某一个状态的动画。
该功能提供的效果就是两个状态一致的层可以做出一些不同的调整。
Timing:当前层和Souurce层同一个状态使用的动画时间长度不一致时,不勾选复制的层按Source层的时间播放(效果就是复制层动画可能会变快或变慢,Source层动画播放速度不变),勾选则Source层按复制层的时间播放(效果就是Source层动画可能会变快或变慢,复制层动画播放速度不变)。
表示启动IK动画,上一篇文章已经详解就不赘述。
我们在Animator Controller中除了可以创建一个State外还可以创建一个Blend Tree,如下:
那么我们看下新创建的Blend Tree和State有什么区别:
唯一的区别就是Montion指向的类型变成了Blend Tree类型,那么一个Blend Tree其实也就是一个状态,和状态不同的地方就是一个状态只能设定一个动画,而一个Blend Tree则可以设定为多个动画的混合。
混合树是Mecanim动画系统中比较复杂的一个内容,且其分为多个维度,下面我们逐个的来进行学习。
我们以官方的例子使用一维混合树来实现下面的效果:我们人物的跑动分为3个动画,分别是向前跑、向左跑和向右跑,其中向左跑和向右跑人物都会有一定的倾斜,这样更加符合现实的情况,那么我们在状态机中跑动只有一个状态,所以我们的跑动需要设置为混合树来混合这3个动画。
首先我们需要创建一个新的场景,拖入我们的人物模型,然后创建一个Animator Controller并对其进行配置:
注意我们的Run是Blend Tree而不是State,双击Run就可以进入混合树的编辑界面。
右击我们的混合树添加3个Motion,如下:
同时我们设定好3个方向的跑动动画:
我们还需要设定一个名为Direction的Float类型的参数来控制这个混合树:
接下来我们取消Automate Thresholds的选项,并按下图进行选择,系统会为我们配置好阀值:
现在我们点击预览框查看动画播放时就可以通过拖拽小红线来看不同的变化了,我们的可使用的角度范围为-130到130之间。
到现在我们的动画控制器就配置好了。
下面我们使用脚本来控制一下人物,我们给人物添加下面的脚本即可:
1 using UnityEngine;
2 using System.Collections;
3
4 public class TestBlendTree : MonoBehaviour
5 {
6 public float DirectionDampTime = 30.0f;
7
8 private Animator _animator;
9
10 void Start()
11 {
12 _animator = this.GetComponent();
13 }
14
15 void Update()
16 {
17 if(Input.GetKeyDown(KeyCode.W))
18 {
19 _animator.SetBool("run", true);
20 }
21 if(Input.GetKeyUp(KeyCode.W))
22 {
23 _animator.SetBool("run", false);
24 }
25
26 AnimatorStateInfo state = _animator.GetCurrentAnimatorStateInfo(0);
27 //奔跑状态下才允许转弯
28 if(state.shortNameHash == Animator.StringToHash("Run"))
29 {
30 //指定人物转弯通过控制混合数的参数即可
31 float h = Input.GetAxis("Horizontal") * 130.0f;
32 //DirectionDampTime 指示了每秒可以到达的最大值
33 //deltaTime 表示当前帧的时间
34 _animator.SetFloat("Direction", h, DirectionDampTime, Time.deltaTime);
35 }
36 else
37 {
38 //重置一下参数
39 _animator.SetFloat("Direction", 0);
40 }
41 }
42 }
为了让摄像机跟随人物,我们直接添加官方给出的这个脚本到摄像机上即可:
1 using UnityEngine;
2 using System.Collections;
3
4 public class ThirdPersonCamera : MonoBehaviour
5 {
6 public float distanceAway; // distance from the back of the craft
7 public float distanceUp; // distance above the craft
8 public float smooth; // how smooth the camera movement is
9
10 private GameObject hovercraft; // to store the hovercraft
11 private Vector3 targetPosition; // the position the camera is trying to be in
12
13 Transform follow;
14
15 void Start(){
16 follow = GameObject.FindWithTag ("Player").transform;
17 }
18
19 void LateUpdate ()
20 {
21 // setting the target position to be the correct offset from the hovercraft
22 targetPosition = follow.position + Vector3.up * distanceUp - follow.forward * distanceAway;
23
24 // making a smooth transition between it's current position and the position it wants to be in
25 transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * smooth);
26
27 // make sure the camera is looking the right way!
28 transform.LookAt(follow);
29 }
30 }
记得将人物的Tag设置为Player,同时脚本也要设置一下:
每个混合树的动画有一些要注意的地方:
同1维混合树,不过二维混合树已经作为一个平面来处理,同时需要两个参数来进行控制。对于更复杂的动画融合可以使用该模式,这里就不深入学习了。
我们可以将两个1维混合树合并为一个2维混合树来控制。
多维混合树在Unity5时添加,其配置更加复杂,一般使用在脸部表情的动画融合上。
资源加载是必备的知识点,这里就说说Mecanim动画的资源如何打包及加载。
注意,Unity4.x和Unity5.x的AssetBundle打包策略不一样,本笔记是基于Unity4.x的AssetBundle进行打包的。
我们一般使用FBX类型的模型及动画文件,而动画文件的储存一般有两种情况,一是所有的动画和模型都一起存放到一个文件中,还有一种情况是模型单独一个文件而动画单独一个文件。这里我们就两种情况都看一下。
使用的资源是Unity3D自带的以及从一本教材中取出的两种类型的动画资源,同时需要对其动画创建对应的Animator Controller。
一个FBX文件保存了模型、骨骼和动画,如下图:
下面是配置的Animator Controller:
需要注意的是,官方并没有提供为Animator设置Animator Controller的接口,所以我们必须将配置好的GameObject制作为一个预制件进行加载。
1 using UnityEngine;
2 using System.Collections;
3
4 public class AllInOneResourcesLoad : MonoBehaviour
5 {
6 private Animator _animator;
7
8 void Start()
9 {
10 GameObject go = Resources.Load("AllInOne/ConstructorPrefab");
11
12 GameObject man = Instantiate(go) as GameObject;
13 _animator = man.GetComponent();
14 }
15
16 void OnGUI()
17 {
18 if(GUI.Button(new Rect(0, 0, 100, 30), "idle"))
19 {
20 _animator.SetBool("walk", false);
21 _animator.SetBool("run", false);
22 }
23 if(GUI.Button(new Rect(100, 0, 100, 30), "walk"))
24 {
25 _animator.SetBool("walk", true);
26 _animator.SetBool("run", false);
27 }
28 if(GUI.Button(new Rect(200, 0, 100, 30), "run"))
29 {
30 _animator.SetBool("walk", false);
31 _animator.SetBool("run", true);
32 }
33 if(GUI.Button(new Rect(300, 0, 100, 30), "jump"))
34 {
35 _animator.SetTrigger("jump");
36 }
37 }
38 }
1 using UnityEngine;
2 using UnityEditor;
3
4 public class CreateAllInOneAB
5 {
6 [MenuItem("Tool/CreateAllInOneAB")]
7 private static void Create()
8 {
9 BuildPipeline.BuildAssetBundle(null, new[]
10 {
11 AssetDatabase.LoadAssetAtPath("Assets/Resources/AllInOne/ConstructorPrefab.prefab", typeof(GameObject))
12 },
13 Application.streamingAssetsPath + "/AllInOne.assetbundle",
14 BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets | BuildAssetBundleOptions.UncompressedAssetBundle,
15 BuildTarget.StandaloneWindows64);
16 }
17 }
1 using UnityEngine;
2 using System.Collections;
3
4 public class AllInOneAssetBundleLoad : MonoBehaviour
5 {
6 private Animator _animator;
7
8 void Start()
9 {
10 AssetBundle assetBundle = AssetBundle.CreateFromFile(Application.streamingAssetsPath + "/AllInOne.assetbundle");
11
12 GameObject go = assetBundle.Load("ConstructorPrefab", typeof(GameObject)) as GameObject;
13
14 GameObject man = Instantiate(go) as GameObject;
15 _animator = man.GetComponent();
16 }
17
18 void OnGUI()
19 {
20 if(GUI.Button(new Rect(0, 0, 100, 30), "idle"))
21 {
22 _animator.SetBool("walk", false);
23 _animator.SetBool("run", false);
24 }
25 if (GUI.Button(new Rect(100, 0, 100, 30), "walk"))
26 {
27 _animator.SetBool("walk", true);
28 _animator.SetBool("run", false);
29 }
30 if (GUI.Button(new Rect(200, 0, 100, 30), "run"))
31 {
32 _animator.SetBool("walk", false);
33 _animator.SetBool("run", true);
34 }
35 if (GUI.Button(new Rect(300, 0, 100, 30), "jump"))
36 {
37 _animator.SetTrigger("jump");
38 }
39 }
40 }
还有一种情况是模型和动画是分为多个FBX文件存放的,比如下面是模型文件:
虽然有一个Take 001的动画,但是实际上我们并不使用该动画,而是使用下面仅保存了动画的FBX文件:
下面是配置的Animator Controller:
除了没有提供设置Animator Controller的接口,也无法在运行时对动画剪辑进行增加删除的操作,所以我们一般打包时就收集所有的依赖项一起打包,归根结底还是只需要一个制作好的预制件即可。
从这个角度看,其实是否将动画进行拆分最终的使用方式都是一样的。
1 using UnityEngine;
2 using System.Collections;
3
4 public class ResourcesLoad : MonoBehaviour
5 {
6 private Animator _animator;
7
8 void Start()
9 {
10 GameObject go = Resources.Load("ZombieNurse/ZombieNursePrefab");
11
12 GameObject man = Instantiate(go) as GameObject;
13 _animator = man.GetComponent();
14 }
15
16 void OnGUI()
17 {
18 if(GUI.Button(new Rect(0, 0, 100, 30), "idle"))
19 {
20 _animator.SetBool("run", false);
21 }
22 if(GUI.Button(new Rect(100, 0, 100, 30), "run"))
23 {
24 _animator.SetBool("run", true);
25 }
26 if(GUI.Button(new Rect(200, 0, 100, 30), "attack"))
27 {
28 _animator.SetTrigger("attack");
29 }
30 if(GUI.Button(new Rect(300, 0, 100, 30), "dead"))
31 {
32 _animator.SetTrigger("dead");
33 }
34 }
35 }
1 using UnityEditor;
2 using UnityEngine;
3
4 public class CreateAB
5 {
6 [MenuItem("Tool/CreateAB")]
7 private static void Create()
8 {
9 BuildPipeline.BuildAssetBundle(null, new[]
10 {
11 AssetDatabase.LoadAssetAtPath("Assets/Resources/ZombieNurse/ZombieNursePrefab.prefab", typeof(GameObject))
12 },
13 Application.streamingAssetsPath + "/AB.assetbundle",
14 BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets | BuildAssetBundleOptions.UncompressedAssetBundle,
15 BuildTarget.StandaloneWindows64);
16 }
17 }
1 using UnityEngine;
2 using System.Collections;
3
4 public class AssetBundleLoad : MonoBehaviour
5 {
6 private Animator _animator;
7
8 void Start()
9 {
10 AssetBundle assetBundle = AssetBundle.CreateFromFile(Application.streamingAssetsPath + "/AB.assetbundle");
11
12 GameObject go = assetBundle.Load("ZombieNursePrefab", typeof(GameObject)) as GameObject;
13
14 GameObject man = Instantiate(go) as GameObject;
15 _animator = man.GetComponent();
16 }
17
18 void OnGUI()
19 {
20 if(GUI.Button(new Rect(0, 0, 100, 30), "idle"))
21 {
22 _animator.SetBool("run", false);
23 }
24 if(GUI.Button(new Rect(100, 0, 100, 30), "run"))
25 {
26 _animator.SetBool("run", true);
27 }
28 if(GUI.Button(new Rect(200, 0, 100, 30), "attack"))
29 {
30 _animator.SetTrigger("attack");
31 }
32 if(GUI.Button(new Rect(300, 0, 100, 30), "dead"))
33 {
34 _animator.SetTrigger("dead");
35 }
36 }
37 }
还记得读书的时候熬夜打《波斯王子》的时光,我们的王子通过跳跃穿过墙壁的小洞、在高层建筑上进行攀爬和跳跃,还有在操作失误掉下高楼和触发必死机关后使用时之沙的时光倒流功能回归死亡之前的开挂技能。
而现在这些功能使用Unity3D的Mecanim都可以非常简单的实现了!
这篇笔记主要记录下Mecanim的3个高级功能:
可以说Unity3D现在已经具备了开发一款媲美国际大厂的动作游戏的能力了。
这里我们直接使用官网给出的示例,打开场景Scale Capsule:
运行游戏,我们发现此时人物是不能通过跳跃穿过墙上的小孔的:
原因很简单,人物的胶囊碰撞体比小孔要大。当我们勾选了右上角的Apply Scale之后,再跳跃试试看:
此时我们发现当人物跳跃时,胶囊碰撞体就会进行缩小,而我们的人物就可以钻过这个小洞了,在跳跃之后胶囊体会进行恢复。
我们找到动画文件Dive,就可以看到其设置了两个Curve:
而其中的ScaleOffsetCapsule就是用来控制胶囊体缩放的曲线,我们可以发现其呈现了1->0->1的一个区间。
然后我们看看参数的设置:
参数必须是Float类型同时名称必须和动画文件中的Curve名称一致,这样当我们播放跳跃动画时该参数就会根据预设好的曲线来取值了,注意该参数后方的输入框已经禁用了,不能输入。
最后我们看下脚本ScaleCapsule的实现:
通过每帧获取参数的值来实现跳跃的碰撞体变小的功能。
核心是使用到了Animator的MatchTarget方法,帮助如下:
http://docs.unity3d.com/ScriptReference/Animator.MatchTarget.html
这里我们还是直接使用官网给出的示例,打开场景Target Match:
我们发现在箱子上方已经做好了一个目标点,我们的人物的右手会匹配到这个点进行动画播放。
需要注意的是,使用MatchTarget的动画必须位于Base Layer层。Match Target会将我们人物的一个部位的位置和目标点的位置进行插值来移动我们人物,结果就是可以通过一个指定高度的攀爬动画来匹配任意高度的攀爬。
我们看下调用的脚本:
1 // Update is called once per frame
2 void Update ()
3 {
4
5 if (animator)
6 {
7 AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0);
8
9 if (Input.GetButton("Fire1")) animator.SetBool("Jump", true);
10
11 if (state.IsName("Base Layer.JumpUp") || state.IsName("Base Layer.FullJump"))
12 {
13 animator.SetBool("Jump", false);
14
15 animator.MatchTarget(RightHand.position, RightHand.rotation, AvatarTarget.RightHand, new MatchTargetWeightMask(new Vector3(1, 1, 1), 0), animator.GetFloat("MatchStart"), animator.GetFloat("MatchEnd"));
16 hasJumped = true;
17 }
18
19 if (hasJumped && state.normalizedTime > 1.2)
20 {
21 hasJumped = false;
22 Application.LoadLevel(0);
23 }
24 }
25 }
Unity3D的Animator类为我们提供了4个方法用于录制和播放动画:
同时我们也可以通过访问Animator提供的一些属性来了解动画录制的一些信息:
使用比较简单就不做示例展示了,需要注意的时,回放录制的动画时人物会回到开始录制的坐标点,而不是在当前坐标点开始播放。