文章目录
Animator Controller
Animator Controller 允许我们定义动画状态,以及动画状态之间的切换条件,来驱动游戏角色播放不同的动画,表现出不同的行为。
- 动画状态机,执行整个状态机逻辑,驱动动画状态运行,执行状态切换。同时定义了驱动参数,可以由游戏逻辑驱动这些参数,再由这些参数驱动动画。
- 动画状态,执行动画逻辑,我们可以添加我们的脚本,来执行动画相关的逻辑。建议是纯表现的逻辑。如果是游戏状态相关,最好不要由动画来驱动,也就是,逻辑驱动动画,而不是动画驱动逻辑。因为触发动画切换的因素太多且不确定,比如网络消息会发生阻塞,网络恢复后会导致大量动画事件;或者战斗过程中,可以被打断的动画导致动画播放序列每次都不一样。
- 动画切换,由参数,和比较条件构成,当条件满足时,会切换到响应的动画。每个状态,可以创建多个到其它状态的切换。
状态
每个状态对应一个动画,所以叫动画状态。而我们的游戏逻辑状态,可能由一系列的动画状态构成。
系统状态
动画状态机创建时,会默认创建3个系统状态:
- AnyState:任意状态,当我们需要在条件满足时,无论当前是哪个状态,都执行切换时,可以定义切换为从该状态开始。
- Entry:当进入该状态时,会首先执行从该状态到默认状态的切换。
- Exit:当需要推出该子状态机时,可以执行一个到该状态的切换。
- UserState:用户定义状态,可以设置为默认状态(右键,Set As Default State),可以为状态添加我们派生子自StateMachineBehaviour 的状态逻辑类。
StateMachineBehaviour
派生该类,并实现相应的接口,来执行一些逻辑。
- OnStateEnter: 当进入该状态时调用
- OnStateUpdate:该状态每帧更新
- OnStateExit:状态推出时调用。当状态切换复杂时,该接口可能不会被调用,比如每帧执行一个切换A->B->C,则B的退出可能不会调用,因为切到C时,A还在推出状态。这也是我们不推荐动画驱动逻辑的原因。
- OnStateMove:如果root motion开启,则在Animator.OnAnimatorMove之后调用。可以在这里执行一些动画位移后的逻辑。
- OnStateIK:如果开启了IK动画,在Animator.OnAnimatorIK之后调用。可以处理IK相关的逻辑。
状态切换
状态切换的触发,依赖于状态机定义的参数。
程序参数
- Float : 浮点数 Animator.SetFloat(stirng name, float value)
- Int : 整肃 Animator.SetInt(string name, int value)
- Bool :布尔 Animator.SetBool(string name, bool value)
- Trigger:触发器 Animator.SetTrigger(string name), Animator.ResetTrigger(string name)
其中,Float,Int,Bool,代码中设置好值后,可以在状态切换条件中选择,并设置比较方式。
但是需要注意的是Trigger类型,设置好后,如果我们调用GetTrigger()接口,则该参数会被重置,也就是说, SetTrigger 后的第一次 GetTrigger 是有效的,文档中的描述是被动画切换使用后重置。
一个状态的切换,同时仅一个可以激活并执行切换。但是,在当前的切换过程中,如果设置了切换打断,则可以触发其它的切换。
切换属性
可以选择一条切换线,在Inspector面板设置切换属性来控制切换:
属性 | 功能 |
---|---|
Has Exit Time | 是否启用Exit Time。Exit Time的逻辑功能类似于float参数,但是不能进行设置。 |
Exit Time | 如果Has Exit Time选中,则将normalizedTime>ExitTime添加到切换条件中。该时间是一个0-1的归一化时间,用来定义在动画播放的时间百分比。比如0.9,表示动画播放到90%时的第一帧,条件为true,切换才有效,到下一帧,就无效了,这点要格外注意。 如果动画是循环的,并且该值设置成小于1的数,则每个循环,会执行一次检查。 如果设置为大于1的数,则会在执行若干次循环后检测,比如3.5,表示动画循环3次后,第四次播放到50%进行检测。 |
Fixed Duration | 该复选框选中,表示用秒来定义状态切换过程时间,否则,用归一化时间0-1 |
Transition Duration | 设置进行切换状态时的切换时间。可以在切换图中看到该参数。 |
Transition Offset | 目标状态的时间偏移,即从哪个时间开始播放目标状态动画。比如0.5,表示目标状态从50%开始播放动画。 |
Interruption Source | 用来定义打断事件源 |
Ordered Interruption | 打断时,是否考虑优先级(状态的切换列表顺序,上面地优先级高) |
Conditions | 进行切换的条件,可以由一个,多个,或者没有条件。如果没有条件,则只考虑Exit Time,当符合条件时进行切换。如果有多个条件,只有所有条件都为true,才执行切换。 条件组成:参数名字,判断条件,判断值 如果勾选了Has Exit Time,则Exit Time也将作为条件之一。 |
Transition Interruption
关于Transition Interruption,有一篇专门的讲解我已经翻译好点这里
利用 Interruption Source 和 Ordered Interruption ,来控制切换可以被如何打断。
Interruption Source 属性
AnyState 的切换,永远被优先考虑,其它的参考 Interruption Source 的设置
Value | 功能 |
---|---|
None | 不可打断(AnyState除外,依然可以打断) |
Current State | 当前状态的其它切换如果触发,可以打断当前正在执行的切换 |
Next State | 下个状态的切换如果触发,可以打断当前正在执行的切换 |
Current State Then Next State | 先检查当前状态,再检查下个状态,如果切换触发,则打断 |
Next State Then Current State | 先检查下个状态,再检查当前状态,如果切换触发,则打断 |
Ordered Interruption 属性
复选项,如果选中,则高优先级的切换,不会被低优先级的切换打断,但是高优先级可以打断低优先级的切换。
只有 AnyState 可以被自己打断。
Transition graph
要调整上面的值,可以直接再相关的区域直接输入,也可以使用切换图。切换图用图形化的方式来修改相关值。
- 拖动 Duration out 符号,来改变 Duration 参数的值
- 拖动 Duration in 符号,来改变 Duration 和 Exit Time 参数的值
- 拖动 Target State 来调整 Transition Offset
- 拖动 Preview playback 符号,来在Inspector窗口底部预览切换效果。
Blend Tree 状态之间的切换
如果切换的当前或下个状态,是一个Blend Tree的状态,则Blend Tree的参数也会在Inspector面板上显示。调节这些参数可以预览不同的Blend Tree参数配置对切换的影响效果。如果Blend Tree包含2个长度不同的动画,那么需要测试播放这两个动画时的切换的结果。调节这些参数不会影响运行时效果,仅仅是在编辑时查看效果。
Conditions
状态切换可以有一个条件,多个,或者没有条件。如果切换没有条件,那么动画系统也会将Exit Time 作为唯一的条件,当到达时间时触发切换。如果有多个条件,必须所有条件都满足,才触发切换。
条件包括
- 事件参数名字
- 条件判断(小于或大于)
- 条件参数,进行判断的值
IK 动画
Unity IK
IK 动画,即 Inverse Kinematics(反向动力学)动画,意思是由子骨骼的运动反过来影响父骨骼。Humaniod的Mecanim 支持IK,只需要选中动画Layer,点击其右上角的齿轮设置按钮,并在其弹出菜单中选中IK选项。
之后我们就可以在代码中调用Animator的IK相关的接口来控制IK动画
- Animator.SetIKPositionWeight(AvatarIKGoal goal, float value):IK位置权重,该权重是用于跟当前骨骼动画计算的位置进行插值。参数AvatarIKGoal,是要影响的骨骼,目前定义有左右手,左右脚。
- Animator.SetIKPosition(AvatarIKGoal goal, float value):IK位置
- Animator.SetIKRotationWeight(AvatarIKGoal goal, float value):IK旋转权重,含义同上
- Animator.SetIKRotation(AvatarIKGoal goal, float value):IK旋转
- Animator.SetLookAtPosition(Vector3 lookAtPosition):设置看向某个目标点
- vecotr3 Animator.bodyPosition:设置身体质心的世界位置,该值只能在OnAnimatorIK()函数调用中设置。
- vecotr3 Animator.bodyRotation:设置身体质心的世界旋转,该值同样只能在OnAnimatorIK()中设置。
在使用IK的过程中,根据经验,如果我们是要抓住一个东西,不能直接把这个物体的位置和旋转设置给IK,因为这样角色的手就穿插到物体里了,通常我们为该物体创建一个“把手”,使用该把手作为IK的目标点。
FinalIK
Unity内置的IK系统,能实现的功能有限,可能无法满足我们的需求,那么可以使用FinalIK,一个第三方的IK动画组件。
这里推荐一篇腾讯游戏学院的入门文章:Unity3d Final IK插件的使用
Root Motion
Animation Root Transform
通常情况下,我们播放动画,根据配置的移动和旋转速度,在代码中计算每帧的旋转和位移,并应用到角色的Transform上,但事实上,角色的运动并不总是匀速的,即使时跑动,速度也是一个时刻变化的值。尤其是当由动画融合时,比如走,跑切换,我们很难定义切换过程中,速度的变化,使角色的动画跟位移完美进行匹配。
这时我们可以考虑一种更加逼真的方式,由美术在3dMax动画软件中编辑好位移和旋转随动画一起导出到FBX中,Animator在播放动画时直接将美术调好的动画应用到角色上。
首先我们要正确设置动画
相关设置:
Root Transform Rotation:旋转控制,表示动画中的Root的旋转将会应用到角色上。
Bake into Pose,表示将该选装烘焙到动画姿态中,可以简单认为,关闭了Root Transform Rotation对我们角色的控制
Root Transform Position(Y):高度控制,Root Transform的高度,是否能影响我们的角色。Bake into Pose同样表示不影响
RootTransform Position(XZ):位移控制,即在平地上的移动。Bake into Pose含义相同
当我们设置好动画,比如我们开启了在XZ平面上的位移控制,然后需要在角色的AnimatorController的Inspector面板中,勾选ApplyRootMotion选项,则播放该动画时,角色的位移将由动画控制。该选项也可以在代码中,通过Animator.applyRootMotion 在实时运行过程中修改。
MonoBehaviour.OnAnimatorMove
如果动画中没有位移信息,我们可以在代码中根据配置的速度更新角色位置。可以通过实现MonoBehaviour.OnAnimatorMove()来实现:
public class MyMove : MonoBehaviour
{
void OnAnimatorMove()
{
float moveSpeed = 1.0f;
Vector3 newPos = transform.position;
newPos += transform.forward * moveSpeed * Time.deltaTime;
transform.position = newPos;
}
}
当然,还有一种方法,就是可以在Animation的设置面板中,创建Curve,并命名为“RunSpeed”:
该曲线定义了动画过程中的速度。
为了使用该参数,我们还需要在AnimatorController中,定义一个同名的float参数
然后,像上面,可以在代码中利用该曲线参数控制角色移动:
public class MyMove : MonoBehaviour
{
void OnAnimatorMove()
{
Animator animator = GetComponent<Animator>();
float moveSpeed = animator.GetFloat("RunSpeed");
Vector3 newPos = transform.position;
newPos += transform.forward * moveSpeed * Time.deltaTime;
transform.position = newPos;
}
}
Animation Curves,还可以实现一些其它的逻辑,比如定义动画过程中CharactorController的胶囊体的Height和YOffset,来确保动画过程可以穿过一些狭小的空间。
Animation Layers
Unity使用 Animation Layers 来管理不同的身体部分的复杂动画状态机。例如下半身负责跑跳,上半身负责投掷,射击。
选择AnimatorController的Layers面板,来编辑Layers。
通过点击右上角的小“+”号来创建一个层。
通过Layer右上角的小齿轮按钮,打开Layer控制面板
通过Weight参数控制Layer,0表示禁止该Layer。
每个Layer,可以为其指定Mask(动画将要被应用到角色的哪个部分),以及Blending混合类型。Override表示其它层的动画数据将被忽略掉。Additive表示动画将加到上面的层上。
Mask属性用来指定该Layer在哪些骨骼上播放动画。比如,只在上半身播放投掷动画,下半身保持走,跑,或站立。可以像下面这样,指定Mask
符号"M"表示该Layer设置的Mask。
Animation Layer syncing
有时候,我们可能希望复用其它Layer定义的状态机。例如,我们要模拟受伤行为,而且有受伤时的走,跑,跳动画,对应正常状态的走跑跳。这时可以在受伤层上,选择Sync,并为Source Layer 选择正常状态的Layer。则受伤Layer的状态机会自动根据Source Layer的状态机创建相同的状态机。我们也可以在该Layer内修改状态机,会同步到同步Layer,但是,状态动画,是单独的,修改一个层的状态的动画,不会影响到其它层。也就是只同步状态机结构。
Timing 复选框,用来调整在同步Layers的动画播放时间,由参数weight决定,用来解决2个Layer的动画长度不一样的情况。如果没由选择Timing,同步Layer的动画将被调整,具体是根据原始层的动画长度来拉伸动画时长。如果选择了Timing,则会在2个动画之间,根据weight进行平衡。
Animator Override Controller
在实际的开发过程中,可能不同的角色,使用的状态机结构及切换逻辑都是一样的,只是各自的动画是不同的,这时可以用 Animator Override Controller,创建方法同 Animator Controller 一样,创建好后,在Project 窗口中选中,在Inspector面板中,修改自己要播放的动画就行了
手动采样动画 Sample Animation
有时我们可能需要在编辑器中实现自己的动画播放预览,能够拖动播放,或者我们需要根据动画生成一些数据,比如生成GPU Skinning数据,这时我们就需要在非运行时播放动画。
实现该功能,我们只需要拿到AnimationClip对象,调用其SampleAnimation即可
Animator animator = GetComponent<Animator>();
AnimatorController ac = (AnimationController)animator.runtimeAnimatorController;
AnimationClip[] clips = ac.animationClips;
AnimationClip animClip = clips[0];
animClip.Sample(gameObject, animClip.length*0.5f);
Blend Trees
Blend trees 可以看我的专门的一篇文章:
Blend Trees