プロジェクトの中にある、自前のライブラリを見ていたら、エディタ拡張用のAttributeクラスを見つけました。
そういえば、一時期エディタ拡張にはまっていたような気がします。
そんな自作Attributeクラスの中で、とあるプロパティが配列の何番目を調べるコードがありました。
むちゃくちゃな方法で要素番号を取得してやがる……。
作った日のことを思い出しながら、もう一度同じ過ちを繰り返す、そんなお話。
この記事にはUnity5.5.1f1を使用しています。
- Attributeを自分で拡張する
- OnGUI()が呼ばれた際、このプロパティは何番目?
- 無理やりな方法その1 Inspector上の名前で推測する。
- 無理やりな方法その2 SerializedPropertyから全力で探す
- 成果物A 配列にて各要素の表示される名前とIndexを変更するAttribute
- 成果物B 配列にて各要素の表示される名前がEnumと連動するAttribute
Attributeを自分で拡張する
Unityのエディタ拡張については以下をご参考ください。
anchan828.github.io
Unityプロジェクトの中には自作のAttribute
がいくつかあり、その自作のAttribute
の中には、変数が配列時に使用する専用ものがありました。
OnGUI()が呼ばれた際、このプロパティは何番目?
では、何もしないAttribute
とそれを使うコンポーネントを作成してみます。
Attribute
クラス TestAttribute.cs
using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif // UNITY_EDITOR public class TestAttribute : PropertyAttribute { } // class TestAttribute [CustomPropertyDrawer( typeof( TestAttribute ) )] public class TestAttributeDrawer : PropertyDrawer { public override void OnGUI( Rect i_position, SerializedProperty i_property, GUIContent i_label ) { EditorGUI.PropertyField( i_position, i_property, i_label, true ); } public override float GetPropertyHeight( SerializedProperty i_property, GUIContent i_label ) { return EditorGUI.GetPropertyHeight( i_property ); } } // class TestAttributeDrawer
TestAttribute
を使うクラス TestComponent.cs
using UnityEngine; public class TestComponent : MonoBehaviour { [SerializeField, Test] private int m_value; } // class TestComponent
これを実際にInspector上で見ると、このようになります。
特に変化は見られません。
TestAttribute
のOnGUI()
では渡されたプロパティをEditorGUI.PropertyField()
を使ってデフォルトの描画しているだけなので当然です。
では、TestComponent
のvalue
の型を配列にしてみましょう。
using UnityEngine; public class TestComponent : MonoBehaviour { [SerializeField, Test] private int[] m_value; } // class TestComponent
ちゃんと配列としてInspector上で描画されています。
ここで気になるのは、TestAttributeDrawer
のOnGUI()
は、配列の各要素に対して呼ばれているのか、それとも配列の変数に対して呼ばれているのかが気になります。
ログを仕込んで確認してみましょう。
[CustomPropertyDrawer( typeof( TestAttribute ) )] public class TestAttributeDrawer : PropertyDrawer { public override void OnGUI( Rect i_position, SerializedProperty i_property, GUIContent i_label ) { Debug.Log( i_label.text ); EditorGUI.PropertyField( i_position, i_property, i_label, true ); } public override float GetPropertyHeight( SerializedProperty i_property, GUIContent i_label ) { return EditorGUI.GetPropertyHeight( i_property ); } } // class TestAttributeDrawer
ログを見る限り、配列の各要素に対して呼ばれることを確認しました。
ではOnGUI()
が呼ばれた際のTestAttributeDrawer
は、何番目の要素を描画してようとしているのかはわかるのでしょうか。
TestAttributeDrawer
は引数として渡された値を描画するだけですから、TestAttributeDrawer
クラスそのものは知りえないでしょう。
だとすると引数として渡されたものから何か情報が得られるかもしれません。
この中で一番怪しそうなのは、SerializedProperty
です。
スクリプトリファレンスを見てみましょう。
https://docs.unity3d.com/ja/current/ScriptReference/SerializedProperty.html
あ、あれ。欲しい情報が無い。
isArray
のような、配列かどうかを判断するプロパティならあるけど、「私は配列の何番目の要素です」的なプロパティがない。
マジかよ……。
だがここで諦めるわけにはいかない!
どんな手を使ってでも要素番号を取得して見せるぜ!
無理やりな方法その1 Inspector上の名前で推測する。
調査開始
さて、配列やリストを使った際のInspector上の要素名に注目してみよう。
Element 0
Element 1
Element 2
Element 3
Element 4
そう、Inspector上で要素名はElement [要素番号]の形をとっているようだ。
ということは、ここの文字列から取得すれば、目的を達成できるではないか!
では、このElement○○の文字列はどこにあるのか……、第3引数のGUIContentが知っているようだぞ。
よし、ここから要素数を取得してみよう!
[CustomPropertyDrawer( typeof( TestAttribute ) )] public class TestAttributeDrawer : PropertyDrawer { public override void OnGUI( Rect i_position, SerializedProperty i_property, GUIContent i_label ) { int index = GetElementIndex( i_label ); if( index >= 0 ) { // 取得した要素番号を利用して、ラベルを変更してみる! i_label.text = string.Format( "NewName [{0}]", index ); } EditorGUI.PropertyField( i_position, i_property, i_label, true ); } public override float GetPropertyHeight( SerializedProperty i_property, GUIContent i_label ) { return EditorGUI.GetPropertyHeight( i_property ); } private int GetElementIndex( GUIContent i_label ) { string countText = i_label.text.Replace( "Element ", "" ); int index = 0; // "Element "の文字列を破棄して残った文字は、要素番号を表す数字だけになるはずだ! if( int.TryParse( countText, out index ) ) { return index; } // 配列の要素じゃないと、この理論は通じんわな! return -1; } } // class TestAttributeDrawer
よっしゃー!
出来たー!
この理論が正しいことが証明されましたよ!
欠点
こういう名前の変数に対して、先ほど作ったTestAttribute
を使ってみましょう。
するとどうなるか……。
public class TestComponent : MonoBehaviour { [SerializeField, Test] private int[] m_value; [SerializeField, Test] private int m_element5; } // class TestComponent
でしょうね。
文字列でやっちゃうとこんなことになるよね。
配列でもなんでもない変数でも、Elementと数字を組み合わせた変数名にされると、このTestAttribute
を使っちゃうとこのなるよね。
まあ、このAttribute
は配列以外では使わない!というルールで対応するしかないですね……。
無理やりな方法その2 SerializedPropertyから全力で探す
調査開始
いや、もっと何かあるはずだ。
リファレンスを見るだけでは見つからなかったが、もっと詳しく調べれば何かあるはずだ!
まず、ブレークポイントで止めてSerializedPropertyの中身を見てみましょう。
うーん、何か情報はないものか……。
おや、これは。
propertyPath
……。
このプロパティの中身に入っている値、"m_value.Array.data[0]"。これはもしや……。
他の要素数ではどんな結果になっているか見てみましょう。
m_value.Array.data[1]
m_value.Array.data[2]
m_value.Array.data[3]
m_value.Array.data[4]
こ、これは使えるかもしれません。
Arrayとdata[...]が配列の要素を表している感じがする。
このpropertyPath
から要素番号を取得してみましょう。
[CustomPropertyDrawer( typeof( TestAttribute ) )] public class TestAttributeDrawer : PropertyDrawer { public override void OnGUI( Rect i_position, SerializedProperty i_property, GUIContent i_label ) { int index = GetElementIndex( i_property ); if( index >= 0 ) { // 取得した要素番号を利用して、ラベルを変更してみる! i_label.text = string.Format( "NewName [{0}]", index ); } EditorGUI.PropertyField( i_position, i_property, i_label, true ); } public override float GetPropertyHeight( SerializedProperty i_property, GUIContent i_label ) { return EditorGUI.GetPropertyHeight( i_property ); } private int GetElementIndex( SerializedProperty i_property ) { string propertyPath = i_property.propertyPath; var propertys = propertyPath.Split( '.' ); // このデータが配列内の要素ならば、「aaaa.Array.data[...]」の形になるはずだ! if( propertys.Length < 3 ) { return -1; } // クラスを経由して、パスが長くなった場合でも、このデータが配列内の要素ならば、その後ろから二番目は「Array」になるはずだ! string arrayProperty = propertys[ propertys.Length - 2 ]; if( arrayProperty != "Array" ) { return -1; } // このデータが配列内の要素ならば、data[...]の形になっているはずだ! var paths = propertyPath.Split( '.' ); var lastPath = paths[ propertys.Length - 1 ]; if( !lastPath.StartsWith( "data[" ) ) { return -1; } // 数字の要素だけ抜き出すんだ! var regex = new System.Text.RegularExpressions.Regex( @"[^0-9]" ); var countText = regex.Replace( lastPath, "" ); int index = 0; if( !int.TryParse( countText, out index ) ) { return -1; } return index; } } // class TestAttributeDrawer
よっしゃー!
出来たー!
この理論が正しいことが証明されましたよ!
ラベルの文字列の時と違って、m_element5
やm_array
といった名前の変数は変換されていません。
欠点
欠点も何も、SerializedProperty.propertyPath
に対する知識が少なくて、この文字列フォーマットに対する情報が皆無に近いんですよね。
そもそも、配列なら必ずArray.data[..]で来るよなんてソース見つからなかったですからね。
そう考えると、よくわからんプロパティに対して、保証のされない理論を展開して、システム組んでいるので、信頼性にかけるコード以外のなにものでもないんですよ。
はぁ……、とりあえず動いてはいるようだからこのまま行きますが、SerializedProperty.propertyPath
の情報はもう少し調査が必要そうですね。
成果物A 配列にて各要素の表示される名前とIndexを変更するAttribute
さて、それでは今回調査した要素番号の取得を利用して作成されたAttribute
を紹介しましょう。
まず1つ目が、配列にて各要素の表示される名前とIndexを変更するAttributeです。
要素名を指定した文字列に変更する機能と、Inspector上の要素番号の開始数字を変更する機能を持っています。
【Unity】配列にて各要素の表示される名前とIndexを変更するAttribute
このように使用します。
using UnityEngine; public class TestComponent : MonoBehaviour { [SerializeField, CustomListLabelAttribute( "Attack", 1 )] private int[] m_valueA; [SerializeField, CustomListLabelAttribute( "Magic", 3 )] private int[] m_valueB; } // class TestComponent
成果物B 配列にて各要素の表示される名前がEnumと連動するAttribute
続いては、配列にて各要素の表示される名前がEnumと連動するAttributeです。
要素名がEnumの文字列に変換されます。Enumの数以上に要素がある場合は、普通の状態になります。
【Unity】配列にて各要素の表示される名前がEnumと連動するAttribute
このように使用します。
using UnityEngine; public enum EAttack { Sword, Lance, Axe, } public enum EMagic { Fire, Wind, Thunder, } public class TestComponent : MonoBehaviour { [SerializeField, Uraproject.EnumListLabel( typeof( EAttack ) )] private int[] m_valueA; [SerializeField, Uraproject.EnumListLabel( typeof( EMagic ) )] private int[] m_valueB; } // class TestComponent
なんの役に立つかわかりませんが、ご自由にお使いくだされ。