今回もエディタ拡張のお話。
カスタムアトリビュートを駆使して Animator
のパラメーターを取得しますよ。
この記事にはUnity2020.3.20f1を使用しています。
.Netのバージョン設定には.Net4.x を使用しています。
文字入力でパラメータ名を指定したくない!
Animator
は便利なものです。
パラメーターを駆使すれば、簡単にアニメーションを切り替れますからね。
こんな感じでサクッと作れます。
// 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
しかし……、パラメーター名をテキスト直書きって危ないなぁ。
パラメーター名を間違えて入力する恐れが高すぎるー!
実際のパラメータを選択できるようにしたいものです。
ではそんなアトリビュートを作りましょうか。
アトリビュート上から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がないよ"); } } }
これで断然使いやすくなりましたね。
よかったよかった。
ScriptableObject
のほうも確認しておきましょうか。
あれ、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]; } }
ようやく上手くいきましたね。
これで 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