PropertyDrawerが思ったより便利だった

[サムザップ #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/sumzap2) の12/18の記事です。

株式会社サムザップでUnityエンジニアやってる二宮です。
最近、Editor拡張を教えてもらって、今更ながらナニコレスゴクツカエルって思ったので紹介します。

# PropertyDrawer

今回紹介したいのはこちらっ！
[PropertyDrawer](https://docs.unity3d.com/Manual/editor-PropertyDrawers.html)

これを使うと、型をシリアライズした時にカスタマイズしたViewで表示されるようになります。

これが、思ったよりもだいぶ強力なのです。

# 素敵な使用例

マスターデータを参照して情報を表示したい！みたいな場合に使えます。
そう例えばローカライズ！

UIにテキストデータを書き込みたいところだけれど、テキストデータは言語設定によって変わるため、直シリアライズはNG,,,.
そうすると、「マスターのキーをシリアライズして、Runtimeでテキストデータを言語設定に応じて取得して表示」となるのですが、これは設定が結構辛くなる。

シリアライズ時のキーがあっているかどうかがぱっと見わかりづらい。
こうなると、うっかりキーを打ち間違えたり、Previewが面倒だったりします。

じゃあちゃんとカスタマイズして、選択式にするぞ、、、！とかって考えてはみるものの、
Editor拡張で頑張ろうとしても表示するUIが変わると、それごとに拡張書かなければならなくなってしまって辛くなってきます。
こちとらマスターデータからテキストをとって表示したいだけなのに！

そんな時に！プロパティドロワー！

こんな感じにいつも通りシリアライズするだけで、どのスクリプトでも同じ拡張が表示されます。
Textでもポップアップでも、トグルのラベルでもボタンでも！ なんと素敵な、、、！

```
[RequireComponent(typeof(Text))]
public class TextView : MonoBehaviour
{
    [SerializeField] private LocalizedText _text;

    private void Awake()
    {
        GetComponent<Text>().text = _text.Value;
    }
}

/// ローカライズされたテキストデータ
[Serializable]
public class LocalizedText
{
    // マスターから読み込むのに使うキー
    [SerializeField] private string _textKey;
    // Viewで使うテキストデータ
    public string Value => TextMaster.Instance.GetText(_textKey);
}

/// 仮想テキストマスター。言語設定に合わせたテキストのマスターデータを読み込んで使う
public class TextMaster
{
    private static TextMaster _instance = null;
    public static TextMaster Instance => _instance ?? (_instance = new TextMaster());

    public readonly Dictionary<string, string> Data = new Dictionary<string, string>();

    public TextMaster()
    {
        // TODO: 言語設定に合わせたテキストマスターを取得する
        for (int i = 0; i < 10; i++)
        {
            Data[$"key{i}"] = $"テキストデータ {i}";
        }
    }

    /// マスターからテキストデータを取得する
    public string GetText(string key) => Data[key];
}
```

作るのが一つで良いから、選択式にしたりとか便利さを追求したくなっちゃいますよね！
どうでしょう？使えそうって思ったんじゃないですか？？

よさが十分に伝わったところで、簡単に実装例を出しておきますね。

# PropertyDrawerの実装方法

ローカライズに使えそうなサンプル実装をしてみました。

## 作ったもの
<img width="451" alt="スクリーンショット 2019-12-18 12.59.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/7531eb1a-2bbc-a60a-dcbf-cff15271296c.png">

ポップアップ形式で、仮想マスターデータからテキストを選ぶ形式にしてみました。
シリアライズしているのはマスターのキー（LocalizedText._textKey)のはずですが、テキストデータが表示されるようにしています。

## PropertyDrawerの実装

```
[CustomPropertyDrawer(typeof(LocalizedText))]
public class PropertyDrawerLocalizedText : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // 現在格納されているキーデータをもとに、ポップアップに必要なデータを取得
        var keys = TextMaster.Instance.Data.Keys.ToArray();
        var textKeyProperty = property.FindPropertyRelative("_textKey"); // カスタムしているプロパティがもつ"_textKey"変数のSerializedPropertyを取得する
        var previousIndex = Array.IndexOf(keys, textKeyProperty.stringValue);

        // ポップアップを表示
        var selectedIndex = EditorGUI.Popup(position, previousIndex, TextMaster.Instance.Data.Values.ToArray());

        // ポップアップで値を変えていれば、キーを取得してシリアライズ情報を更新
        if (selectedIndex != previousIndex)
        {
            previousIndex = selectedIndex;
            textKeyProperty.stringValue = keys[previousIndex];

            // 修正があったらシリアライズしてあるオブジェクトに変更を反映
            textKeyProperty.serializedObject.ApplyModifiedProperties(); // これがないとデータが何も変更されない
        }
    }
}
```

Editor拡張全般に言える気がしますが、割とごにょっとします。
でもまぁこれでこの型をシリアライズしたら毎回ポップアップが表示されてくれると思えば全然良いですよね。

## 注意点
* EditorGUILayoutが使えなくて、EditorGUIで頑張る必要がある
* 高さが変わる場合、高さを指定する必要がある

Layout系が使えないので、Rectをきちんと指定する必要があって、不慣れだと辛いです。辛かったです。
PropertyDrawerでは、高さの変更を記述してあげる必要があります。
これを怠って、こんな感じに１行追加するだけだと、残念な感じになります。

<img width="446" alt="スクリーンショット 2019-12-18 13.20.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/57c24eff-6fed-22ce-1c47-dbcd39d9f94d.png">

```
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    var keys = TextMaster.Instance.Data.Keys.ToArray();

    EditorGUI.LabelField(position, "テキストキー");
    position.y += EditorGUIUtility.singleLineHeight;

    var textKeyProperty = property.FindPropertyRelative("_textKey");
    var previousIndex = Array.IndexOf(keys, textKeyProperty.stringValue);
```

EditorGUI初心者だったのでしっかりハマりました。はい。

こうやって、高さを１行増やしたことを通知してあげる必要があるんですね。

```
/// プロパティの高さを取得する。カスタムによって高さが変わるなら必須
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
    return base.GetPropertyHeight(property, label) + EditorGUIUtility.singleLineHeight;
}
```

というわけで、実装にちょいと癖はあるので気をつけつつ、本当に便利なので是非使ってみてください〜
明日は@sato_tatsukiさんの記事です。