Unity 动画系列五 常用脚本API

参考
学习笔记 --- 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);//有过度的切换动画,传入参数(动画名字,过度时间)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,820评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,648评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,324评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,714评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,724评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,328评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,897评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,804评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,345评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,431评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,561评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,238评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,928评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,417评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,528评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,983评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,573评论 2 359

推荐阅读更多精彩内容