今回はUnityエディタ拡張のお話です。
カスタムアトリビュートを作って、他の変数のプロパティを取得することに躍起になっています。
この記事にはUnity2020.3.20f1を使用しています。
.Netのバージョン設定には.Net4.x を使用しています。
カスタムアトリビュートから他のプロパティの情報を知りたい!
さて、このクラスを見てほしい。
変数だけがある簡単なコンポーネントさ。
// GameManager.cs using UnityEngine; public class GameManager : MonoBehaviour { [SerializeField] private GameObject m_object = default; [SerializeField] private int m_intValue = 0; [SerializeField] private bool m_flagValue = false; } // class GameManager
そしてアトリビュートを作ってみた。
特に何も仕事をしていないアトリビュートさ。
// TestFindAttribute.cs using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif public class TestFindAttribute : PropertyAttribute { } // class TestFindAttribute #if UNITY_EDITOR [CustomPropertyDrawer(typeof(TestFindAttribute))] public class TestFindDrawer : PropertyDrawer { public override void OnGUI( Rect aPosition, SerializedProperty aProperty, GUIContent aLabel) { Debug.Log(aProperty); } } // class TestFindDrawer #endif
この作ったアトリビュートを先ほどのコンポーネントの変数につけてあげよう。
// GameManager.cs [SerializeField] private GameObject m_object = default; [SerializeField, TestFind] private int m_intValue = 0; [SerializeField] private bool m_flagValue = false;
さて、ここからが問題だ。
僕は m_intValue
に自作したアトリビュートである TestFindAttribute
をつけている。
当然のことながら、アトリビュートの描画用クラスである TestFindDrawer
では m_intValue
のプロパティの情報は取得できるはずだ。
ではこの状態で、アトリビュートをつけていない m_object
や m_flagValue
の情報は持ってこれるだろうか?
今回はそれにチャレンジします。
どんな手を使ってでも持ってくるぞ!
えっ、 CustomEditor
を使って GameManager
ごとエディタ拡張してしまえばいいんじゃないかって?
生憎だがそれは無しだ!
今回僕は PropertyAttribute
のみで戦う!
MonoBehaviour など UnityEngine.Object を継承した者
まずはアトリビュートを描画する PropertyDrawer
を確認しておこう。
OnGUI()
が Inspector などに描画している部分だ。
ここが今回の主戦場になる。
そして肝となるのが引数の SerializedProperty
。
この子がいろいろと知っているはずだ。
尋問していこう。
今回はint型変数の m_intValue
にアトリビュートをつけているため、intValue
のプロパティを使って値を取得するのが通例だ。
propertyType
でつけている変数の型が取得できる。
ただ、これはアトリビュートをつけている変数の話だ。
今回使うものではなさそうだ。
ポイントは serializedObject
というプロパティだ。
m_intValue
のシリアライズしたオブジェクトの情報が格納されている。
今回でいうならば GameManager
のことか。
これは重要な情報だ。
ここからなら m_intValue
の所持者である GameManager
の情報を持ってこれそうだ。
serializedObject
が持つ target
は UnityEngine.Object
型。
これなら GameManager
が持ってこれるじゃないか。
試してみよう。
// TestFindAttribute.cs [CustomPropertyDrawer(typeof(TestFindAttribute))] public class TestFindDrawer : PropertyDrawer { public override void OnGUI( Rect aPosition, SerializedProperty aProperty, GUIContent aLabel) { // m_intValue の所持者である GameManager が取得できた! Object obj = aProperty.serializedObject.targetObject; GameManager gameManager = obj as GameManager; Debug.Log(gameManager.name); } } // class TestFindDrawer
GameManager
への変換が完了した。
これなら PropertyDrawer
内で GameManager
の関数などガンガン使える。
ただ、今回はGameManager
が使いたいのではなく、その中にある変数の情報をしりたいのだ。
すなわち、Objectまで変換する必要がない。
serializedObject
の状態で十分だ。
そして、serializedObject
には FindProperty()
というプロパティを探す関数が用意されている。
これで、GameManager
の情報を全て引き出すことが出来るはずだ!
// TestFindAttribute.cs public class TestFindDrawer : PropertyDrawer { public override void OnGUI( Rect aPosition, SerializedProperty aProperty, GUIContent aLabel) { // GameManager の他のプロパティにアクセス出来た! var serializedObject = aProperty.serializedObject; SerializedProperty property = serializedObject.FindProperty("m_object"); Debug.Log(property.name); } } // class TestFindDrawer
ついに目的となるアトリビュートがついていない変数の情報を取得することに成功したぞ!
ただこのままではこのアトリビュートは、"m_object"という名前の変数を探すだけのものになるので、今後使うことはないだろう。
外側からどの名前の変数を取得するかを指定できるようにしてあげて、このアトリビュートを利用性を高めよう。
// TestFindAttribute.cs using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif public class TestFindAttribute : PropertyAttribute { public TestFindAttribute(string aName) { name = aName; } public string name { get; private set; } = string.Empty; } // class TestFindAttribute #if UNITY_EDITOR [CustomPropertyDrawer(typeof(TestFindAttribute))] public class TestFindDrawer : PropertyDrawer { private new TestFindAttribute attribute => base.attribute as TestFindAttribute; public override void OnGUI( Rect aPosition, SerializedProperty aProperty, GUIContent aLabel) { // GameManager の他のプロパティにアクセス出来た! var serializedObject = aProperty.serializedObject; SerializedProperty property = serializedObject.FindProperty(attribute.name); Debug.Log(property.displayName); } } // class TestFindDrawer #endif
// GameManager.cs using UnityEngine; public class GameManager : MonoBehaviour { [SerializeField] private GameObject m_object = default; [SerializeField, TestFind("m_flagValue")] private int m_intValue = 0; [SerializeField] private bool m_flagValue = false; } // class GameManager
これでアトリビュートの引数にしていした変数名のプロパティを取得できるようになった。
この方はMonoBehaviour
を継承したクラスだけでなく、ScriptableObject
を継承したものでも可能だ。
// MyScriptableObject.cs using UnityEngine; [CreateAssetMenu(menuName = "MyScriptableObject", fileName = "MyScriptableObject")] public class MyScriptableObject : ScriptableObject { [SerializeField, TestFind("m_stringValue")] private float m_floatValue = 0.0f; [SerializeField] private string m_stringValue = string.Empty; } // class MyScriptableObject
UnityEngine.Object ではない者
さてこれでミッションは完了した。
……と言えるだろうか。
いくつかのことを知ったことで、この世界の全てが分かったと思うのは愚か者の考えだ。
この場合はどうだろうか。
System.Serializable
をつけた自前のクラスを GameManager
に持たせてみた。
// GameManager.cs using UnityEngine; [System.Serializable] public class SubClass { [SerializeField, TestFind("m_stringValue")] private float m_floatValue = 0.0f; [SerializeField] private string m_stringValue = string.Empty; } // class SubClass public class GameManager : MonoBehaviour { [SerializeField] private GameObject m_object = default; [SerializeField] private SubClass m_subClass = null; [SerializeField] private bool m_flagValue = false; } // class GameManager
この場合は TestFindDrawer
側でエラーが起きてしまう。
m_stringValue
なんてものは知らん! とね。
FindProperty()
を使って調べているのは GameManager
のプロパティさ。
GameManager
は m_stringValue
は持っていない。
持っているのは、 m_subClass
であり、その中にある。
この TestFindDrawer
はまだ不完全なのさ。
ではどうすれば良いのだろう?
ここで使うのが SerializedProperty
の propertyPath
さ。
名前が表している通り、変数のパスが書かれている。
今回 TestFindAttributeは
SubClassの
m_floatValueについている。
そして
GameManagerが
SubClassを持っている。
この場合
propertyPath` には m_subClass.m_floatValue と書かれているんだ。
親の変数名を取得することが可能になるんだ。
これさえ分かればこっちのもの。
文字列操作で親の変数名がある場合の対応を追加してみよう。
// TestFindAttribute.cs [CustomPropertyDrawer(typeof(TestFindAttribute))] public class TestFindDrawer : PropertyDrawer { private new TestFindAttribute attribute => base.attribute as TestFindAttribute; public override void OnGUI( Rect aPosition, SerializedProperty aProperty, GUIContent aLabel) { // アトリビュートの変数のクラスまでパスを辿るよ。 string path = attribute.name; string rootPath = aProperty.propertyPath; int splitIndex = rootPath.LastIndexOf("."); if (splitIndex >= 0) path = $"{rootPath.Substring(0, splitIndex)}.{path}"; var serializedObject = aProperty.serializedObject; SerializedProperty property = serializedObject.FindProperty(path); Debug.Log(property.displayName); } } // class TestFindDrawer
これなら取得できるようだ。
更に入れ子になっても大丈夫。
// GameManager.cs using UnityEngine; [System.Serializable] public class SubSubClass { [SerializeField, TestFind("m_v3Value")] private Vector2 m_v2Value = Vector2.zero; [SerializeField] private Vector3 m_v3Value = Vector3.zero; } // class SubClass [System.Serializable] public class SubClass { [SerializeField] private float m_floatValue = 0.0f; [SerializeField] private string m_stringValue = string.Empty; [SerializeField] private SubSubClass m_subsubClass = default; } // class SubClass public class GameManager : MonoBehaviour { [SerializeField] private GameObject m_object = default; [SerializeField] private SubClass m_subClass = null; [SerializeField] private bool m_flagValue = false; } // class GameManager
しかし、配列のことを考えるとさらに頭が痛くなる。
配列の場合のパスは 変数名.Array.data[] の形になるので、
単純に "." で親子判定はできない。
なおかつ string は SerializedProperty
では char の配列扱いになるので
isArray
は常時 true になる という厄介な仕様なのだ。
なので変数の親子関係で考えねばならない以下の6パターンをすべて網羅する計算式を作る必要がある。
- 変数
- 配列変数
- クラス -> 変数
- クラス -> 配列変数
- 配列クラス -> 変数
- 配列クラス -> 配列変数
もう頭が回らなくなってきたのでこんな感じで締めます。
[CustomPropertyDrawer(typeof(TestFindAttribute))] public class TestFindDrawer : PropertyDrawer { private new TestFindAttribute attribute => base.attribute as TestFindAttribute; public override void OnGUI( Rect aPosition, SerializedProperty aProperty, GUIContent aLabel) { string path = aVariableName; string fullPath = aBaseProperty.propertyPath; string parentPath = fullPath; // string は char の配列扱いになるので、isArray は常時true. // やむを得ないのでパス最後の文字列で判断する。 if (aBaseProperty.isArray && fullPath.EndsWith("]")) { int dotIndex = parentPath.LastIndexOf(".Array.data"); if (dotIndex >= 0) parentPath = parentPath.Substring(0, dotIndex); // 複数の場合は name が data になっているのでパスが取得できないので // "."以降を消すことで変数名を消す。 // "."が無い場合は親子関係が無いものとみなし全消し。 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; SerializedProperty property = serializedObject.FindProperty(path); Debug.Log(property.displayName); } } // class TestFindDrawer
絶対もっときれいなコードができると思うの……。
とりあえずこれらの情報がいつか役に立つ日が来るかもしれない。
いつの日か……。