Unityのエディタ拡張で、AnimatorControllerの現在のステート名を取得したりステートの切り替わりを監視したりする方法をまとめました。
Unity2019.3.0
やりたいこと
UnityのAnimatorで「今どのステートにいるか」をチェックするには以下のように書く必要があります。
Animator animator; animator.GetCurrentAnimatorStateInfo(0).IsName("Move");
この方法には事前にステート名を知っておく必要があったり、すべてのステート名の配列を得られなかったりという欠点があります。
また今のステート名を得るのに全ステート名をIsName()でチェックする必要があったりと不便です。
そこでこの記事ではこの欠点を解消してAnimatorを使いやすくしてみます。
全ステート名を取得する
まず全ステート名を取得する必要があります。
これはランタイムでは行えないので、エディタ上から取得します。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor.Animations; public static class AnimatorEditorUtility { /// <summary> /// 全てのステートとそのフルパスを取得する /// </summary> private static void GetAllStatesAndFullPaths(AnimatorStateMachine stateMachine, string parentPath, List<(AnimatorState state, string fullPath)> result) { if (!string.IsNullOrEmpty(parentPath)) { parentPath += "."; } parentPath += stateMachine.name; // 全てのステートを処理 foreach (var state in stateMachine.states) { var stateFullPath = $"{parentPath}.{state.state.name}"; result.Add((state.state, stateFullPath)); } // サブステートマシンを再帰的に処理 foreach (var subStateMachine in stateMachine.stateMachines) { GetAllStatesAndFullPaths(subStateMachine.stateMachine, parentPath, result); } } }
これを以下のように使うとすべてのステートとそのフルパスを得られます。
AnimatorController ac; var result = new List<(AnimatorState state, string fullPath)>(); GetAllStatesAndFullPaths(ac.layers[0].stateMachine, null, result);
ステート名をシリアライズしておくためのStateMachineBehaviour
前節でエディタ上からは全ステートを取得できるようになりました。
ただランタイムで使うことが目的なので、これをStateMachineBehaviourにシリアライズして使うことにします。
using UnityEngine; using System.Linq; public class AnimatorStateEvent : StateMachineBehaviour { [SerializeField] private int _layer; public int Layer => _layer; [SerializeField] private string[] _stateFullPaths; public string[] StateFullPaths => _stateFullPaths; /// <summary> /// 今のステート名 /// </summary> public string CurrentStateName { get; private set; } /// <summary> /// 今のステートのフルパス /// </summary> public string CurrentStateFullPath { get; private set; } /// <summary> /// ステートが変わった時のコールバック /// </summary> public event System.Action<(string stateName, string stateFullPath)> stateEntered; private int[] _stateFullPathHashes; private int[] StateFullPathHashes { get{ if (_stateFullPathHashes == null) { _stateFullPathHashes = _stateFullPaths .Select(x => Animator.StringToHash(x)) .ToArray(); } return _stateFullPathHashes; } } /// <summary> /// 取得する /// </summary> public static AnimatorStateEvent Get(Animator animator, int layer) { return animator.GetBehaviours<AnimatorStateEvent>().First(x => x.Layer == layer); } public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { base.OnStateEnter(animator, stateInfo, layerIndex); for (int i = 0; i < StateFullPathHashes.Length; i++) { var stateFullPathHash = StateFullPathHashes[i]; if (stateInfo.fullPathHash == stateFullPathHash) { CurrentStateFullPath = _stateFullPaths[i]; CurrentStateName = CurrentStateFullPath.Split('.').Last(); stateEntered?.Invoke((CurrentStateName, CurrentStateFullPath)); return; } } throw new System.Exception(); } }
_stateFullPathsにシリアライズしておくことで、現在のステート名や、ステート変化時のコールバックが得られます。
AnimatorStateEventのセットアップを自動化
さて最後に前節のAnimatorStateEventを自動的にセットアップされるようにします。
Selection.selectionChanged
を使ってAnimatorControllerやそのサブアセット(LayerやState)の選択が解除されたときに
そのAnimatorControllerのレイヤーにAnimatorStateEventをアタッチしてセットアップします。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor.Animations; using UnityEditor; using System.Linq; public static class AnimatorEditorUtility { private static string _selectedObjectPath = null; /// <summary> /// 選択が外れた時にAnimatorStateEventを更新する /// </summary> [InitializeOnLoadMethod] private static void SetupAnimatorStateEventOnDeselect() { // 選択が外れた時に更新 Selection.selectionChanged += () => { if (!string.IsNullOrEmpty(_selectedObjectPath) && _selectedObjectPath.EndsWith(".controller")) { var animatorController = AssetDatabase.LoadAssetAtPath<AnimatorController>(_selectedObjectPath); SetupAnimatorStateEvent(animatorController); } _selectedObjectPath = AssetDatabase.GetAssetPath(Selection.activeObject); }; } /// <summary> /// AnimatorStateEventをセットアップする /// </summary> public static void SetupAnimatorStateEvent(AnimatorController animatorController) { for (int i = 0; i < animatorController.layers.Length; i++) { var layer = animatorController.layers[i]; var rootStateMachine = layer.stateMachine; // レイヤーにStateMachineBehaviourをアタッチする var animatorStateEvent = layer.stateMachine.behaviours.FirstOrDefault(x => x is AnimatorStateEvent); if (animatorStateEvent == null) { animatorStateEvent = layer.stateMachine.AddStateMachineBehaviour<AnimatorStateEvent>(); } var so = new SerializedObject(animatorStateEvent); so.Update(); so.FindProperty("_layer").intValue = i; var statesProperty = so.FindProperty("_stateFullPaths"); // サブステートマシンを含めた全てのステートマシンを取得 var allStatesAndFullPaths = new List<(AnimatorState state, string fullPath)>(); GetAllStatesAndFullPaths(rootStateMachine, null, allStatesAndFullPaths); // ステートのフルパスを格納する statesProperty.arraySize = allStatesAndFullPaths.Count; for (int j = 0; j < statesProperty.arraySize; j++) { statesProperty.GetArrayElementAtIndex(j).stringValue = allStatesAndFullPaths[j].fullPath; } so.ApplyModifiedProperties(); so.Dispose(); } } }
ここまでで、AnimatorControllerのレイヤーに自動的にAnimatorStateEventがアタッチされ初期化されるようになりました。
使う
前節の通りセットアップは自動的に行われるので、あとは下記のようにして使うだけです。
using UnityEngine; public class Example: MonoBehaviour { private void Awake() { var animator = GetComponent<Animator>(); var animatorStateEvent = AnimatorStateEvent.Get(animator, 0); // 現在のステート animatorStateEvent.CurrentStateName; // ステートが変わった時のコールバック animatorStateEvent.stateEntered += _ => {} } }