在做人物行为和动画部分时遇到这样一个情况:人会执行多种动画,不同的动画在进入或退出状态机时需要执行不同的操作,或者什么操作都不需要。
方法有两种,其一是写多个类,都继承自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类中的其他获取脚本的方法,通过路径哈希值或者名字获取副本脚本,但是结果都是空,实现这个功能好像只有这一种方法