参考
学习笔记 --- Unity动画系统
Unity动画系统详解10:子状态机是什么?
一、Parameters
1.脚本中获取/设置动画参数的方法
//这里的名称要与Animator窗口中,动画参数的名称对应
//通常对于调用频繁的动画参数我们使用哈希值进行快速访问
int runHash=Animator.StringToHash("Run");
//下面设置/获取动画参数均有使用String参数名称进行映射的重载和使用哈希值进行映射的重载
//获取设置Float类型参数,通常结合Input轴线
animator.GetFloat(blendHash);
animator.SetFloat(blendHash, Input.GetAxis("Horizontal"));
//获取设置Int类型参数
animator.GetInteger(intHash);
animator.SetInteger(intHash,Number);
//获取设置Bool类型参数
animator.GetBool(boolHash);
animator.SetBool(boolHash, true / false);
//触发,取消触发Trigger的方法
animator.SetTrigger(jumpHash);
animator.ResetTrigger(jumpHash);
2.ResetTrigger
转自Unity之碰到哪说到哪-ResetTrigger
ResetTrigger是个what?再此之前我并不知道,准确说看到过但是并没有care。开始了解它,是 因 为 出 BUG 了 !!
- 项目中播放动画统一使用全局的一个通用方法。播放动画接口调用SetTrigger。
- 摇杆开始移动时,调用SetTrigger("Run"),结束时,调用SetTrigger("Idel")。
- 当角色在run时,点击了一个npc,触发寻路接口移动到npc,当然寻路开始时,也会在调用一次settrigger("Run").
- 当寻路过程中,再次控制摇杆移动时(打断寻路),没有问题,但是当停止摇杆时,应该播放idle动作,但是实际停止后还是播放run。可是看log。我明明最后一次调用了SetTrigger("Idle")
So着重看了下SetTrigger。
- SetTrigger可以改变动画状态机的状态,用于触发动画
- SetTrigger是四个接口之一,其他还有SetFloat、SetInt、SetBool
- SetTrigger本质上是SetBool,不同点在于,SetBool有两个可选择的值,false/true。但是SetTrigger比较特殊,调用SetTrigger会自动激活状态,同时又会自动设置状态为false。
- 当摇杆滑动时,调用SetTrigger播放run动画,可以在当前帧通过GetTrigger("homerun") 看到激活状态是true。 当过了一帧后,再次GetTrigger("homerun") 是false。可以看到,trigger会自动回到false。
- 摇杆在滑动角色在跑动时,又调用寻路接口,再次触发SetTrigger("homerun"). 这个时候,homerun的trigger状态又被设置成true。 但是重要的是:因为已经在homerun状态了,unity并不会重新进入这个状态,所以homerun的trigger状态并不会自动进入false。
- 所以在我停止的摇杆的时候,虽然我调用了SetTrigger("comidle"), unity会进入idle状态,但是因为homerun的trigger状态一直是true,所以进入idle状态后,又会进入homerun状态。由此引起的bug。
解决办法ResetTrigger。所以SetTrigger() 之前,我们需要清除可能已经被激活的Trigger。如下方法:
/// <summary>
/// 清除所有的激活中的trigger缓存
/// </summary>
public void ResetAllTriggers(Animator animator)
{
AnimatorControllerParameter[] aps = animator.parameters;
for (int i = 0; i < aps.Length; i++)
{
AnimatorControllerParameter paramItem = aps[i];
if (paramItem.type == AnimatorControllerParameterType.Trigger)
{
string triggerName = paramItem.name;
bool isActive = animator.GetBool(triggerName);
if (isActive)
{
animator.ResetTrigger(triggerName);
}
}
}
}
二、State/Transaction
1.脚本中获取State/Transaction状态信息
首先我们要获取动画层ID
int layerID = animator.GetLayerIndex("Base Layer");
这里的LayerID就是Animator窗口中的动画层从上到下的排序
image.png
之后我们可以通过以下方法来获取State状态信息
AnimatorStateInfo animatorStateInfo;
AnimatorTransitionInfo transitionInfo;
//获取当前状态/过渡出发状态的信息
animatorStateInfo = animator.GetCurrentAnimatorStateInfo(layerID);
//获取将要过渡到的状态信息
animatorStateInfo = animator.GetNextAnimatorStateInfo(layerID);
//获取过渡信息
transitionInfo = animator.GetAnimatorTransitionInfo(layerID);
2.状态的shortNameHash与fullPathHash
我们获取到的状态信息中,并不包含State的名称,而是State的短名和完整名的哈希值
例如这个State名为Idle,那么其ShortNameHash就是哈希 Idle
//我们先预设状态的哈希值
int idleHash = Animator.StringToHash("Idle");
//在Update中加入以下代码
int layerID = animator.GetLayerIndex("Base Layer");
animatorStateInfo = animator.GetCurrentAnimatorStateInfo(layerID);
if (animatorStateInfo.shortNameHash == idleHash)//判定当前状态是否是Idle状态
{
Debug.Log("OnState Idle");
}
测试结果,在Idle状态下产生了输出。
而对于fullPathHash则是追溯动画层,所有子动画组,的整个路径,以及State名称的整个字符串进行哈希算法获得的值。例如对于下面这个状态的fullPathHash为:"Base Layer.FlyMechine.Fly"
image.png
public class InfoDebug : MonoBehaviour
{
Animator animator;
AnimatorStateInfo animatorStateInfo;
int flyHash = Animator.StringToHash("Base Layer.FlyMechine.Fly");
// Start is called before the first frame update
void Start()
{
animator = gameObject.GetComponent<Animator>();
}
// Update is called once per frame
void Update()
{
animatorStateInfo = animator.GetCurrentAnimatorStateInfo(0);
if (animatorStateInfo.fullPathHash == flyHash)
{
Debug.Log("OnState Fly");
}
}
}
运行结果,Fly状态下产生输出
3. tagHash 状态标签
我们可以设置状态的标签名,从而对状态进行归类
image.png
int tagHash = Animator.StringToHash("tagName");
if(animatorStateInfo.tagHash==tagHash){
//do something
}
4.过渡状态的nameHash与userNameHash
对于一个过渡状态,它拥有一个name(下图中对应"Fly -> TakeOn"这个字符串的哈希值,注意空格!!!)以及一个可以在Inspector窗口中设置的userName
image.png
AnimatorTransitionInfo transitionInfo;
transitionInfo = animator.GetAnimatorTransitionInfo(layerID);
Debug.Log(transitionInfo.nameHash);
Debug.Log(transitionInfo.userNameHash);
5.不同状态下 CurrentState NextState Transition 的信息对应
我们抽象出动画状态机三个状态来解释不同阶段下,三种信息的对应关系
状态A 状态A到B的过渡(A -> B) 状态B
在执行状态A时:
- CurrentStateInfo对应状态A的信息
- NextStateInfo和TransitionInfo是空信息,它们中包含的各种哈希值都为0
在执行状态A向B的过渡时(A->B):
- CurrentStateInfo对应状态A的信息
- NextStateInfo对应状态B的信息
- TransaitonInfo对应过渡 A -> B 的信息
在过渡完成,执行状态B时:
- CurrentStateInfo对应状态B的信息
- NextStateInfo和TransitionInfo是空信息,它们中包含的各种哈希值都为0
三、State Machine Behaviour
State Machine Behaviour是一种特殊的脚本。和通用的Unity脚本(MonoBehaviour)挂到GameObject上面类似,StateMachineBehaviour可以挂到Animator Controller的State上面。可以在StateMachineBehaviour脚本中编写代码,在状态进入、离开、停留在特定的state时执行。你就不需要自己去检测状态的变化。
可能用于的场景举例:
- 进入、离开状态时播放音效
- 只在特定的状态中执行一些代码
- 只在特定的状态中激活特效
选中一个State,点击Inspector中的Add Behaviour按钮可以选择已有的StateMachineBehaviour或创建一个新的StateMachineBehaviour。
image.png
image.png
StateMachineBehaviour中有一些预定义的事件方法:
- OnStateMachineEnter 转换到一个StateMachine时调用。注意转换到子状态机中的状态时不会调用。
- OnStateMachineExit 离开StateMachine时调用。注意转换到子状态机中的状态时不会调用。
- OnStateEnter 进入当前State时调用
- OnStateExit 离开当前State时调用
- OnStateUpdate 处于当前状态时,每次Update都会调用(不包括Enter和Exit的两帧)
- OnStateMove 在MonoBehaviour.OnAnimatorMove之后调用。相当于Mono脚本中OnAnimatorMove的作用,使用之前提到的RootMotion模式三,但仅针对这个State状态运行时
- OnStateIK 在MonoBehaviour.OnAnimatorIK之后调用。相当于Mono脚本中的OnAnimatorIK的作用,但仅针对这个State状态运行时
触发方法时,都会将下面三个变量作为参数传入
- Animator:当前脚本所在的State,在游戏运行时对应的Animator组件
- AnimatorStateInfo:当前脚本所在的State的信息
- layerIndex:当前脚本所在的State,所在动画层的ID
因此相较于Mono脚本,StateMechinBehaviour脚本能够直接获取到Animator组件以及State信息,并在对应的接口执行一些控制逻辑。并且StateMechinBehaviour脚本能够直接针对某个状态实施一些逻辑,不需要像Mono脚本中针对一些参数,先判定State状态,再进行设定。
一个StateMechineBehaviour可以被挂载多个State上,我们可以根据传入的StateInfo进行分支逻辑,但通常我们都会针对一个State专门创建出一个SMB。
1.OnStateEnter/OnStateUpdate/OnStateExit 的具体触发细节
我们在Unity2018.3版本测试了上面这三个方法,在正常过渡的情况下,以及过渡打断的情况下的触发细节,一遍我们更好的使用上面三个方法。以下我们是经过测试所得出的结论,测试过程相关这里就不过多赘述了
Case 1:
我们抽象出
- 状态A
- 状态A到B的过渡(A->B)
- 状态B
这样的两个状态进行正常过渡的情况下
-
当执行状态A时:
每帧执行OnStateUpdata_A -
当状态A到B的过渡被触发的那一帧:
(OnStateEnter会在指向这个状态的过渡被触发时执行)
执行了OnStateEnter_B
执行了OnStateUpdate_A -
当执行状态A到状态B的过渡时:
(执行过渡的过程中,每帧先执行CurrentState的Update,之后执行NextState的Update)
(这个过渡状态下Update的执行顺序是绝对的)
每帧先执行OnStateUpdata_A,后执行OnStateUpdata_B -
当进入到状态B时的那一帧:
(正常过渡下,进入到其它状态的那一帧会执行上一状态的Exit)
执行了OnStateExit_A
执行了OnStateUpdata_B -
当执行状态B时:
每帧执行OnStateUpdata_B
Case 2:
我们抽象出
- 状态A
- 状态A到B的过渡(A->B)
- 状态B
- 打断(A->B)过渡并指向C的过渡(->C)
这样的执行状态A->B的过渡被打断,并转而向状态C过渡的情况。这里无所谓(->C)究竟是(A->C)Current打断或是(B->)Next打断,我们在这两种情况下得到了相同的结论。
-
当执行状态A时:
每帧执行OnStateUpdata_A -
当状态A到B的过渡被触发的那一帧:
执行了OnStateEnter_B
执行了OnStateUpdate_A -
当向状态B过渡的过程中:
每帧先执行OnStateUpdata_A,后执行OnStateUpdata_B -
当过渡被打断的那一帧:
(Exit被触发的另一种情况,当指向该State的过渡被打断时触发)
执行了OnStateUpdata_A
执行了OnStateExit_B
执行了OnStateEnter_C -
当向状态C过渡的过程中:
每帧先执行OnStateUpdata_A,后执行OnStateUpdata_C -
当进入到状态C时的那一帧:
执行了OnStateExit_A
执行了OnStateUpdata_C -
当执行状态C时:
每帧执行OnStateUpdata_C
总结:
我们不难发现在正常过渡,以及过渡打断的情况下,任意State中的三个状态方法都是能够形成闭环,在不同状态的切换中,保证都被执行的。结合对上面测试的理解,我们就可以将原先写在Mono脚本中的一些动画参数设置的方法,通过StateMechinBehaviour的三个状态方法来进行简洁,快速的实现。
image.png
int jumpHash = Animator.StringToHash("Jump");
//先预存哈希值
//Update中判定状态以及输入,进行Trigger设定
animatorStateInfo = animator.GetCurrentAnimatorStateInfo(0);
animatorStateInfo2 = animator.GetNextAnimatorStateInfo(0);
if (animatorStateInfo.shortNameHash == runStateHash || animatorStateInfo2.shortNameHash == runStateHash)
{
//这里我们支持由Idle状态快速向Jump状态过渡,因此不止时CurrentState为RunState
//在执行Idle向Run状态的过渡,NextState为RunState时就允许设置Trigger
//我们也开启了 Idle->RunTree 针对NextState的打断
//从而站立起跑的瞬间就可以进行冲刺翻身跳的过渡
if (Input.GetMouseButtonDown(0))
{
animator.SetTrigger(jumpHash);
}
}
else
{
animator.ResetTrigger(jumpHash);
}
我们可以运用StateMechineBehaviour来实现上面的功能,将下面的脚本挂载在RunTree状态上即可。可以看到下面的代码要简洁许多,包括快速过渡的功能在内,下面的代码与上面的代码所实现的功能完全相同
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StateMechine : StateMachineBehaviour
{
int jumpHash = Animator.StringToHash("Jump");
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (Input.GetMouseButtonDown(0))
{
animator.SetTrigger(jumpHash);
}
}
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.ResetTrigger(jumpHash);
}
}
2.OnStateIK/OnStateMove的触发与细节
OnStateIK与OnStateMove,的触发细节相同,都是在这三个时期被调用:
- 指向该状态的过渡中
- 该状态运行时
- 从该状态触发的过渡中
对于OnStateMove的作用效果:
- 如果Mono脚本中不实现OnAnimatorMove,那么OnStateMove在触发时,都将覆盖掉Apply Root Motion的勾选与不勾选
- 当执行过渡时,角色将受到所有被触发的OnStateMove共同作用
- 如果脚本中实现OnAnimatorMove,角色将受到所有被触发的OnStateMove与脚本中的OnAnimatorMove共同作用
OnStateIK的作用效果是同所有触发的OnStateIK和Mono中的OnAnimatorIK一起作用于角色。通常我们只使用OnStateMove/OnAnimatorMove之一,OnStateIK/OnAnimatorIK之一来进行RootMotion,以及IK的控制,使用OnStateMove/OnStateIK时可通过传参判定状态,避免过渡状态下的共同作用。
3.挂载在Layer动画层/子动画组上的StateMechineBehavior
StateMechieBehavior还可以被挂载在动画层(动画组)上
image.png
此时StateMechineBehaviour的接口方法调用就变为了:
- OnStateEnter:该动画层(组)(包括子动画组)中的任何一个State在Enter时调用
- OnStateUpdate:该动画层(组)(包括子动画组)中的任何一个State在运行时调用
- OnStateExit:该动画层(组)(包括子动画组)中的任何一个State在退出时调用
- OnStateMove:完全相当于Mono脚本中OnAnimatorMove的作用,使用之前提到的RootMotion模式三,但仅针对这个动画层(组)运行时
- OnStateIK:完全相当于Mono脚本中的OnAnimatorIK的作用,会在后面AnimatorIK中被一起提到,,但仅针对这个动画层(组)运行时
此时StateMechineBehaviour的作用范围是该动画层(组),以及所有子动画组中的状态。触发OnStateEnter/OnStateUpdate/OnStateExit,传入的AnimatorStateInfo会是对应状态的Info,注意使用分支逻辑。
OnStateMove的触发细节与作用效果与挂载在State上时的触发细节和作用效果类似。指向该动画层及子层的过渡中,该动画层及子动画层被运行时,从该动画层出发向父层级的过渡中,OnStateMove都会被触发。
如果Mono中不实现OnAnimatorMove,OnStateMove将覆盖Apply Root Motion的勾选与不勾选,并同所有被触发的OnStateMove共同作用于角色。或是同所有被触发的OnStateMove,与Mono脚本中实现的OnAnimatorMove一起作用于角色。
OnStateIK的触发细节与OnStateMove相同。作用效果和之前一样,同所有触发的OnStateIK和Mono中的OnAnimatorIK一起作用于角色。
四、位置预判SetTarget
测试效果,可以看到平地无阻挡情况下能够正确预判位置,放置圆环。但如果受到其它物理互动影响根节点(重力,碰撞),预判位置仍是理想的动画效果的位置。
主角即将到达某个位置时,生成一个圆环
我们可以使用位置预判,在执行一段动画的过程中,预判当NormalizedTime(百分比进程)到达某一时刻时,人物某一节点的位置和方位。相关方法:
animator.SetTarget(AvatarTarget, normalizedTime);
animator.targetPosition;//获取预判位置的属性
animator.targetRotation;//获取预判方位的属性
AvatarTarget是一个节点枚举类,包括:Body(重心),Root(根节点),Left/Right Hand(左右手),Left/Right Foot(左右脚)
注意!!! SetTarget和获取属性不能在同一时间被一起使用。SetTarget需要多帧的执行,进行运算,才能找到正确的位置和方位,如果获取时SetTarget还没有运算完成,那么将返回所在物体的Transform对应位置和方位。
并且这个方法只能在Apply Root Motion情况下,进行预判,原理是根据Clip中节点的运动进行位置和方位的计算,如果人物受到重力,或碰撞体阻拦,影响到了根节点的运动,那么预判位置就会与实际位置不符。
一段示例代码,在Update中
animatorStateInfo = animator.GetCurrentAnimatorStateInfo(0);
if (animatorStateInfo.shortNameHash == jumpHash)
{
if (setPos)
{
animator.SetTarget(AvatarTarget.Body, 0.44f);
if (animatorStateInfo.normalizedTime > 0.2f)
{
setPos = false;
Circle.position = animator.targetPosition;
Circle.forward = transform.forward;
}
}
}
else
{
setPos = true;
}
四、Animation API
Play("ation 1" );//播放动画,传入参数为动画名字
Stop("ation 1");//停止动画,传入参数为动画名字
CrossFade("ation 1", 0.5f);//有过度的切换动画,传入参数(动画名字,过度时间)
网友评论