unity运行时获取特定动画状态机挂载的脚本

在做人物行为和动画部分时遇到这样一个情况:人会执行多种动画,不同的动画在进入或退出状态机时需要执行不同的操作,或者什么操作都不需要。

方法有两种,其一是写多个类,都继承自StateMachineBehaviour,并为每一个类写进入或退出函数,然后将这些类脚本各自拖到需要的状态机上。第二种方法是,只写一个类,并声明两个委托变量,依次在进入和退出时执行,然后在外界为这两个委托绑定函数,类程序如下:

/// <summary>
/// 挂在每个某些动画的状态机上,在动画开始和结尾执行相关操作
/// </summary>
public class StateOperation : StateMachineBehaviour
{
    private Action enterAction;     //进入时执行
    private Action exitAction;          //退出时执行

    /// <summary>
    /// 添加进入动画时执行的函数
    /// </summary>
    public void AddEnterEvent(Action action)
    {
        enterAction = action;
    }

    /// <summary>
    /// 添加退出动画时执行的函数
    /// </summary>
    public void AddExitEvent(Action action)
    {
        exitAction = action;
    }

    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        enterAction?.Invoke();
    }

    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        exitAction?.Invoke();
    }
}

不同的动画状态需求不一样,有些需要在进入时调用函数,有些需要在退出时调用,或者两者都要,所以另外还需要一个类,游戏运行后为各个状态机添加脚本并绑定函数。

我一开始的实现方法仅仅是简单的添加脚本绑定函数,如下(这种方法是错误的):

		//类中缓存有一个私有变量animator,在构造函数中初始化,构造函数未列出
        private void AddBehaviourAndEvent()
        {
            AnimatorController controller = animator.runtimeAnimatorController as AnimatorController;
            var temp = controller.layers[0].stateMachine.states;        //获取状态机数组

            foreach (ChildAnimatorState item in temp)           //遍历所有状态机
            {
                if(item.state.name == ConstDefinedValue.attack)     //如果是攻击状态,添加脚本并注册相关函数
                {
                    StateOperation operation = item.state.AddStateMachineBehaviour<StateOperation>();		//给状态机添加脚本
                    //紧接着绑定函数
                    operation.AddEnterEvent(() =>
                    {
                        //进入攻击状态时需要执行的函数。。。
                    });
                    operation.AddExitEvent(() =>
                    {
                        //退出攻击状态时需要执行的函数。。。
                    });
                }
                
                if(item.state.name == ConstDefinedValue.jump)       //如果是跳跃状态
                {
                    //同上,添加脚本,然后添加需要执行的函数
                }
            }
        }

但是如果仅仅这样写的话,在运行时执行到该段动画时委托会报空,说明这里的函数并没有绑定上。

进入unity界面运行起来之后看一下挂载上的脚本。

在这里插入图片描述
显示确实挂载上去了,但是如果把Hierarchy视图中的人物点一下,再又点一下状态机。

在这里插入图片描述
这里显示的是一个clone,说明代码添加的脚本对象被它给复制了一份再挂上去的,这个是没有绑定函数的,原来绑定过函数的那个对象不知道跑哪去了(但是代码是在添加脚本后立即绑定函数,即使是后来把对象复制了一份,委托也应该一并复制过去了,不应该报空。另外添加脚本的AddStateMachineBehaviour<T>()函数所在的类是在UnityEditor命名空间下,我猜可能这个函数里面就已经弄出了两个对象,一个是在编辑器界面上用,一个是在运行时用,注册过委托的那个只是编辑器上的对象)。

所以办法就是,先把所有需要添加脚本的全部添加上,并把脚本的引用用Dictionary存储起来,其中key是枚举项,和动画状态机的名字对应,value是哈希表,存储我们添加的脚本的引用。然后依次遍历每一个脚本,把他的name属性(由Object继承来的,每个对象都有)改为这个枚举项转换的字符串,最后通过Animator的GetBehaviours<T>()函数获取到所有克隆后的脚本,因为名字已经被唯一标识,此时就可以根据需要修改特定状态机的脚本。程序如下:

    /// <summary>
    /// 需要添加动画状态机控制脚本的动画枚举项
    /// </summary>
    public enum AnimStateMachineKey
    {
        Jump,
        Dodge,
        Damaged,
        Attack
    }

    public class StateOperationManager
    {
        //存储添加的脚本的引用,和枚举项对应
        private Dictionary<AnimStateMachineKey, HashSet<StateOperation>> stateOperationDic;
        private Animator animator;

        /// <summary>
        /// 构造函数,传入动画对象,创建字典,并依次执行添加动画,更改名字,绑定函数脚本
        /// </summary>
        public StateOperationManager(Animator animator)
        {
            stateOperationDic = new Dictionary<AnimStateMachineKey, HashSet<StateOperation>>();
            this.animator = animator;
            InitializeOperationDic();   //向字典中添加引用
            ChangeBehaviourName();      //改脚本的名字
            AddAllEvents();     //添加监听
        }

        /// <summary>
        /// 在游戏结束时,在其他类中由OnDestroy函数中调用,把添加上的脚本销毁掉,由这个类添加的
        /// 脚本在编辑器中也会添加一份,如果在结束游戏时不销毁的话,脚本会越积越多,一个状态机挂载多份脚本
        /// </summary>
        public void DestroyMachineScripts()
        {
            AnimatorController controller = animator.runtimeAnimatorController as AnimatorController;
            var temp = controller.layers[0].stateMachine.states;
            foreach (ChildAnimatorState item in temp)
            {
                foreach (var item2 in item.state.behaviours)
                {
                    Object.DestroyImmediate(item2, true);
                }
            }
            var temp2 = controller.layers[0].stateMachine.stateMachines[0].stateMachine.states;
            foreach (var item in temp2)
            {
                foreach (var item2 in item.state.behaviours)
                {
                    Object.DestroyImmediate(item2, true);
                }
            }
        }

        /// <summary>
        /// 遍历所有的状态机以和子状态机,把需要添加脚本的添加上,并把脚本的引用存储在字典中
        /// </summary>
        private void InitializeOperationDic()
        {
            AnimatorController controller = animator.runtimeAnimatorController as AnimatorController;
            var temp = controller.layers[0].stateMachine.states;
            foreach (ChildAnimatorState item in temp)   //遍历所有状态机
            {
                if (item.state.name == ConstDefinedValue.damaged)   //如果状态机中是受伤动画
                    AddBehaviourAndSaveToDic(AnimStateMachineKey.Damaged, item.state);
                if (item.state.name == ConstDefinedValue.jump)      //如果是跳跃动画
                    AddBehaviourAndSaveToDic(AnimStateMachineKey.Jump, item.state);
                if (item.state.name == ConstDefinedValue.dodge)     //如果是闪避动画
                    AddBehaviourAndSaveToDic(AnimStateMachineKey.Dodge, item.state);
            }
            var temp2 = controller.layers[0].stateMachine.stateMachines[0].stateMachine.states;
            foreach (var item in temp2)     //遍历所有子状态机
            {
                if (item.state.tag == ConstDefinedValue.attack)     //如果是攻击动画
                    AddBehaviourAndSaveToDic(AnimStateMachineKey.Attack, item.state);
                if (item.state.name == ConstDefinedValue.damaged)       //如果是受伤动画
                    AddBehaviourAndSaveToDic(AnimStateMachineKey.Damaged, item.state);
            }
        }

        /// <summary>
        /// 给状态机添加脚本并把引用存储在字典中
        /// </summary>
        private void AddBehaviourAndSaveToDic(AnimStateMachineKey key, AnimatorState state)
        {
            var temp = state.AddStateMachineBehaviour<StateOperation>();
            if (stateOperationDic.ContainsKey(key))
                stateOperationDic[key].Add(temp);
            else
            {
                stateOperationDic.Add(key, new HashSet<StateOperation>());
                stateOperationDic[key].Add(temp);
            }
        }

        /// <summary>
        /// 给每个脚本改名字,改成对应枚举项的名字,因为脚本也是继承自Object,也有名字
        /// </summary>
        private void ChangeBehaviourName()
        {
            foreach (KeyValuePair<AnimStateMachineKey, HashSet<StateOperation>> item in stateOperationDic)
            {
                foreach (StateOperation key in item.Value)
                {
                    key.name = item.Key.ToString();
                }
            }
        }

        /// <summary>
        /// 通过Animator类中的GetBehaviours()函数获取到所有脚本,这个获取到的就是克隆后的副本
        /// 上一步改名字就是为了这一步,这里获取到的是所有脚本的一个数组,只有通过名字才能知道它分别是
        /// 哪个状态机上的脚本。
        /// </summary>
        private void AddAllEvents()
        {
            foreach (var item in animator.GetBehaviours<StateOperation>())
            {
            	//如果脚本的名字叫Attack(Clone),则添加对应监听,注意是副本,所以名字里要有一个(Clone),下面三个同理
                if (item.name == ConstDefinedValue.attack + ConstDefinedValue.clone)
                    AddEventToAttackStateMachine(item);
                if (item.name == ConstDefinedValue.damaged + ConstDefinedValue.clone)
                    AddEventToDamagedStateMachine(item);
                if (item.name == ConstDefinedValue.jump + ConstDefinedValue.clone)
                    AddEventToJumpStateMachine(item);
                if (item.name == ConstDefinedValue.dodge + ConstDefinedValue.clone)
                    AddEventToDodgeStateMachine(item);
            }
        }

        /// <summary>
        /// 添加攻击动画监听
        /// </summary>
        private void AddEventToAttackStateMachine(StateOperation item)
        {
            item.AddExitEvent(() =>
            {
                //退出攻击状态需要执行的函数
            });
        }

        /// <summary>
        /// 添加受伤动画监听
        /// </summary>
        private void AddEventToDamagedStateMachine(StateOperation item)
        {
            item.AddEnterEvent(() =>
            {
                //进入受伤状态需要执行的函数
            });
            item.AddExitEvent(() =>
            {
                //退出受伤状态需要执行的函数
            });
        }

        /// <summary>
        /// 添加跳跃动画监听
        /// </summary>
        private void AddEventToJumpStateMachine(StateOperation item)
        {
            item.AddExitEvent(() =>
            {
                //退出跳跃状态需要执行的函数
            });
        }

        /// <summary>
        /// 添加闪避动画监听
        /// </summary>
        private void AddEventToDodgeStateMachine(StateOperation item)
        {
            item.AddExitEvent(() =>
            {
                //退出闪避状态需要执行的函数
            });
        }
    }

另外我也尝试了一下Animator和AnimatorController类中的其他获取脚本的方法,通过路径哈希值或者名字获取副本脚本,但是结果都是空,实现这个功能好像只有这一种方法

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值