美文网首页
Unity 动画系列五 常用脚本API

Unity 动画系列五 常用脚本API

作者: 合肥黑 | 来源:发表于2021-11-30 09:36 被阅读0次

    参考
    学习笔记 --- 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。
    image.png
    • 当摇杆滑动时,调用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);//有过度的切换动画,传入参数(动画名字,过度时间)
    

    相关文章

      网友评论

          本文标题:Unity 动画系列五 常用脚本API

          本文链接:https://www.haomeiwen.com/subject/qbkozltx.html