【Unity-Animator】通过 StateMachineBehaviour 实现回调

引言

StateMachineBehaviour (SMB) 是 Unity Animator 系统中的行为脚本基类,允许开发者在动画状态机的特定时机执行自定义逻辑。它提供了比传统 Animation Event 更强大的功能,支持状态进入/退出、更新、过渡等生命周期回调。SMB 特别适合实现动画同步、状态管理、音效触发、粒子控制等需求。

SMB 的优势在于解耦设计:动画逻辑与代码分离,状态机可重用;可视化编辑:在 Animator 窗口直接配置参数;继承扩展:支持多重继承和组合。适用于 Unity 2018.4+,推荐 Unity 2022 LTS。

StateMachineBehaviour 生命周期详解

核心回调方法

SMB 提供了完整的状态机生命周期钩子:

回调方法时机用途
OnStateEnter状态进入时(一次)初始化、音效播放、粒子发射
OnStateUpdate状态更新时(每帧)动画混合、动态参数调整
OnStateExit状态退出时(一次)清理资源、状态切换逻辑
OnStateMove有移动时(每帧)角色控制器同步、IK 调整
OnStateIKIK 更新时(每帧)逆运动学、手脚定位
OnStateMachineEnter状态机进入时全局初始化、层级设置

参数传递机制

  • Animator 参数:通过 Animator.GetFloat/SetFloat 等访问
  • SMB 属性:序列化字段暴露到 Inspector
  • 反射参数Animator.StringToHash 优化性能

基础实现示例

1. 基本状态进入回调

using UnityEngine;

public class AttackStateBehaviour : StateMachineBehaviour
{
    [Header("Attack Settings")]
    public float damage = 50f;
    public AudioClip attackSound;
    public ParticleSystem hitEffect;

    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 播放攻击音效
        AudioSource audioSource = animator.GetComponent<AudioSource>();
        if (audioSource && attackSound)
            audioSource.PlayOneShot(attackSound);

        // 触发粒子效果
        if (hitEffect)
            hitEffect.Play();

        // 应用伤害(通过事件通知)
        CharacterCombat combat = animator.GetComponent<CharacterCombat>();
        if (combat)
            combat.DealDamage(damage);
    }
}

2. 动画结束检测与回调

public class AnimationCompleteBehaviour : StateMachineBehaviour
{
    public string completeParameter = "AttackComplete";  // Animator 布尔参数

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 检测动画是否完成(normalizedTime >= 1)
        if (stateInfo.normalizedTime >= 1.0f && !animator.GetBool(completeParameter))
        {
            animator.SetBool(completeParameter, true);
            Debug.Log("Animation Complete!");
        }
    }

    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 重置参数
        animator.SetBool(completeParameter, false);
    }
}

高级应用场景

1. 动画事件系统(自定义回调)

创建事件委托系统,实现解耦回调:

using UnityEngine;
using System;

public class AnimationEventSystem : MonoBehaviour
{
    public static AnimationEventSystem Instance;

    // 全局事件委托
    public event Action<string, object[]> OnAnimationEvent;

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }

    public void TriggerEvent(string eventName, params object[] args)
    {
        OnAnimationEvent?.Invoke(eventName, args);
    }
}

// SMB 中触发事件
public class EventTriggerBehaviour : StateMachineBehaviour
{
    [Header("Event Settings")]
    public string eventName = "OnFootstep";
    public float eventTime = 0.5f;  // 动画时间点

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if (stateInfo.normalizedTime >= eventTime && eventTime > 0)
        {
            AnimationEventSystem.Instance?.TriggerEvent(eventName, 
                new object[] { animator.gameObject, stateInfo });
            eventTime = -1f;  // 防止重复触发
        }
    }
}

外部监听示例

public class FootstepListener : MonoBehaviour
{
    void Start()
    {
        AnimationEventSystem.Instance.OnAnimationEvent += HandleAnimationEvent;
    }

    void HandleAnimationEvent(string eventName, object[] args)
    {
        if (eventName == "OnFootstep")
        {
            GameObject target = args[0] as GameObject;
            // 播放脚步音效
            AudioSource.PlayClipAtPoint(footstepClip, target.transform.position);
        }
    }
}

2. 状态切换与条件管理

public class StateTransitionBehaviour : StateMachineBehaviour
{
    [Header("Transition Conditions")]
    public string nextStateParameter = "CanMove";
    public float minDuration = 1.0f;
    private float stateEnterTime;

    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        stateEnterTime = Time.time;
    }

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 满足条件时切换状态
        if (Time.time - stateEnterTime >= minDuration)
        {
            bool canTransition = CheckTransitionConditions(animator);
            if (canTransition)
            {
                animator.SetBool(nextStateParameter, true);
            }
        }
    }

    private bool CheckTransitionConditions(Animator animator)
    {
        // 自定义切换逻辑
        PlayerInput input = animator.GetComponent<PlayerInput>();
        return input != null && input.IsGrounded && !input.IsAttacking;
    }
}

3. IK 与移动同步

public class IKSolverBehaviour : StateMachineBehaviour
{
    [Header("IK Settings")]
    public bool enableFootIK = true;
    public float footHeight = 0.1f;
    public LayerMask groundLayer = 1;

    public override void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if (!enableFootIK) return;

        SolveFootIK(animator, stateInfo);
    }

    private void SolveFootIK(Animator animator, AnimatorStateInfo stateInfo)
    {
        animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1f);
        animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1f);

        // 左脚 IK
        RaycastHit leftHit;
        if (Physics.Raycast(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + Vector3.up * 0.5f,
            Vector3.down, out leftHit, 1f, groundLayer))
        {
            Vector3 footPos = leftHit.point + Vector3.up * footHeight;
            animator.SetIKPosition(AvatarIKGoal.LeftFoot, footPos);
        }

        // 右脚 IK(类似)
        // ...
    }
}

参数配置与优化

1. Inspector 可视化配置

使用自定义属性绘制器增强 SMB 编辑体验:

using UnityEngine;
using UnityEditor;

public class SMBPropertyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);

        // 自定义参数分组
        EditorGUILayout.PropertyField(property.FindPropertyRelative("damage"), new GUIContent("Damage Amount"));
        EditorGUILayout.PropertyField(property.FindPropertyRelative("attackSound"), new GUIContent("Sound Clip"));

        EditorGUI.EndProperty();
    }
}

2. 性能优化技巧

public class OptimizedSMB : StateMachineBehaviour
{
    private static readonly int HashDamage = Animator.StringToHash("Damage");
    private bool hasTriggered = false;

    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 使用哈希优化参数访问
        animator.SetFloat(HashDamage, damage);
        hasTriggered = false;
    }

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 避免每帧检查,使用状态标志
        if (!hasTriggered && stateInfo.normalizedTime > 0.2f)
        {
            TriggerEffect(animator);
            hasTriggered = true;
        }
    }
}

3. 多状态共享逻辑

使用基类封装公共功能:

public abstract class BaseCombatBehaviour : StateMachineBehaviour
{
    [Header("Shared Combat Settings")]
    public float baseDamage = 10f;
    public AudioClip[] hitSounds;

    protected CharacterCombat GetCombatComponent(Animator animator)
    {
        return animator.GetComponent<CharacterCombat>();
    }

    protected void PlayRandomHitSound(Animator animator)
    {
        if (hitSounds.Length > 0)
        {
            AudioClip clip = hitSounds[Random.Range(0, hitSounds.Length)];
            AudioSource.PlayClipAtPoint(clip, animator.transform.position);
        }
    }
}

public class LightAttackBehaviour : BaseCombatBehaviour
{
    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        baseDamage *= 0.8f;  // 轻攻击伤害
        GetCombatComponent(animator)?.DealDamage(baseDamage);
        PlayRandomHitSound(animator);
    }
}

调试与测试

1. SMB 调试工具

public class SMBDebugger : StateMachineBehaviour
{
#if UNITY_EDITOR
    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        Debug.Log($"[{GetType().Name}] Entered state: {stateInfo.shortNameHash} at time: {Time.time}");
    }

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if (Input.GetKeyDown(KeyCode.F1))  // 调试热键
        {
            Debug.Log($"State Info - NormalizedTime: {stateInfo.normalizedTime}, Length: {stateInfo.length}");
        }
    }
#endif
}

2. 动画状态可视化

在 Animator 窗口:

  1. 选中状态InspectorBehaviours 列表查看 SMB
  2. 右键状态Debug 模式观察参数变化
  3. Animation Window 配合使用,精确时间点测试

常见问题与解决方案

问题原因解决方案
SMB 不触发状态未激活/层级错误检查 Animator 层权重、状态机连接
参数不同步并发修改使用 Animator.SetTrigger 而非布尔
性能问题OnStateUpdate 每帧执行添加条件判断,减少计算
IK 不生效Avatar 配置错误检查 Rig → Humanoid 设置
多层冲突层级遮罩问题调整 Layer Mask,设置正确权重

最佳实践总结

  1. 单一职责:每个 SMB 专注一个功能,避免复杂逻辑
  2. 参数化配置:大量使用序列化字段,支持运行时调整
  3. 事件驱动:通过委托/事件系统实现组件间通信
  4. 性能意识:避免 OnStateUpdate 中的重计算,使用缓存
  5. 版本兼容:测试不同 Unity 版本的行为差异
  6. 文档化:在 SMB 中添加详细注释和使用说明

完整工作流示例

// 1. 创建 SMB → 2. 配置参数 → 3. 拖拽到状态 → 4. 测试回调
// Animator 状态:Idle → Attack (SMB: AttackBehaviour) → Recovery (SMB: RecoveryBehaviour)

通过 StateMachineBehaviour,开发者可以构建高度模块化、可重用的动画系统,大幅提升开发效率和维护性。结合 Animator 参数和事件系统,能实现复杂的动画状态管理!

如需特定动画效果的 SMB 实现或调试帮助,请提供更多细节!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注