在Unity3D的当前版本中,Animator Controller已经全面取代了原来(大概是4.x版本左右)的Animation系统。
从本质上来说,Animator Controller是一个内置在Unity3D中的,专门用于动画控制的“状态机”,所以学习Animator Controller和学习PlayMaker有一定的共通之处。
从实际的使用场景来说,Animator Controller虽然可以建立复杂的运动逻辑,但其本身并不具备任何交互能力,也就是说我们不能仅使用Animator Controller就达到“控制”目标对象动画的目的,还必须撰写Script(脚本)来操纵Animator Controller中的Parameter(参数)从而真正控制对象的动画。或者,使用PlayMaker来代替C#脚本。
因此,本章内容主要分为三个部分:
- 从fbx文件中导入或直接在Unity3D中制作动画片段(Animation Clips)
- 在Animator Controller设计动画逻辑
- 在PlayMaker中制作交互控制Animator Controller
生成动画片段(Clips)
导入FBX文件中的动画
可以参看文章:为Unity3D创建素材(2):模型、绑定、动画 中的相关内容
FBX中的动画会被识别为动画片段文件(.anim
),但存在于FBX中的动画片段不可被编辑,所以如果需要修改或者重复调用这些文件,建议还是Ctrl+D复制一份之后再使用。
另外,FBX中的动画是根据角色的Avatar设置而生成的,因此虽然理论上动画可以重复使用,但其前提是两个角色都使用了相同的Avatar设置。最简便的方法是将所有角色都设置成Humanoid类型的Avatar,这样就可以避免因为Avatar的不同而产生动画映射出错的问题。
比如"Unity-chan!" Model和HQ Fighting Animation FREE这两个素材,虽然都使用了同一个角色模型,骨骼设置也都是一样的,但由于Avatar设置不同,所以个素材中的动画是不能直接共用的,必须将一个修改成另一个的Avatar(比如将HQ Fighting Animation FREE中的所有带Animation的FBX都设置成Humanoid形式)之后才能通用。
创建动画
简单的动画可以在Unity中直接生成。
从菜单Window
-> Animation
打开动画面板。如果选择的游戏物体上没有附加任何动画片段,动画面板上应该是这样的:
点击Create
按钮并保存文件,则为该游戏物体创建了一个动画片段(.anim),点击左上方的“录制”按钮可以进入录制状态,也可以理解成进入“自动创建关键帧”的状态。在这个状态中我们对任何参数进行修改,都会自动为这些参数创建动画关键帧,并记录在动画片段中。
除了Transform组件上的属性,我们还可以手动点击Add Property
为该游戏物体上其他组件的可动画参数创建关键帧动画。
关闭“录制”按钮可以将动画保存起来。一个游戏物体上可以附加有多个动画片段,比如下图中的Cube就是当前正在编辑的动画片段的名称,而点击这个名称则可以切换到其他动画片段或者创建新的动画片段。
添加了动画片段的游戏物体中会自动添加上Animator组件,并自动附上Controller。要注意的是,下图中的Controller参数中的Cube和上面动画片段的Cube并不是一回事,只是Unity自动创建了相同名称的Controller和动画片段而已。
双击Cube Controller打开编辑器:
这是最简单的一个动画逻辑:当被载入时,播放Cube动画。
单击这个橘色的Cube
,在Inspector中会显示这个“状态(State)”的详细设置:
这个状态所调用动画片段(Motion
)是“Cube”,播放速度(Speed
)是正常1倍速,由于这个动画片段中仅有一个关键帧,所以其实它是不包含任何“动画”的,只不过是一个“姿势”而已。对于Unity3D来说,“姿势(Pose)”和“动画(Animation)”本质上是一样的。
双击这个橘色的Cube会直接选择到Cube动画片段文件:
下面我们给这个Cube创建一个自动旋转的动画片段:
这个动画很简单:3秒钟以内,Y轴旋转360度。有几个细节值得留意一下:首先,Transform上的动画关键帧,一加就是加3个(x、y、z),这是因为Unity对于位置数据是作为Vector3类型来处理的;然后,当处于“录制”状态时,Animator组件是处于“不激活”状态的,这样我们才可以在场景视图中观察到所制作的动画。
打开Animator面板,可以看到多出来一个灰色的状态“Rotate”,这就是刚刚制作的旋转动画。如果没有自动出现,可以将Rotate.anim文件拖进来。
运行游戏时,这个Cube是不会旋转的,因为当前被播放的是Cube动画。
我们可以右键单击Rotate
,选择Set as Layer Default State
,这样会让Rotate
变成初始“状态”,游戏运行时就会自动播放了。
设计动画逻辑
Animator Controller的基本动画逻辑是:一个游戏物体可以使用任意多个动画片段,每个动画片段都是一个独立的“状态”,而运动的变化则体现为从一个状态到另一个状态的过渡。
比较先进的是,Animator Controller中状态到状态的过渡,可以是“渐变”的,因此运动变化显得非常自然。
用小方块做一个简单的例子
我们先从这个简单的小方块动画开始。
这里我们有两个状态,静止和旋转。我们可以将初始状态依然设置给Cube
,然后右键单击Cube
,选择Make Transition
,然后将箭头指给Rotate
状态。
这个意思是“当Cube状态的动画播放完毕,则过渡到播放Rotate状态的动画”。点击这个连线,可以看到实际的Transition参数:
从这里可以看到两个动画片段是如何“融合过渡”的(其实跟Maya的Trax Editor,或者Premiere的轨道编辑器挺像的)。
我们可以看到这个过渡并不是一开始就发生了的,而是在Cube
状态停留了大概1秒钟的时候才在大概1/4秒的时间内过渡到Rotate
状态。
我们可以在这个轨道图上对整个动画过渡进行修改。
不论怎么修改,整个过渡都依然是“自然过渡”,也就是前一个状态播放完毕过渡到下一个状态。除非我们为它提供一个条件(Condition)。
目前的Conditions一栏中是空的,点击小+
号添加一个条件,但提示说:“没有任何参数存在”。条件是需要用参数来指挥的。
回到Animator Controller面板,左上方Parameters一栏中点击小+
号添加一个新的参数,这里我们选择Bool类型,命名为“Rotate”。
然后指定这个Rotate
参数为刚刚创建的Condition,并设置当Rotate
取值为true
时,条件成立。
同理,我们再创建一个从Rotate
状态到Cube
状态的Transition,并添加一个条件,并设置成当Rotate
取值为false
时,条件成立。
运行场景,小方块一直在Cube
状态循环,手动点击Rotate
参数使其为true
,小方块开始旋转。手动修改Rotate
参数为false
,小方块又停了下来。
目前小方块的旋转和停止都有点“滞后”,这是因为两个Transition中都勾选Has Exit Time
选项,这个选项保证必须播放到一定时间才能进行过渡,在有些情况下是需要的,但这个简单例子中却并不需要。
取消这两个勾选,我们就作出了一个非常简单的旋转动画控制逻辑。
将这个场景保存起来。
用Unity-Chan做一个复杂一些的例子
新建场景,导入"Unity-chan!" Model和HQ Fighting Animation FREE这两个素材。
找到UnityChan
-> Models
中的unitychan.fbx
文件,拖到场景中。建议拖动到Hierarchy面板中松开鼠标,这样会将模型生成在场景原点位置,如果直接拖到场景面板中,则很有可能不在原点位置。
在Assets文件夹中单击右键创建一个新的Animator Controller资源,命名为UnityChan_AC
,并将这个Animator Controller配置文件拖到场景中unitychan
物体的Animator组件的Controller参数上去,这就为该角色的Animator指定了一个控制器。
双击打开UnityChan_AC
,目前是没有任何状态的,这是因为这个.fbx
文件只是一个模型文件,不包含任何动画,当然也就没有任何“状态”。我们可以在目录:UnityChan
-> Animations
中找到包含动画数据的.fbx
文件,但这些文件的模型贴图都不正确。
选择某个动画文件,并不能看到正确的动画预览,即便是点击小箭头展开之后选择其中包含的动画数据文件,也只能看到模型不完整的预览动画。这是因为这些动画文件中根本就只有骨骼和面部多边形,并没有身体的模型数据。
我们可以将场景中的unitychan
模型拖动到预览窗口,这样就可以预览到该动画在我们希望的目标模型上的表现了。
点击预览窗口的播放按钮可以播放动画。
由于Animations文件夹里面动画很多,我直接利用搜索功能来寻找所需要的动画文件。
- 这里是搜索栏,搜索的范围默认是整个Assets文件夹,但可以修改为搜索当前文件夹或者搜索Asset Store;
- 这里的滑杆拖到最左边是以文件名方式显示,适合同时查看较多的文件对象,拖到最右边是最大的图标显示,适合查看文件对象内容;
- 这里可以看到当前选择的文件所处的文件夹路径,可以以此判断是否是我们想要的资源,因为有可能在不同的文件夹下有同样名称的资源,比较容易发生混淆;
- 这种图标就代表了动画数据文件,选择这样的文件可以在预览窗口中预览到动画效果。
WAIT00
是我现在所需要的静止动画,其他几个wait动画都过于动感了。
将WAIT00
拖动到Animator Controller中,设置为初始状态。运行场景,可以看到角色已经不是T-Pose站立了。如果当前的摄影机角度不适合查看角色模型,可以自行修改一下。
如果我们使用“idle”作为关键词来搜索,可以搜索到另一个素材包中所包含的Idle动画文件,但这个文件不能直接应用在现在这个角色身上。
如果强行使用,会得到非常奇怪的效果:
找到这个Idle动画数据所在的文件:FUCM05_0000_Idle.fbx
,修改其导入参数的Animation Type为Humanoid
(UnityChan的模型就是用的Humanoid类型),点击Apply
应用该修改。
再次运行场景,这回动画就显示正常了。
从“站”到“走”到“跑”
下面我想实现这样的一个动画逻辑:角色根据其移动速度自动选择是站立原地不动,还是慢慢行走,或者快速奔跑。这个动画逻辑非常适合使用Animator Controller的Blend Tree来制作。
Blend Tree可以被看作是一个特殊的“状态”。
在Animator Controller面板中点击右键,新建一个Blend Tree,修改状态名称为Localmotion
,并设置其为初始状态:
双击打开Localmotion
,可以看到:
- 默认Blend Type为
1D
,也就是说当前使用的一维模式,仅使用一个参数来控制动画混合; - 自动为我们创建了一个
Blend
参数,数据类型是Float,同时指定Blend
参数为该Blend Tree的控制参数,也就是说,当Blend
数值从0~1变化时,角色依次从一个Motion变化到另外的Motion; - 当前Motion栏中是空的,需要点击小
+
号来添加融合的动画片段数据。
添加3个Motion,出现一个图示表示这3个Motion是如何被混合的。
使用搜索功能搜出WAIT00
、WALK00_F
、RUN00_F
三个clips,分别依次拖到这3个Motion框中。
Animator Controller中的图示变成下面这个样子:
拖动Blend
滑杆,可以看到后面3个状态依次高亮(高亮表明在融合中的权重较高),且Inspector面板中的图示也相应发生变化。
运行场景,这时拖动滑杆就不是很有作用了,因为这个滑杆并不会影响到真正Blend参数的取值。但我们可以手动修改Blend值来检查动画混合是否正确:
- 当
Blend
= 0时,角色播放静止动画; - 当
Blend
= 0.5时,角色播放步行动画; - 当
Blend
= 1时,角色播放跑步动画; - 当
Blend
处于0 ~ 0.5,或者0.5 ~ 1之间时,角色播放的动画是融合的。
跳跃动画
跳跃动画的一般设计逻辑是这样的:当某个按键被触发时,角色动画立刻切换到播放跳跃动画,同时利用动力学或者直接编辑位置属性的方式让角色模型沿Y轴发生上升下降的运动模拟跳跃,然后检测角色是否落地(碰撞体与地面发生接触),如果落地,则立刻切换回行走动画或站立动画。
在Animator Controller中,我们没办法去移动角色或者检测角色是否落地,只能做动画的切换。
回到Base Layer,搜索出JUMP00
动画片段,拖到Animator Controller中,修改状态名称为Jump
。
新建一个Bool类型的变量IsJump
。
从Localmotion
创建一个Transition指向Jump
,再从Jump
创建一个Transition指向Localmotion
。
选择从Localmotion
指向Jump
的Transition,取消Has Exit Time
的勾选,并新建一个Condition,设置条件为:IsJump
= true。
同理选择从Jump
指向Localmotion
的Transition,为其设置条件为:IsJump
= false。
这时如果我们运行场景,当手动设置IsJump
为true
时,角色会马上开始跳跃,然后如果手动设置IsJump
为false
,角色马上转回运动状态(Blend
参数设置为0.5或者1)。
但如果一直处于IsJump
= true,Jump
动画播放完以后不会自动转换会Localmotion
。如果想要自动转换,可以再创建一个从Jump
指向Localmotion
的Transition,不取消
Has Exit Time
的勾选,也不设置任何条件。
运行场景,发现并没有如我们所想的转换动画到Localmotion
,而是不断重复Jump
动画,我一开始认为这是因为JUMP00
动画片段被默认设置为Loop循环播放了,所以我找到JUMP00
动画片段所在的JUMP00.fbx
文件,发现其Loop Time
并没有被勾选。其实关键原因还在于IsJump
= true这个设置。Jump
跳转回Localmotion
之后,因为IsJump
依然为true,所以立刻又跳转回Jump
了。这不是我们在Animator Controller中能够解决的问题,我们留到后面再解决吧。
受打击动画和死亡动画
这两种动画的逻辑都是在满足特定条件下即激活,但与跳跃动画的实现有一些不同之处。主要表现在受打击和死亡都不需要使用Bool类型的参数来控制,只需要一个“开关”就好了,所以通常使用Trigger类型的参数来进行触发。
新建两个Trigger类型的参数:Hurt
、Die
。然后搜索DAMAGE00
动画片段作为受打击动画。
这次我们从Any State
新建一个Transition到DAMAGE00
,设置无退出时间,设置其条件为Hurt(因为是Trigger,所以并无取值,是最方便的一种条件了)。
然后从DAMAGE00
创建一个Transition到Localmotion
,使其受打击完毕以后继续返回正常休息或行走姿势。
大家可以测试一下先让角色跳起再让角色受伤,动画混合得还挺不错的。从
Any State
开始设置Transition挺方便,但也蛮容易出现奇奇怪怪的混合结果,大家要小心一点。
这两个素材中我都找不到合适的死亡动画,勉强拿DamageDown
用用,大家别忘了修改其所在的.fbx
文件的Avatar设置,并确保动画的Loop Time
未勾选。
从Any State
新建一个Transition到DamageDown
,设置无退出时间,设置其条件为Die
,这次就不用再设置跳出的Transition了,因为这是死亡状态了嘛。
这里会有一个小bug,因为既然从Any State
都可以过渡到DamageDown,那么从DamageDown
本身也可以,所以我们可以一再激活Die
Trigger来让角色“死了又死”,不过这个bug很容易在脚本中被修复,这里也暂时不管他。
连击
很多格斗游戏或者动作游戏中都有连击(Combo)的概念,大概意思是如果连续攻击判定成立,角色依次播放不同的攻击动画,以达到华丽的视觉效果。
我们这里也利用Animator Controller做一个简单的连击动画逻辑。
在Animator Controller中单击右键,选择Create Sub-State Machine
创建一个“子状态机”,更改名称为“Attack”。
“子状态机”相当于把一个复杂的状态机的一部分“打包”起来,方便我们的设置。
双击进入Attack
,然后搜索出Jab
(左轻拳)、Hikick
(高踢)、Spinkick
(回旋踢)、RISING_P
(升龙拳)四个动画片段(都在HQ Fighting Animation FREE素材中),拖到Attack子状态机中。
新建一个名称为Combo
的Bool类型参数,以及一个名称为Attack
的Trigger类型参数。
依次建立Jab
到Hikick
、Hikick
到Spinkick
、Spinkick
到RISING_P
以及RISING_P
到Jab
的Transition,统统设置成有Exit Time,触发条件为Combo = true。
然后在依次建立从这4个状态到Exit
状态的Transition,统统设置成有Exit Time,触发条件为Combo = false。
这样设置的意思是:如果“连击”条件成立,则依次轮换进行轻拳、高踢、回旋踢、升龙拳四种攻击,否则,就在当前攻击动画完成后进入“退出”状态。
然后返回Base Layer,设置从Localmotion
到Attack
的Transition,无Exit Time,触发条件为Attack
Trigger,同时设置从Attack
到Localmotion
的Transition,不做任何修改。
因为Attack子状态机中的所有状态都回归到
Exit
状态,所以从Attack
到Localmotion
的Transition显示为灰色,表明是自然过渡。我们在Attack子状态机中还可以直接设置某个状态往Base Layer中的某个状态的Transition,那就要连接到
(Up) Base Layer
状态并自行选择目标状态了,我们这次不搞得这么麻烦。
测试发现,从Localmotion
过渡到Jab的时候几乎看不到轻拳攻击的动作,这是因为默认的过渡时间太长,导致出拳动作被“洗”掉了。修改Transition中的动画过渡,让其变得很短就可以了。
运行场景,设置Blend
为0.5,角色开始步行,然后勾上Combo
,再激活Attack
,角色开始“四连击”,由于这几个动作的动画片段都没有设置成原地动画,所以实际在场景中是有位移的,看起来挺爽快。
取消Combo
的勾选,角色回归步行动画。
如果不希望有位移,可以为这几个动画所在的.fbx
文件设置Root Motion:在Animation设置中,找到Motion一栏,然后设置Root Motion Node
为<Root Transform>
,最后点击Apply
确认修改。
控制Animation Controller
准备场景
- 安装PlayMaker插件;
- 为场景添加一个地面Plane(10×10×10);
- 为unitychan游戏物体添加一个Rigidbody组件,一个Capsule Collider组件,修改参数以适合角色模型;
使用PlayMaker操控Animator Controller的参数
PlayMaker提供了一系列与Animator Controller有关的Actions:
我们暂时只关心与Animator参数有关的内容。
为场景中的unitychan添加一个Fsm,改名为AC_Test,我们用这个Fsm来测试一下如何控制Animator Controller的参数,从而操纵角色动画。
在State 1中添加1个Set Animator Float
行为,设置使用speed
变量来控制Blend
参数:
然后在Variable面板中设置speed
变量在Inspector中可见,运行场景,现在就可以在Inspector中通过调整speed
变量来控制角色停走跑了。
可以再添加一个Get Axis Vector
行为,将方向输入向量储存为axis vector
变量,将Magnitude(可以理解为输入向量的长度)储存为magnitude
变量。修改Set Animator Float
行为的Value参数,改为使用magnitude
变量来控制Blend
参数值。
运行场景,现在可以通过“AWSD”或者方向键来操控角色从停到走到跑了。
Get Axis Vector
行为我们在设置角色运动控制中用得非常多,具体可以参见 PlayMaker简单实例(2):角色与摄影机的运动控制 一文中的内容。在不做放大的情况下,Magnitude的取值会在0~1.414之间变化,单轴向最大值1,两个轴向一起最大值为1.4左右(2的平方根)。
在State 1
中添加一个Get Button Down
行为,设置当“Jump”按钮(默认设置是空格键为“Jump”按钮)被按下时,触发事件“Jump”,然后跳转到State 2
。然后再添加一个Set Animator Bool
行为,设置Animator的IsJump
参数值为false。
在State 2
中添加一个Set Animator Bool
行为,设置Animator的IsJump
参数值为true,然后添加一个Wait
行为,设置为等待1.5秒之后触发事件“Landing”,跳转回State 1
。
一些解释:
- 在
State 1
中添加Set Animator Bool
行为的目的是确保IsJump
参数值在State 1
中为false,否则不会停止跳跃动画,这也解决了之前Animator Controller设置的一个小bug。Wait
行为中设置1.5秒的原因是Jump动画大概在1.5秒的时候就完成了,这里这样设置可以保证不会连续多次跳跃。更有效的做法是监测碰撞体是否碰到了地面,或者监测Rigidbody的Y轴速度是否大于某个数值(比如0.1)来确保跳跃动画完成时切换状态。
下面是关于攻击的设置。首先在State 1
中添加一个Get Key Down
行为,设置当F
键被按下时,触发事件“Attack”,跳转到State 3
,同时在State 1
中用Set Animator Bool
行为设置Combo
参数值为false。
在State 3
中:
- 使用
Set Animator Trigger
行为,设置Attack
被触发(所以一进入State 3
角色就开始攻击); - 同时使用
Set Animator Bool
行为设置Combo
参数值为true,开始连击; - 使用
Get Key Down
行为,设置当F
键被按下时,触发事件“Attack”,重新进入State 3
; - 最后使用
Wait
行为,设置0.2秒后触发Stop Attack
事件,跳转到State 1
,结束攻击状态。
这样做的效果是:如果F键被按得足够频繁(间隔 < 0.2s),则State 3
会不停重置,导致Wait
行为一直不能累积足够时间来触发Stop Attack
事件。
这只是一个非常简陋的逻辑,大家可以自行探索更合理更有效的逻辑思路。
这样,我们就制作了控制角色动作的交互逻辑,下面将这套交互与真正的角色运动结合起来。
删除AC_Test或者使其不激活,为unitychan另建一个Fsm:
在State 1
中先设置角色的运动速度:
- 首先用
Get Axis Vector
行为获得基于相机视角的方向输入,储存为input axis
变量,将Magnitude储存为magnitude
变量 - 用
Vector3 Multiply
行为将input axis
变量放大,倍数参数使用新建的speed
变量(设置初始值为6) - 用
Set Velocity
行为给角色添加上运动速度,速度矢量使用input axis
,坐标空间选择世界坐标(World
),勾选Every Frame
选项; - 在
unitychan
的Rigidbody组件中勾选x轴和z轴的旋转约束,避免角色发生“侧翻”。
然后在State 1
中设置角色的面朝方向:
- 用
Get Position
行为获得Owner位置my position
; - 用
Vector Operator
行为将my Position
加上input axis
,储存为aim position
(这是一个角色指向输入方向的位置); - 用
Smooth Look At
行为让角色平滑地朝向该位置,并确定Keep Vertical
选项被勾选以保持Y轴始终指向天空; - 最后使用
Set Animator Float
行为,设置使用magnitude
变量值来控制Blend
参数。
然后再按前面的做法依次添加上对跳跃、攻击等动画的控制。
最后对Animation Controller做一点小修改,删掉从RISING_P
到Jab
的Transition,让RISING_P
成为“终结技”。
受伤和死亡的动画控制就留给大家自己完成吧。
实际应用中对于跳跃动画的实现
真正要做个可玩的跳跃交互逻辑需要对角色的速度以及碰撞有所判断。至少要像下面这样有3个不同的状态。我使用一个刚体(Rigidbody)小方块来做演示:
State 1
中设置了小方块的移动和自动转向的交互逻辑,最后添加一个Get Key Down
来探测Space
键是否被按下,按下则跳转到State 2
。
State 2
中利用Add Force
行为来实现跳起,也就是给角色一个向上的力量,我设置Force Mode为Impulse
可以保证这个力量的一致性和瞬时性。
在Get Velocity
行为中,我实时获取Y轴速度,然后用Float Compare
行为监测其是否为负,为负代表小方块在下降,这时候就可以跳转到State 3
去判断是否落地了。
State 3
中使用Collision Event
行为监测是否有标签为“ground”的碰撞体与角色碰撞,如果有,则跳转回State 1
结束跳跃。
最后把地面物体添加上标签“ground”,这样就可以用空格键控制小方块跳跃了。