うら干物書き

ゲームを作っています。

【Unity】俺はこのプロパティが配列の何番目のものかを知りたいだけなんだ!

 プロジェクトの中にある、自前のライブラリを見ていたら、エディタ拡張用のAttributeクラスを見つけました。
 そういえば、一時期エディタ拡張にはまっていたような気がします。

 そんな自作Attributeクラスの中で、とあるプロパティが配列の何番目を調べるコードがありました。
 むちゃくちゃな方法で要素番号を取得してやがる……。

 作った日のことを思い出しながら、もう一度同じ過ちを繰り返す、そんなお話。


この記事にはUnity5.5.1f1を使用しています。

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上で見ると、このようになります。

f:id:urahimono:20170212164744p:plain

 特に変化は見られません。  TestAttributeOnGUI()では渡されたプロパティをEditorGUI.PropertyField()を使ってデフォルトの描画しているだけなので当然です。

 では、TestComponentvalueの型を配列にしてみましょう。

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    [SerializeField, Test]
    private int[] m_value;

} // class TestComponent

f:id:urahimono:20170212164756p:plain

 ちゃんと配列としてInspector上で描画されています。
 ここで気になるのは、TestAttributeDrawerOnGUI()は、配列の各要素に対して呼ばれているのか、それとも配列の変数に対して呼ばれているのかが気になります。
 ログを仕込んで確認してみましょう。

[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

f:id:urahimono:20170212164808p:plain

 ログを見る限り、配列の各要素に対して呼ばれることを確認しました。
 ではOnGUI()が呼ばれた際のTestAttributeDrawerは、何番目の要素を描画してようとしているのかはわかるのでしょうか。

 TestAttributeDrawerは引数として渡された値を描画するだけですから、TestAttributeDrawerクラスそのものは知りえないでしょう。
 だとすると引数として渡されたものから何か情報が得られるかもしれません。
 この中で一番怪しそうなのは、SerializedPropertyです。

 スクリプトリファレンスを見てみましょう。
https://docs.unity3d.com/ja/current/ScriptReference/SerializedProperty.html

 あ、あれ。欲しい情報が無い。
 isArrayのような、配列かどうかを判断するプロパティならあるけど、「私は配列の何番目の要素です」的なプロパティがない。

 マジかよ……。

 だがここで諦めるわけにはいかない!
 どんな手を使ってでも要素番号を取得して見せるぜ!

無理やりな方法その1 Inspector上の名前で推測する。

調査開始

 さて、配列やリストを使った際のInspector上の要素名に注目してみよう。

f:id:urahimono:20170212164822p:plain

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

f:id:urahimono:20170213230118p:plain

 よっしゃー!
 出来たー!
 この理論が正しいことが証明されましたよ!

欠点

 こういう名前の変数に対して、先ほど作ったTestAttributeを使ってみましょう。
 するとどうなるか……。

public class TestComponent : MonoBehaviour
{
    [SerializeField, Test]
    private int[] m_value;

    [SerializeField, Test]
    private int m_element5;

} // class TestComponent

f:id:urahimono:20170212164834p:plain

 でしょうね。
 文字列でやっちゃうとこんなことになるよね。
 配列でもなんでもない変数でも、Elementと数字を組み合わせた変数名にされると、このTestAttributeを使っちゃうとこのなるよね。
 まあ、このAttribute配列以外では使わない!というルールで対応するしかないですね……。

無理やりな方法その2 SerializedPropertyから全力で探す

調査開始

 いや、もっと何かあるはずだ。
 リファレンスを見るだけでは見つからなかったが、もっと詳しく調べれば何かあるはずだ!

 まず、ブレークポイントで止めてSerializedPropertyの中身を見てみましょう。

f:id:urahimono:20170212164905p:plain

 うーん、何か情報はないものか……。
 おや、これは。

f:id:urahimono:20170212164914p:plain

 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]

 こ、これは使えるかもしれません。
 Arraydata[…]が配列の要素を表している感じがする。
 この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

f:id:urahimono:20170212164926p:plain

 よっしゃー!
 出来たー!
 この理論が正しいことが証明されましたよ!
 ラベルの文字列の時と違って、m_element5m_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

f:id:urahimono:20170212164936p:plain

成果物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

f:id:urahimono:20170212164946p:plain

 なんの役に立つかわかりませんが、ご自由にお使いくだされ。