Help us understand the problem. What is going on with this article?

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

この記事はgumi Inc. Advent Calendar 2019の12/08の記事です。

SerializeReferenceの簡単な概要

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

public interface IHoge
{
}

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

[Serializable]
public class Hoge2 : IHoge
{
    public int FugaFuga;
}
[SerializeReference]
private IHoge _hoge = null; //Hoge1でもHoge2でも挿せる. SerializeFieldでは無理だった

SerializeReferenceの挙動を実現するためにSerializePropertyのレベルで変更が入っています。具体的には、managedReferenceValueというものが追加されました。IHogeの実体としてHoge1を使いたければ以下のようなコードを書けばOKです。

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

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

【Unity】インターフェイスをSerialize出来るようにするSerializeReferenceのための表示attributeを作ってみた

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

環境

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

今回作るもの

public class HogeList : ScriptableObject
{
    [SerializeReference]
    private IHoge[] _hoges = null;
}

上記のようなポリモーフィックな配列に対して、以下のように具象型を選べるReorderableListを作ります
image.png

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

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

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

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

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

ReorderableList.onAddDropdownCallbackを指定すると、+ボタンの右下になんかドロップダウンを出してくれそうな▼が表示されます。
image.png
でもそれだけです。ドロップダウンに相当するUIはこちらで用意してあげる必要があります(ェー)。このようなときに便利なのがGenericMenuです。

_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

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

Unity2019.2からTypeCacheが導入されたので、AppDomainからAssembly引っこ抜いてGetTypesして...というコードは書かなくてよくなりました。

var types = TypeCache.GetTypesDerivedFrom<IHoge>();

これだけでHoge1とHoge2が取れます。便利。これでGenericMenuに詰めるべきものが取得できました。

_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にぶち込むだけなのでもう簡単です。

_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

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

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

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

[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

と思いきや落とし穴が…

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

つくって…
image.png

消して…次にHoge2
image.png

image.png

あんれー!!!???

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

まとめ

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

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away