Unity 使用StateMachineBehaviour让动画控制器状态自动关闭跳出

早期在动画控制这里经常遇到这样的问题,例如有一个角色,我们想让他执行wave的挥手动画,我们一般会调用

animator.SetInteger("wave", 1);

我们希望动画wave播放完毕后要退出。可以制作如下:

在这里插入图片描述

有经验的朋友知道,这样直接执行的话会出现反复挥手的动画调用,因为动画变量wave已经被改成了1。

在进入Exit后回到Default State后,因为wave是1,所以又进入了wave。
请添加图片描述

发现有的初学者在游戏业务脚本中去控制wave的关闭,例如延迟一个时间,然后再次调用

animator.SetInteger("wave", 0);

这样的确也是一个办法,但是这样稍微复杂一点的动画控制起来就很麻烦,特别是做动画的动画师也没办法自己调试动画,就非要程序的介入, 特别是对不会写代码的动画师来说更是如此。

本来是个很简单的东西,发现很多初学者不知道,这里就分享出来。利用StateMachineBehaviour脚本来制作。

先分享一个我早期的一个脚本,可以借鉴一下:

using UnityEngine;
using System.Collections;

//用来控制参数状态 add by lpj thinbug 2016/6/3
// Input state
[System.Serializable]
public struct BoolData
{
    public string name;
    
    public bool data;
}
[System.Serializable]
public struct FloatData
{
    public string name;
    
    public float data;
}
[System.Serializable]
public struct IntData
{
    public string name;
    
    public int data;
}
public class TBAnimatorParmChange : StateMachineBehaviour
{
    
    public BoolData[] BoolExit;
    public FloatData[] FloatExit;
    public IntData[] IntExit;

    public BoolData[] BoolEnter;
    public FloatData[] FloatEnter;
    public IntData[] IntEnter;
    //OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        int i;
        for (i = 0; i < BoolEnter.Length; i++)
        {
            animator.SetBool(BoolEnter[i].name, BoolEnter[i].data);
        }
        for (i = 0; i < FloatEnter.Length; i++)
        {
            animator.SetFloat(FloatEnter[i].name, FloatEnter[i].data);
        }
        for (i = 0; i < IntEnter.Length; i++)
        {
            animator.SetInteger(IntEnter[i].name, IntEnter[i].data);
        }
    }

    // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
    //override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    //
    //}

    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        int i;
        for (i = 0; i < BoolExit.Length; i++)
        {
            animator.SetBool(BoolExit[i].name, BoolExit[i].data);
        }
        for (i = 0; i < FloatExit.Length; i++)
        {
            animator.SetFloat(FloatExit[i].name, FloatExit[i].data);
        }
        for (i = 0; i < IntExit.Length; i++)
        {
            animator.SetInteger(IntExit[i].name, IntExit[i].data);
        }
    }

    // OnStateMove is called right after Animator.OnAnimatorMove(). Code that processes and affects root motion should be implemented here
    //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    //
    //}

    // OnStateIK is called right after Animator.OnAnimatorIK(). Code that sets up animation IK (inverse kinematics) should be implemented here.
    //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    //
    //}
}

我们来看看如何使用吧。
第一步:因为我们希望在进入Wave动画后就可以关闭变量wave了,所以我们点击Wave State,点击按钮Add Behaviour,如图:

在这里插入图片描述
这里就会出现我刚才添加的脚本TBAnimatorParmChange。

因为wave是int型变量,所以我们在Int Enter事件中添加
在这里插入图片描述
意思就是在OnStateEnter的时候,把wave变量改为0,这样wave运行完毕后就直接退出不会再次进入了。

我们来看看效果:
请添加图片描述
当我把变量wave改为1后,动画进入Wave后wave变量又称为了0。

这里就是StateMachineBehaviour最简单的用法。

引用:
StateMachineBehaviour

<think> 我们正在讨论如何在Unity中为StateMachineBehaviour添加动画事件。根据引用[1]的内容,我们知道StateMachineBehaviour是通过Animator状态机中的状态来添加的。但是,动画事件(Animation Events)通常是在动画片段(Animation Clip)中直接添加的,它们可以调用游戏对象上的方法。然而,StateMachineBehaviour本身是一个脚本,它附加在状态机的状态上,而不是游戏对象上。因此,我们需要一种机制,使得动画事件能够触发StateMachineBehaviour中的方法。 解决思路: 1. 在StateMachineBehaviour脚本中定义公共方法,这些方法可以被动画事件调用。 2. 由于StateMachineBehaviour脚本是附加在状态上的,而动画事件需要调用游戏对象上的方法,因此我们需要在StateMachineBehaviour脚本中获取到游戏对象,然后让动画事件调用该游戏对象上的某个方法,该方法再转发给StateMachineBehaviour中的方法。 3. 或者,我们可以在StateMachineBehaviour中注册一个事件系统,但这样会复杂一些。 更直接的方法: 由于动画事件只能调用游戏对象上的方法,我们可以让StateMachineBehaviour在进入状态时,将其自身注册到游戏对象的某个组件中(例如一个代理组件),然后动画事件调用代理组件的方法,再由代理组件调用当前状态StateMachineBehaviour的特定方法。 但是,这种方法需要维护状态和代理组件之间的关系,可能比较复杂。 另一种简单且常用的方法是: 1. 在StateMachineBehaviour中,我们可以通过Animator组件获取到游戏对象(animator.gameObject)。 2. 然后,我们可以在游戏对象上添加一个脚本,该脚本作为动画事件调用的桥梁。这个桥梁脚本可以有一个方法,该方法再调用StateMachineBehaviour中的方法。但是,如何让桥梁脚本知道当前活动的StateMachineBehaviour实例呢? 考虑到一个游戏对象可能有多个状态,且同一时间可能有多个状态处于活动状态(不同的层),所以我们需要在桥梁脚本中维护当前活动的StateMachineBehaviour实例的引用。这可能会变得复杂。 因此,我们可以采用以下设计: 步骤: 1. 创建一个桥梁脚本(例如:AnimationEventBridge),将其附加到Animator所在的游戏对象上。 2. 在StateMachineBehaviour的OnStateEnter方法中,将当前StateMachineBehaviour实例注册到桥梁脚本中(通过设置桥梁脚本的一个引用变量)。 3. 在StateMachineBehaviour的OnStateExit方法中,从桥梁脚本中注销。 4. 在桥梁脚本中,提供一个公共方法(例如:OnAnimationEvent),该方法会调用当前注册的StateMachineBehaviour实例的相应方法。 注意:这种方法假设同一时间同一层只有一个状态,或者我们需要处理多个状态的情况。为了简化,我们可以设计为每个层都有一个独立的桥梁,或者使用一个列表来存储多个状态。但这样会增加复杂度。 另一种更简单的方式(如果每个状态只关心自己的动画事件): 我们可以在StateMachineBehaviour中,通过游戏对象发送消息(SendMessage)的方式,但是这样不够直接,且效率较低。 考虑到复杂性和通用性,我们可以这样实现: 在桥梁脚本中,我们存储一个字典,以动画层索引(layerIndex)为键,以该层当前活动的StateMachineBehaviour实例为值。这样,当动画事件发生时,我们可以指定要通知的层,然后调用该层当前状态的相应方法。 但是,动画事件本身并不能传递层索引。因此,我们需要在动画事件中传递一个标识(比如状态名或层索引)来指定要调用的状态。 这可能会让动画事件设置变得复杂。 因此,我们采用一种更直接的方法:每个状态机行为都对应一个唯一的桥梁方法,但这样需要每个状态机行为都在游戏对象上有一个代理方法。 考虑到这些,我们回到最初的想法:既然动画事件只能调用游戏对象上的方法,那么我们在游戏对象上添加一个脚本,该脚本有一个公共方法,这个方法又调用StateMachineBehaviour中的方法。那么如何关联呢? 我们可以这样做: 在StateMachineBehaviour脚本中,我们在OnStateEnter时,将自身的一个方法(比如一个委托)注册到游戏对象的某个中心服务(比如一个字典,以状态名为键,以委托为值)。然后在游戏对象的桥梁脚本中,提供一个公共方法,该方法接受一个状态名和一个事件名,然后调用该状态名对应的委托(如果存在)。 但这样还是有点复杂。 实际上,Unity动画事件支持传递一个字符串参数,我们可以利用这个参数来区分要调用的方法。同时,我们也可以传递其他参数(整数、浮点数、字符串等)。 因此,我们可以设计一个桥梁脚本,它有一个公共方法,比如: ```csharp public void TriggerStateMachineBehaviourEvent(string methodName, float floatParameter, int intParameter, string stringParameter) { // 这里,我们如何找到当前活动的StateMachineBehaviour呢?我们可以遍历所有层,然后找到当前状态,再调用其上的方法?这样效率不高。 } ``` 这个方法的参数与动画事件的参数一致(动画事件最多可以传递一个float, int, string和object,但实际只支持前三种)。 然后,在StateMachineBehaviour中,我们定义一些方法,然后在桥梁脚本中我们调用这些方法。但是,桥梁脚本并不知道当前状态是哪个StateMachineBehaviour。 所以,我们需要在桥梁脚本中维护一个当前活动的StateMachineBehaviour的列表(按层)。然后在触发动画事件时,由动画事件指定要通知的层,然后调用该层当前状态的特定方法。 具体步骤: 1. 创建一个桥梁脚本,附加到Animator所在游戏对象上。该脚本包含一个字典,用于存储每个层(layerIndex)当前活动的StateMachineBehaviour实例。 2. 在StateMachineBehaviour的OnStateEnter中,将自己注册到桥梁脚本中(指定层索引)。 3. 在OnStateExit中,从桥梁脚本中注销。 4. 桥梁脚本提供一个公共方法,比如: ```csharp public void InvokeForLayer(int layerIndex, string methodName, object parameter) { if (activeBehaviours.TryGetValue(layerIndex, out var behaviour)) { // 使用反射调用behaviour的methodName方法,参数为parameter // 或者,我们可以定义一些预定义的方法名,然后通过switch调用 } } ``` 5. 在动画事件中,调用桥梁脚本的InvokeForLayer方法,传递层索引、方法名和参数。 但是,使用反射会影响性能,尤其是在频繁调用时。 为了避免反射,我们可以要求StateMachineBehaviour实现一个包含特定方法的接口,然后桥梁脚本中调用接口的方法。 例如,我们定义一个接口: ```csharp public interface IAnimationEventHandler { void OnAnimationEvent(string eventName, float floatParam, int intParam, string stringParam); } ``` 然后,让StateMachineBehaviour实现这个接口。 在桥梁脚本中,我们可以这样调用: ```csharp public void InvokeForLayer(int layerIndex, string eventName, float floatParam, int intParam, string stringParam) { if (activeBehaviours.TryGetValue(layerIndex, out var behaviour)) { if (behaviour is IAnimationEventHandler handler) { handler.OnAnimationEvent(eventName, floatParam, intParam, stringParam); } } } ``` 在动画事件中,我们这样设置: - 函数:选择桥梁脚本的InvokeForLayer方法。 - 传递参数:层索引(int)、事件名(string)、float参数、int参数、string参数。 这样,每个StateMachineBehaviour都可以通过实现IAnimationEventHandler接口来处理事件。 但是,这种方法需要动画事件中指定层索引,而层索引是数字,我们可以在动画事件中作为int参数传递。 另外,我们也可以不按层,而是按状态名或其他标识,但层索引在注册时已经确定,且每个层在同一时间只有一个活动状态。 因此,我们按照层索引来注册和调用。 步骤详细代码: 1. 创建桥梁脚本:AnimationEventDispatcher ```csharp using System.Collections.Generic; using UnityEngine; public class AnimationEventDispatcher : MonoBehaviour { // 存储每个层当前活动的StateMachineBehaviour(实现了IAnimationEventHandler接口) private Dictionary<int, IAnimationEventHandler> activeHandlers = new Dictionary<int, IAnimationEventHandler>(); // 注册一个层的处理程序 public void RegisterHandler(int layerIndex, IAnimationEventHandler handler) { activeHandlers[layerIndex] = handler; } // 注销一个层的处理程序 public void UnregisterHandler(int layerIndex) { activeHandlers.Remove(layerIndex); } // 动画事件调用的方法 public void InvokeForLayer(int layerIndex, string eventName, float floatParam, int intParam, string stringParam) { if (activeHandlers.TryGetValue(layerIndex, out var handler)) { handler.OnAnimationEvent(eventName, floatParam, intParam, stringParam); } } } ``` 2. 创建接口:IAnimationEventHandler ```csharp public interface IAnimationEventHandler { void OnAnimationEvent(string eventName, float floatParam = 0, int intParam = 0, string stringParam = null); } ``` 3. 在自定义的StateMachineBehaviour中实现接口,并注册和注销: ```csharp using UnityEngine; public class CustomStateBehaviour : StateMachineBehaviour, IAnimationEventHandler { private AnimationEventDispatcher dispatcher; override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // 获取或添加AnimationEventDispatcher组件 if (dispatcher == null) { dispatcher = animator.GetComponent<AnimationEventDispatcher>(); if (dispatcher == null) dispatcher = animator.gameObject.AddComponent<AnimationEventDispatcher>(); } // 注册自己 dispatcher.RegisterHandler(layerIndex, this); } override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (dispatcher != null) { dispatcher.UnregisterHandler(layerIndex); } } // 实现接口 public void OnAnimationEvent(string eventName, float floatParam = 0, int intParam = 0, string stringParam = null) { // 在这里处理事件,根据事件名做不同的事情 Debug.Log($"Received event: {eventName} in state behaviour"); // 可以添加具体逻辑 } } ``` 4. 在动画剪辑中添加动画事件: - 在动画时间线上选择要添加事件的位置。 - 点击"Add Event"按钮。 - 在事件函数中,选择AnimationEventDispatcher组件的InvokeForLayer方法。 - 设置参数,例如: - layerIndex: 该动画所在的层索引(整数,比如0) - eventName: 自定义事件名称(字符串,比如"AttackHit") - floatParam, intParam, stringParam: 根据需要传递。 注意:这种方法要求每个要处理动画事件的状态机行为都必须实现IAnimationEventHandler接口,并在进入和退出时注册和注销。 优点: - 避免了反射,性能较好。 - 通过接口强制实现了事件处理方法。 缺点: - 需要为每个层单独注册,并且一个层只能有一个处理程序(因为每个层同一时间只有一个活动状态)。 - 需要添加一个额外的组件(AnimationEventDispatcher)。 另一种更轻量的方法(如果每个状态机行为只需要处理自己的动画事件,并且游戏对象上只有一个Animator): 我们可以让每个状态机行为在OnStateEnter时,将自己设置为游戏对象上某个全局变量(比如一个静态类中)的当前处理程序,但这样无法处理多个层。 考虑到通用性,我们推荐上述方法。 因此,总结一下,在Unity中为StateMachineBehaviour添加动画事件的方法如下: 1. 创建动画事件接口和事件分发器组件。 2. 让StateMachineBehaviour实现该接口,并在进入状态时注册,退出状态时注销。 3. 在动画事件中调用事件分发器的方法,并传递层索引和事件参数。 4. 在StateMachineBehaviour的接口方法中处理事件。 这样,我们就实现了在StateMachineBehaviour中处理动画事件。 注意:如果同一个层有多个状态机行为(例如通过子状态机),那么需要确保注册的是当前最上层的状态。但通常一个层只有一个状态是活动的(不考虑过渡期),所以上述方法是可行的。 引用说明: 引用[1]告诉我们如何添加StateMachineBehaviour状态,引用[3]提到了在OnStateEnter和OnStateExit中管理生命周期,这与我们注册和注销处理程序的思想一致。 </think>### Unity中为StateMachineBehaviour添加动画事件的方法 在Unity中为`StateMachineBehaviour`添加动画事件,需要通过**动画事件桥接机制**实现。由于动画事件(Animation Events)只能直接调用游戏对象(GameObject)上的方法,而`StateMachineBehaviour`是附加在状态上的组件,我们需要通过游戏对象作为中间层来传递事件。以下是详细实现步骤: #### 1. **创建事件桥接组件** 在Animator所在的游戏对象上添加一个桥接脚本,用于接收动画事件并转发给当前活动的`StateMachineBehaviour`: ```csharp using UnityEngine; using System.Collections.Generic; public class AnimationEventBridge : MonoBehaviour { private Dictionary<int, StateMachineBehaviour> activeBehaviours = new Dictionary<int, StateMachineBehaviour>(); // 注册当前层的状态机行为 public void RegisterBehaviour(int layerIndex, StateMachineBehaviour behaviour) { activeBehaviours[layerIndex] = behaviour; } // 解除注册 public void UnregisterBehaviour(int layerIndex) { activeBehaviours.Remove(layerIndex); } // 处理动画事件 (由动画事件调用) public void HandleAnimationEvent(string eventName) { foreach (var behaviour in activeBehaviours.Values) { if (behaviour is IAnimationEventHandler handler) { handler.OnAnimationEvent(eventName); } } } } ``` #### 2. **创建事件处理接口** 定义接口规范状态机行为的事件处理能力: ```csharp public interface IAnimationEventHandler { void OnAnimationEvent(string eventName); } ``` #### 3. **实现StateMachineBehaviour** 在自定义状态机行为中实现事件处理接口,并在生命周期方法中注册/注销: ```csharp using UnityEngine; public class CustomStateBehaviour : StateMachineBehaviour, IAnimationEventHandler { private AnimationEventBridge eventBridge; override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // 获取或创建事件桥接器 eventBridge = animator.GetComponent<AnimationEventBridge>(); if (eventBridge == null) eventBridge = animator.gameObject.AddComponent<AnimationEventBridge>(); // 注册当前行为 eventBridge.RegisterBehaviour(layerIndex, this); } override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // 注销当前行为 if (eventBridge != null) eventBridge.UnregisterBehaviour(layerIndex); } // 实现事件处理接口 public void OnAnimationEvent(string eventName) { Debug.Log($"Received animation event: {eventName}"); // 根据事件名执行不同逻辑 switch (eventName) { case "AttackStart": // 攻击开始逻辑 break; case "Footstep": // 脚步声逻辑 break; } } } ``` #### 4. **在动画编辑器中添加事件** 1. 打开动画窗口 (Window > Animation > Animation) 2. 选择目标动画片段 3. 在时间线上定位需要添加事件的位置 4. 点击"Add Event"按钮 5. 在检视面板中设置: - **Function**: 选择 `AnimationEventBridge.HandleAnimationEvent` - **String Parameter**: 输入事件名称(如 "AttackStart") #### 5. **工作流程说明** 1. **事件触发**:动画播放到指定时间点时触发事件 2. **桥接转发**:`AnimationEventBridge`接收Unity原生事件 3. **行为处理**:桥接器调用当前活动状态的`OnAnimationEvent`方法 4. **状态响应**:状态机行为执行自定义逻辑 #### 注意事项 1. **多层动画**:桥接器支持多层动画,每层独立处理事件 2. **性能优化**:避免在事件中执行重负载操作 3. **事件命名**:使用清晰的事件名(如 "JumpPeak"、"LandImpact") 4. **参数传递**:可通过扩展接口支持更多参数类型(如float/int) ```csharp // 扩展接口示例 public interface IAnimationEventHandlerEx { void OnAnimationEvent(string eventName, float floatParam); } ``` > 此方法通过桥接模式解决了动画事件与状态机行为的通信问题,既符合Unity事件系统规范,又能保持状态逻辑的封装性[^1][^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thinbug

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值