SerializeReferenceとReorderableListを組み合わせて使う

この記事は[gumi Inc. Advent Calendar 2019](https://qiita.com/advent-calendar/2019/gumi)の12/08の記事です。

#SerializeReferenceの簡単な概要

Unity219.3からSerializeReference属性が導入され、Unityのシリアライザがポリモーフィックなシリアライズをしてくれるようになりました。`MonoBehaviour`や`ScriptableObject`へ参照はこれまでの`SerializeField`属性でもある程度ポリモーフィックにできたのですが、Serializableなだけのただのclassへは非対応でした。

```csharp
public interface IHoge
{
}

[Serializable]
public class Hoge1 : IHoge
{
    public string HogeHoge;
}

[Serializable]
public class Hoge2 : IHoge
{
    public int FugaFuga;
}
```

```csharp
[SerializeReference]
private IHoge _hoge = null; //Hoge1でもHoge2でも挿せる. SerializeFieldでは無理だった
```

SerializeReferenceの挙動を実現するためにSerializePropertyのレベルで変更が入っています。具体的には、[managedReferenceValue](https://docs.unity3d.com/2019.3/Documentation/ScriptReference/SerializedProperty-managedReferenceValue.html)というものが追加されました。`IHoge`の実体として`Hoge1`を使いたければ以下のようなコードを書けばOKです。

```csharp
var prop = serializedObject.FindProperty("_hoge");
prop.managedReferenceValue = new Hoge1();
```

**このようなコードでOK...というかこのようなコードが必須です**。具象型が決まらないとエディタも入力支援のしようがありません。SerializeReferenceを使う場合は、何らかのエディタ拡張によって具象型を渡してあげるステップが必要なので地味に面倒です。しかしすでに先人の知恵が出回っていて、単一要素の参照であればAttribute書くだけで解決です。先人すごい。

[【Unity】インターフェイスをSerialize出来るようにするSerializeReferenceのための表示attributeを作ってみた](https://qiita.com/tsukimi_neko/items/7922b2433ed4d8616cce)

今回の記事ではReorderableListにポリモーフィックな配列を入れてみようという試みをします。

### 環境
Unity2019.3.0f1を使用しています.

# 今回作るもの

```csharp
public class HogeList : ScriptableObject
{
    [SerializeReference]
    private IHoge[] _hoges = null;
}
```
上記のようなポリモーフィックな配列に対して、以下のように具象型を選べるReorderableListを作ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328832/f6d3ea11-76e0-1f64-f72c-9c0701eb4b7e.png)

やることは以下です.順番につぶしていきましょう。

1. +ボタンが押されたときに、メニューを出せるようにする
1. メニューには、IHogeを継承した型をリストアップする
1. メニューのどれかが選ばれたら対応する型のオブジェクトを作成して_hogesに突っ込む

## +ボタンが押されたときに、メニューを出せるようにする

+ボタンが押されたときにすぐ処理をするのではなく別のUIを表示する場合は、`ReorderableList.onAddDropdownCallback`を用います。

```csharp
_reorderableList.onAddDropdownCallback = (Rect buttonRect, ReorderableList target) =>
{
};
```

`ReorderableList.onAddDropdownCallback`を指定すると、+ボタンの右下になんかドロップダウンを出してくれそうな▼が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328832/cc8cab90-c3cd-c1a0-c081-951e9fc03415.png)
でもそれだけです。ドロップダウンに相当するUIはこちらで用意してあげる必要があります(ェー)。このようなときに便利なのが`GenericMenu`です。

```csharp
_reorderableList.onAddDropdownCallback = (Rect buttonRect, ReorderableList target) =>
{
    var menu = new GenericMenu();
    menu.AddItem(new GUIContent("項目1"), on: false, func: userData =>
    {
        Debug.Log("項目1が押されたぞい");
    }, userData: null);
    menu.ShowAsContext();
};
```

**AddItemの第四引数のuserDataに渡したobjectがクリック時のコールバック関数の引数に渡ってくる**ことだけ覚えていれば簡単に使えると思います。さて、これでメニューが表示されるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328832/0cb05a89-d302-14a3-a70a-2c9c8ea863cf.png)

## メニューには、IHogeを継承した型をリストアップする

Unity2019.2から[TypeCache](https://docs.unity3d.com/2019.2/Documentation/ScriptReference/TypeCache.html)が導入されたので、AppDomainからAssembly引っこ抜いてGetTypesして...というコードは書かなくてよくなりました。

```csharp
var types = TypeCache.GetTypesDerivedFrom<IHoge>();
```
これだけでHoge1とHoge2が取れます。便利。これでGenericMenuに詰めるべきものが取得できました。

```csharp
_reorderableList.onAddDropdownCallback = (Rect buttonRect, ReorderableList target) =>
{
    var menu = new GenericMenu();
    foreach (var type in TypeCache.GetTypesDerivedFrom<IHoge>())
    {
        menu.AddItem(new GUIContent(type.Name), false, obj =>
        {
            var t = (Type)obj;
        },
        type);
    }
    menu.ShowAsContext();
};
```

## メニューのどれかが選ばれたら対応する型のオブジェクトを作成して_hogesに突っ込む

コールバックの中で貰ったTypeを元にオブジェクトを作って`managedReferenceValue`にぶち込むだけなのでもう簡単です。

```csharp
_reorderableList.onAddDropdownCallback = (Rect buttonRect, ReorderableList target) =>
{
    var menu = new GenericMenu();
    foreach (var type in TypeCache.GetTypesDerivedFrom<IHoge>())
    {
        menu.AddItem(new GUIContent(type.Name), false, obj =>
        {
            var t = (Type)obj;
            var index = _reorderableList.serializedProperty.arraySize;
            _reorderableList.serializedProperty.arraySize++;

            var elementProp = _reorderableList.serializedProperty.GetArrayElementAtIndex(index);
            elementProp.managedReferenceValue = (IHoge)Activator.CreateInstance(type);
            serializedObject.ApplyModifiedProperties();
        },
        type);
    }
    menu.ShowAsContext();
};
```

これで型に応じたオブジェクトを作れるようになりました。
が、どうもUnity的にはここでApplyModifiedPropertiesを呼ぶのは良くないようで、以下のエラーが出てしまいます。この記事ではこのエラーは未解決です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328832/39a0abf8-a70f-7c28-4f30-83621158c01f.png)

釈然としない気持ちを抱えつつ、生成されたオブジェクトが正しいかを確認していきましょう。

# 具象型向けのPropertyDrawerを作って確認する

Hoge1とHoge2用のPropertyDrawerを作成します。

```csharp
[CustomPropertyDrawer(typeof(Hoge1))]
public class Hoge1Drawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.LabelField(position, "Hoge1ですよ");
    }
}

[CustomPropertyDrawer(typeof(Hoge2))]
public class Hoge2Drawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.LabelField(position, "Hoge2ですよ");
    }
}
```

`ReorderableList.drawElementCallback`も設定します。このあたりはReorderableListを使うときの定番なのでさくさくと

```
_reorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
{
    var prop = _hogesProperty.GetArrayElementAtIndex(index);
    EditorGUI.PropertyField(rect, prop);
};
```

**できました、やったね！**。ちゃんと表示が分岐しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328832/a09ad4bc-0c62-ea10-b044-c2d309f64922.png)

## と思いきや落とし穴が…

Hoge1をつくって、消して、Hoge2をいれてみると…

つくって…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328832/955847d4-255c-1733-190f-d648471a15c1.png)

消して…次にHoge2
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328832/fa2d475f-b4e9-31b8-cf90-70f6176b929f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328832/1320b52d-2042-f55c-c11b-929eb8a0f589.png)

あんれー！！！？？？

なぜかHoge1のDrawerが呼ばれました。Hoge1が生成されちゃったの…？とおもってデバッグでみたらちゃんとHoge2の値が渡ってきます。Hoge2に対してHoge1のPropertyDrawerが呼ばれてしまうようです。悲しみ。しかしIHogeへのPropertyDrawerを作ることはできませんので、**Unity2019.3.0f1時点でこの問題の回避を行うのであればPropertyDrawerに頼ってはいけない**という結論になりそうです。

# まとめ

1. `ReorderableList.onAddDropdownCallback`と`GenericMenu`と`TypeCache`を使って、ポリモーフィックな配列へのエディタを作成した
2. GenericMenuのクリックハンドラで`SerializedObject.ApplyModifiedProperties()`を呼ばないと保存されないがエラーメッセージがでてしまう
3. PropertyDrawerが型解決を間違うことがある

なんともしょっぱい結末になってしまいましたが、PropertyDrawerあたりに関してはUnityのバージョンアップできっと直ってくれると信じてます。GenericMenuでのエラーメッセージは…うーん、どうするのがいいんでしょうね。

というもやもやを抱えつつ、SerializeReferenceは素晴らしい新機能だと思いますので使いこなしていければと思います。