【Unity】【エディタ拡張】AnimatorControllerの現在のステート名を取得したりステートの切り替わりを監視したりする仕組みを作る

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がアタッチされ初期化されるようになりました。

f:id:halya_11:20200207191226p:plain

使う

前節の通りセットアップは自動的に行われるので、あとは下記のようにして使うだけです。

using UnityEngine;

public class Example: MonoBehaviour
{
    private void Awake()
    {
        var animator = GetComponent<Animator>();
        var animatorStateEvent = AnimatorStateEvent.Get(animator, 0);
        
        // 現在のステート
        animatorStateEvent.CurrentStateName;

        // ステートが変わった時のコールバック
        animatorStateEvent.stateEntered += _ => {}
    }
}

関連

light11.hatenadiary.com