この記事はgumi Inc. Advent Calendar 2019の12/08の記事です。
#SerializeReferenceの簡単な概要
Unity219.3からSerializeReference属性が導入され、Unityのシリアライザがポリモーフィックなシリアライズをしてくれるようになりました。MonoBehaviour
やScriptableObject
へ参照はこれまでの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を作ります
やることは以下です.順番につぶしていきましょう。
- +ボタンが押されたときに、メニューを出せるようにする
- メニューには、IHogeを継承した型をリストアップする
- メニューのどれかが選ばれたら対応する型のオブジェクトを作成して_hogesに突っ込む
+ボタンが押されたときに、メニューを出せるようにする
+ボタンが押されたときにすぐ処理をするのではなく別のUIを表示する場合は、ReorderableList.onAddDropdownCallback
を用います。
_reorderableList.onAddDropdownCallback = (Rect buttonRect, ReorderableList target) =>
{
};
ReorderableList.onAddDropdownCallback
を指定すると、+ボタンの右下になんかドロップダウンを出してくれそうな▼が表示されます。
でもそれだけです。ドロップダウンに相当する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がクリック時のコールバック関数の引数に渡ってくることだけ覚えていれば簡単に使えると思います。さて、これでメニューが表示されるようになりました。
メニューには、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を呼ぶのは良くないようで、以下のエラーが出てしまいます。この記事ではこのエラーは未解決です。
釈然としない気持ちを抱えつつ、生成されたオブジェクトが正しいかを確認していきましょう。
具象型向けの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);
};
できました、やったね!。ちゃんと表示が分岐しています。
と思いきや落とし穴が…
Hoge1をつくって、消して、Hoge2をいれてみると…
あんれー!!!???
なぜかHoge1のDrawerが呼ばれました。Hoge1が生成されちゃったの…?とおもってデバッグでみたらちゃんとHoge2の値が渡ってきます。Hoge2に対してHoge1のPropertyDrawerが呼ばれてしまうようです。悲しみ。しかしIHogeへのPropertyDrawerを作ることはできませんので、Unity2019.3.0f1時点でこの問題の回避を行うのであればPropertyDrawerに頼ってはいけないという結論になりそうです。
まとめ
-
ReorderableList.onAddDropdownCallback
とGenericMenu
とTypeCache
を使って、ポリモーフィックな配列へのエディタを作成した - GenericMenuのクリックハンドラで
SerializedObject.ApplyModifiedProperties()
を呼ばないと保存されないがエラーメッセージがでてしまう - PropertyDrawerが型解決を間違うことがある
なんともしょっぱい結末になってしまいましたが、PropertyDrawerあたりに関してはUnityのバージョンアップできっと直ってくれると信じてます。GenericMenuでのエラーメッセージは…うーん、どうするのがいいんでしょうね。
というもやもやを抱えつつ、SerializeReferenceは素晴らしい新機能だと思いますので使いこなしていければと思います。