【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 调整 |
OnStateIK | IK 更新时(每帧) | 逆运动学、手脚定位 |
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 窗口:
- 选中状态 → Inspector → Behaviours 列表查看 SMB
- 右键状态 → Debug 模式观察参数变化
- Animation Window 配合使用,精确时间点测试
常见问题与解决方案
问题 | 原因 | 解决方案 |
---|---|---|
SMB 不触发 | 状态未激活/层级错误 | 检查 Animator 层权重、状态机连接 |
参数不同步 | 并发修改 | 使用 Animator.SetTrigger 而非布尔 |
性能问题 | OnStateUpdate 每帧执行 | 添加条件判断,减少计算 |
IK 不生效 | Avatar 配置错误 | 检查 Rig → Humanoid 设置 |
多层冲突 | 层级遮罩问题 | 调整 Layer Mask,设置正确权重 |
最佳实践总结
- 单一职责:每个 SMB 专注一个功能,避免复杂逻辑
- 参数化配置:大量使用序列化字段,支持运行时调整
- 事件驱动:通过委托/事件系统实现组件间通信
- 性能意识:避免 OnStateUpdate 中的重计算,使用缓存
- 版本兼容:测试不同 Unity 版本的行为差异
- 文档化:在 SMB 中添加详细注释和使用说明
完整工作流示例
// 1. 创建 SMB → 2. 配置参数 → 3. 拖拽到状态 → 4. 测试回调
// Animator 状态:Idle → Attack (SMB: AttackBehaviour) → Recovery (SMB: RecoveryBehaviour)
通过 StateMachineBehaviour,开发者可以构建高度模块化、可重用的动画系统,大幅提升开发效率和维护性。结合 Animator 参数和事件系统,能实现复杂的动画状态管理!
如需特定动画效果的 SMB 实现或调试帮助,请提供更多细节!