徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】Animatorのパラメータリストが欲しいのさ

今回もエディタ拡張のお話。
カスタムアトリビュートを駆使して Animator のパラメーターを取得しますよ。
f:id:urahimono:20211020202359p:plain


この記事にはUnity2020.3.20f1を使用しています。
.Netのバージョン設定には.Net4.x を使用しています。

文字入力でパラメータ名を指定したくない!

Animator は便利なものです。
パラメーターを駆使すれば、簡単にアニメーションを切り替れますからね。
こんな感じでサクッと作れます。

f:id:urahimono:20211020202412j:plain

// GameManager.cs
using UnityEngine;
public class GameManager : MonoBehaviour
{
    private Animator m_animator = default;

    private void Awake()
    {
        m_animator = GetComponentInChildren<Animator>();
    }

    private void Start()
    {
        m_animator.SetTrigger("Jump");
    }
} // class GameManager

しかし、パラメーター名を引数に直にかくってどうよ?
複数同じパラメーターを指定する箇所があることを考えるとちょっと良くないよね。
定数にしときましょうか。

// GameManager.cs
using UnityEngine;
public class GameManager : MonoBehaviour
{
    private Animator m_animator = default;
    private readonly string locJumpTrigger = "Jump";

    private void Awake()
    {
        m_animator = GetComponentInChildren<Animator>();
    }

    private void Start()
    {
        m_animator.SetTrigger(locJumpTrigger);
    }
} // class GameManager

うーむ、これもまだまだだなぁ。
そもそも Animator のパラメーターはリソースなわけですから、コード上に記述しちゃうのはよくないんじゃない。
エディタ上で指定したほうが良いと思うの。

// GameManager.cs
using UnityEngine;
public class GameManager : MonoBehaviour
{
    [SerializeField]
    private string m_jumpTriggerName = default;
    private Animator m_animator = default;

    private void Awake()
    {
        m_animator = GetComponentInChildren<Animator>();
    }

    private void Start()
    {
        m_animator.SetTrigger(m_jumpTriggerName);
    }
} // class GameManager

f:id:urahimono:20211020202426j:plain

しかし……、パラメーター名をテキスト直書きって危ないなぁ。
パラメーター名を間違えて入力する恐れが高すぎるー!
実際のパラメータを選択できるようにしたいものです。
f:id:urahimono:20211020202437j:plain

ではそんなアトリビュートを作りましょうか。

アトリビュート上からAnimatorを探す

ではまずアトリビュートのベース部分を作ってと。

// AnimatorParametersAttribute.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class AnimatorParametersAttribute : PropertyAttribute
{

} // class AnimatorParametersAttribute

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(AnimatorParametersAttribute))]
public class AnimatorParametersDrawer : PropertyDrawer
{
    public override void OnGUI(
        Rect aPosition,
        SerializedProperty aProperty,
        GUIContent aLabel)
    {
        EditorGUI.PropertyField(aPosition, aProperty, aLabel, true);
    }

    public override float GetPropertyHeight(
        SerializedProperty aProperty, 
        GUIContent aLabel)
    {
        return EditorGUI.GetPropertyHeight(aProperty, true);
    }
} // class AnimatorParametersDrawer
#endif

何はともあれ Animator を持ってこないことには話にならない。
でも、この AnimatorParametersAttribute をつける対象となる変数の型は string
さて、どうしたものか。

SerializedProperty 経由で、シリアライズしているオブジェクト情報が取得できるはず。
そこから GetComponent()Animator が持ってこれるかも。
試してみよう。

// AnimatorParametersAttribute.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(AnimatorParametersAttribute))]
public class AnimatorParametersDrawer : PropertyDrawer
{
    public override void OnGUI(
        Rect aPosition,
        SerializedProperty aProperty,
        GUIContent aLabel)
    {
        Animator animator = GetAnimator(aProperty);
        if (animator)
        {
            // animator.parameters を使ってパラメータ一覧を取得。
            EditorGUI.PropertyField(aPosition, aProperty, aLabel, true);
        }
        else
        {
            EditorGUI.LabelField(aPosition, aLabel.text, "Animatorがないよ");
        }
    }

    private Animator GetAnimator(SerializedProperty aProperty)
    {
        // シリアライズしたオブジェクトが
        // MonoBehaviour など Component を継承したもの前提。
        Object target = aProperty.serializedObject.targetObject;
        Component component = target as Component;
        if (component != null)
            return component.GetComponentInChildren<Animator>();
        return null;
    }
} // class AnimatorParametersDrawer
#endif

OK!
これなら取得できそうだ。
でもこれには問題がある。
今回は MonoBehaviour を継承した自作の GameManager がこのアトリビュートを使っているからこの手段は使える。
でも、普通のクラスだったり、 ScriptableObject などの場合はこの方法では取得できない。
こんな使い方の時ですね。

[CreateAssetMenu(menuName = "MyScriptableObject", fileName = "MyScriptableObject")]
public class MyScriptableObject : ScriptableObject
{
    // ScriptableObject にコンポーネントは
    // アタッチされていないの取得できない。
    [SerializeField, AnimatorParameters]
    private string m_triggerName = default;
} // class MyScriptableObject

[System.Serializable]
public class SubClass
{
    // m_animator を取ってきたいけど現状できない。
    [SerializeField, AnimatorParameters]
    private string   m_triggerName = default;
    [SerializeField]
    private Animator m_animator    = default;
}

これではちょっと限定的すぎますな。
もっと汎用的なものを作りたいですね。

先日、 FindProperty() を駆使することでクラス内の別の変数にアクセスする道があることがわかりました。
この処理を参考に少し改良していきましょう。
www.urablog.xyz

// AnimatorParametersAttribute.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class AnimatorParametersAttribute : PropertyAttribute
{
    public AnimatorParametersAttribute()
        : this(string.Empty) { }
    public AnimatorParametersAttribute(string aVariableName)
    {
        variableName = aVariableName;
    }
    public string variableName { get; private set; } = string.Empty;

} // class AnimatorParametersAttribute

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(AnimatorParametersAttribute))]
public class AnimatorParametersDrawer : PropertyDrawer
{
    private new AnimatorParametersAttribute attribute => base.attribute as AnimatorParametersAttribute;

    public override void OnGUI(
        Rect aPosition,
        SerializedProperty aProperty,
        GUIContent aLabel)
    {
        Animator animator = GetAnimator(aProperty);
        if (animator)
        {
            // animator.parameters を使ってパラメータ一覧を取得。
            EditorGUI.PropertyField(aPosition, aProperty, aLabel, true);
        }
        else
        {
            EditorGUI.LabelField(aPosition, aLabel.text, "Animatorがないよ");
        }
    }

    private Animator GetAnimator(SerializedProperty aProperty)
    {
        // GetComponent() から Animator を取得する場合.
        if (string.IsNullOrEmpty(attribute.variableName))
        {
            Object target = aProperty.serializedObject.targetObject;
            Component component = target as Component;
            if (component != null)
                return component.GetComponentInChildren<Animator>();
        }
        // 変数名から Animator を取得する場合.
        else
        {
            SerializedProperty variableProperty = GetProperty(aProperty, attribute.variableName);
            if (variableProperty == null)
                return null;

            // objValue が nullの場合は int などの
            // まったく Animator と関係ないものを指している場合.
            Object objValue = variableProperty.objectReferenceValue;
            if (objValue == null)
                return null;

            if (objValue is Animator)
                return (Animator)objValue;

            GameObject gameObject = objValue as GameObject;
            if (gameObject != null)
                return gameObject.GetComponentInChildren<Animator>();

            Component component = objValue as Component;
            if (component != null)
                return component.GetComponentInChildren<Animator>();
        }

        return null;
    }

    private SerializedProperty GetProperty(
        SerializedProperty aBaseProperty, string aVariableName)
    {
        string path       = aVariableName;
        string fullPath   = aBaseProperty.propertyPath;
        string parentPath = fullPath;

        if (aBaseProperty.isArray && fullPath.EndsWith("]"))
        {
            int dotIndex = parentPath.LastIndexOf(".Array.data");
            if (dotIndex >= 0)
                parentPath = parentPath.Substring(0, dotIndex);
            int parentIndex = parentPath.LastIndexOf(".");
            if (parentIndex < 0)
                parentIndex = 0;
            parentPath = parentPath.Substring(0, parentIndex);
        }
        else
        {
            int nameIndex = parentPath.LastIndexOf(aBaseProperty.name);
            parentPath    = parentPath.Substring(0, nameIndex);
            int parentIndex = parentPath.LastIndexOf(".");
            if (parentIndex >= 0)
                parentPath = parentPath.Substring(0, parentIndex);
        }

        if (!string.IsNullOrEmpty(parentPath))
            path = $"{parentPath}.{path}";

        var serializedObject = aBaseProperty.serializedObject;
        return serializedObject.FindProperty(path);
    }
} // class AnimatorParametersDrawer
#endif

そして改良した AnimatorParametersAttribute を使えば、
先ほど問題になったクラスでも使えるようになりましたよ。

// MyScriptableObject.cs
using UnityEngine;

[System.Serializable]
public class SubClass
{
    [SerializeField, AnimatorParameters("m_animator")]
    private string   m_triggerName = default;
    [SerializeField]
    private Animator m_animator = default;
}

[CreateAssetMenu(menuName = "MyScriptableObject", fileName = "MyScriptableObject")]
public class MyScriptableObject : ScriptableObject
{
    [SerializeField, AnimatorParameters("m_prefab")]
    private string m_triggerName = default;
    [SerializeField]
    private GameObject m_prefab  = default;
    [SerializeField]
    private SubClass m_subClass  = default;
} // class MyScriptableObject

パラメーターリストを持ってこよう

Animator を手に入れればもう楽勝です。
parameters のプロパティからパラメーターのリストを取得できますからね。
docs.unity3d.com

あとは EditorGUI.Popup() を使ってポップアップを表示すれば完成です!

// AnimatorParametersAttribute.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
using System.Linq;
[CustomPropertyDrawer(typeof(AnimatorParametersAttribute))]
public class AnimatorParametersDrawer : PropertyDrawer
{
    private new AnimatorParametersAttribute attribute => base.attribute as AnimatorParametersAttribute;

    public override void OnGUI(
        Rect aPosition,
        SerializedProperty aProperty,
        GUIContent aLabel)
    {
        Animator animator = GetAnimator(aProperty);
        if (animator)
        {
            string[] parameters = animator.parameters.Select(param => param.name).ToArray();
            if (parameters.Length == 0)
            {
                EditorGUI.LabelField(aPosition, aLabel.text, "Animator に parameter が無いよ!");
                return;
            }

            int index = System.Array.FindIndex(parameters, (name) => name == aProperty.stringValue);
            index = Mathf.Clamp(index, 0, parameters.Length);

            int selectedIndex = EditorGUI.Popup(aPosition, aLabel.text, index, parameters);
            aProperty.stringValue = parameters[selectedIndex];
        }
        else
        {
            EditorGUI.LabelField(aPosition, aLabel.text, "Animatorがないよ");
        }
    }
}

f:id:urahimono:20211020202449j:plain

これで断然使いやすくなりましたね。
よかったよかった。
ScriptableObject のほうも確認しておきましょうか。
f:id:urahimono:20211020202458j:plain

あれ、parameters が取得できてない。

使用している Prefab は同じものを使っていので、データ側の不具合ではない。
どういうことだ。
どうやら、まだこの戦いは続くみたいだね、

Editor なら AnimatorController

Animator から parameters を取得できていたのは、
Hierarchy 上にある GameObject のコンポーネントだった。
うーむ、実体化していないと取ってこれないというのか!

だとすると困ったなぁ。
そもそもパラメータを持っているのは Animator が持っている RuntimeAnimatorController が持っているんだ。
ここから持ってこれればいいんだけど、 RuntimeAnimatorController にそんなプロパティは無い。

そう、ここで重要なのは Runtime と書かれている点だ。
ということは、 Runtime でないものも存在するということだろうか?

それはズバリだった。
UnityEditor.Animations の名前空間に存在するのさ。
AnimatorController という エディタ専用のクラスがね。
docs.unity3d.com

これは先ほどの RuntimeAnimatorController を継承している。
すなわちエディタ中なら変換可能ということさ。
これには parameters のプロパティを持っているで、パラメーターリストが取得できるぞ。
試してみよう。

// AnimatorParametersAttribute.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Animations;
#endif
using System.Linq;
[CustomPropertyDrawer(typeof(AnimatorParametersAttribute))]
public class AnimatorParametersDrawer : PropertyDrawer
{
    private new AnimatorParametersAttribute attribute => base.attribute as AnimatorParametersAttribute;

    public override void OnGUI(
        Rect aPosition,
        SerializedProperty aProperty,
        GUIContent aLabel)
    {
        Animator animator = GetAnimator(aProperty);
        if (animator == null)
        {
            EditorGUI.LabelField(aPosition, aLabel.text, "Animatorがないよ");
            return;
        }

        AnimatorController controller = animator.runtimeAnimatorController as AnimatorController;
        if (controller == null)
        {
            EditorGUI.LabelField(aPosition, aLabel.text, "AnimatorControllerがないよ");
            return;
        }

        string[] parameters = controller.parameters.Select(param => param.name).ToArray();
        if (parameters.Length == 0)
        {
            EditorGUI.LabelField(aPosition, aLabel.text, "Animator に parameter が無いよ!");
            return;
        }

        int index = System.Array.FindIndex(parameters, (name) => name == aProperty.stringValue);
        index = Mathf.Clamp(index, 0, parameters.Length);

        int selectedIndex = EditorGUI.Popup(aPosition, aLabel.text, index, parameters);
        aProperty.stringValue = parameters[selectedIndex];
    }
}

f:id:urahimono:20211020202520j:plain

ようやく上手くいきましたね。
これで Animator のパラメーターが使いやすくなるかな。
ついでに RuntimeAnimatorController を直接変数に設定した場合も追加した完成形がこちらになります。

// AnimatorParametersAttribute.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Animations;
#endif
using System.Linq;
public class AnimatorParametersAttribute : PropertyAttribute
{
    public AnimatorParametersAttribute()
    : this(string.Empty) { }
    public AnimatorParametersAttribute(string aVariableName)
    {
        variableName = aVariableName;
    }
    public string variableName { get; private set; } = string.Empty;
} // class AnimatorParametersAttribute

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(AnimatorParametersAttribute))]
public class AnimatorParametersDrawer : PropertyDrawer
{
    private new AnimatorParametersAttribute attribute => base.attribute as AnimatorParametersAttribute;

    public override void OnGUI(
        Rect aPosition,
        SerializedProperty aProperty,
        GUIContent aLabel)
    {
        AnimatorController controller = GetAnimatorController(aProperty);
        if (controller == null)
        {
            EditorGUI.LabelField(aPosition, aLabel.text, "AnimatorController 無いよ!");
            return;
        }

        string[] parameters = controller.parameters.Select(param => param.name).ToArray();
        if (parameters.Length == 0)
        {
            EditorGUI.LabelField(aPosition, aLabel.text, "Animator に parameter が無いよ!");
            return;
        }

        int index = System.Array.FindIndex(parameters, (name) => name == aProperty.stringValue);
        index     = Mathf.Clamp(index, 0, parameters.Length);

        int selectedIndex     = EditorGUI.Popup(aPosition, aLabel.text, index, parameters);
        aProperty.stringValue = parameters[selectedIndex];
    }

    private AnimatorController GetAnimatorController(SerializedProperty aProperty)
    {
        // GetComponent() から Animator を取得する場合.
        if (string.IsNullOrEmpty(attribute.variableName))
        {
            Object target       = aProperty.serializedObject.targetObject;
            Animator animator = GetAnimator(target);
            if (animator != null)
                return animator.runtimeAnimatorController as AnimatorController;
        }
        // 変数名から Animator を取得する場合.
        else
        {
            SerializedProperty variableProperty = GetProperty(aProperty, attribute.variableName);
            if (variableProperty == null)
                return null;

            // objValue が nullの場合は int などの
            // まったく Animator と関係ないものを指している場合.
            Object objValue = variableProperty.objectReferenceValue;
            if (objValue == null)
                return null;

            AnimatorController controller = objValue as AnimatorController;
            if (controller != null)
                return controller;

            Animator animator = GetAnimator(objValue);
            if (animator != null)
                return animator.runtimeAnimatorController as AnimatorController;
        }

        return null;
    }

    private Animator GetAnimator(Object aReferenceValue)
    {
        if (aReferenceValue is Animator)
            return (Animator)aReferenceValue;

        GameObject gameObject = aReferenceValue as GameObject;
        if (gameObject != null)
            return gameObject.GetComponentInChildren<Animator>();

        Component component = aReferenceValue as Component;
        if (component != null)
            return component.GetComponentInChildren<Animator>();

        return null;
    }

    private SerializedProperty GetProperty(
        SerializedProperty aBaseProperty, string aVariableName)
    {
        string path       = aVariableName;
        string fullPath   = aBaseProperty.propertyPath;
        string parentPath = fullPath;

        if (aBaseProperty.isArray && fullPath.EndsWith("]"))
        {
            int dotIndex = parentPath.LastIndexOf(".Array.data");
            if (dotIndex >= 0)
                parentPath = parentPath.Substring(0, dotIndex);
            int parentIndex = parentPath.LastIndexOf(".");
            if (parentIndex < 0)
                parentIndex = 0;
            parentPath = parentPath.Substring(0, parentIndex);
        }
        else
        {
            int nameIndex = parentPath.LastIndexOf(aBaseProperty.name);
            parentPath    = parentPath.Substring(0, nameIndex);
            int parentIndex = parentPath.LastIndexOf(".");
            if (parentIndex >= 0)
                parentPath = parentPath.Substring(0, parentIndex);
        }

        if (!string.IsNullOrEmpty(parentPath))
            path = $"{parentPath}.{path}";

        var serializedObject = aBaseProperty.serializedObject;
        return serializedObject.FindProperty(path);
    }

    public override float GetPropertyHeight(
        SerializedProperty aProperty, 
        GUIContent aLabel)
    {
        return EditorGUI.GetPropertyHeight(aProperty, true);
    }
} // class AnimatorParametersDrawer
#endif