【エディタ拡張】Unity Editor 上でも interface を参照したい!
目的
同じinterface
を実装しているならMonoBehaviour
やScriptableObject
を区別せずにエディタから参照したい!
最終的な形
以下のような使い方で型指定したinterface
を実装したオブジェクトを参照できるようにした。
using UnityEngine;
public interface ITarget {}
public class User : MonoBehaviour
{
[SerializeField]
InterfaceReference<ITarget> container; // 単一の参照
[SerializeField]
InterfaceReference<ITarget>[] containers; // 配列の場合
}
これでエディタからITarget
インターフェースを実装したMonoBehaviour
やScriptableObject
のみをエディタから与えられるようにしたい。
実装
今回の実装において最も重要なのはInterfaceReference
をエディタ上に表示するためのエディタ拡張である。
また、筆者の環境はUnity 2022.3
である。
InterfaceReference
InterfaceReference
は以下のようにとってもシンプルな構造。
[Serializable]
public class InterfaceReference<Interface>
{
[SerializeField]
UnityEngine.Object _reference;
public Interface current
{
get
{
if (_reference is Interface value) return value;
throw new InvalidCastException();
}
}
}
Unity.Object
はMonoBehaviour
やScriptableObject
などの Unity 上のさまざまなオブジェクトの大元で、これによってどちらの参照も保有できる。
InterfaceReference
にスクリプトから参照を入れる仕組みがないが、そもそもスクリプト内で参照を解決するのであればITarget
の変数なりを定義すればいいだけなので、これを使う必要がないと判断。
どうしても欲しい場合は各自実装すること。そんなに難しくないはずである。
InterfaceReferenceDrawer
上のInterfaceReference
だけでは、UnityEngine.Object
を参照しているだけなので、エディタ上から型引数で指定された interface を実装しているか検証できていない。
これでは実行時でないと検証されないので、それは困る。
ということでエディタ拡張が必要で、エディタ上の表示を変えるためにCustomPropertyDrawer
を作る。
私が実装したのは以下のようにすることでやりたいことができた。
[CustomPropertyDrawer(typeof(InterfaceReference<>), true)]
public class InterfaceReferenceDrawer : PropertyDrawer
{
private static float LineHeight { get { return EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; } }
// フィールドの型の取得(配列の場合、その各要素の型を取得)
protected Type FieldType
{
get
{
var fieldType = fieldInfo.FieldType;
if (fieldType.IsArray)
{
fieldType = fieldType.GetElementType();
}
else if (typeof(IList).IsAssignableFrom(fieldType))
{
fieldType = fieldType.GetGenericArguments()[0];
}
return fieldType;
}
}
// 型引数で指定した interface の型の獲得
protected Type InterfaceType => FieldType.GetGenericArguments()[0];
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var referenceProp = property.FindPropertyRelative("_reference");
// GUI の描画
EditorGUI.BeginProperty(position, label, property);
EditorGUI.ObjectField(position, referenceProp, label);
EditorGUI.EndProperty();
// 入力値の検証
var reference = referenceProp.objectReferenceValue;
if (reference is null) return;
if (reference is GameObject go)
{
// GameObject が入力された場合はアタッチされているコンポーネントを検索する
if (go.TryGetComponent(InterfaceType, out Component component))
{
reference = component;
}
else
{
reference = null;
}
}
if (!IsValid(reference))
{
reference = null;
Debug.LogError($"'{property.displayName}' is able to reference ONLY Object implemented '{InterfaceType}' <at {property.serializedObject.targetObject}>");
}
referenceProp.objectReferenceValue = reference;
}
// reference オブジェクトが InterfaceType を 継承 / 実装 しているかどうかチェックする関数
protected virtual bool IsValid(UnityEngine.Object reference)
{
if (reference is null) return false;
var refType = reference.GetType();
return InterfaceType.IsAssignableFrom(refType);
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return LineHeight;
}
}
最初どのように型引数を取得するのかわからず右往左往した。SerializedProperty
からどうにか型情報を引っ張り出そうとしたが、うまくいかなかった。
公式リファレンスと睨めっこしていたら、PropertyDrawer
にfieldInfo
というプロパティが存在することを発見し、ことなきを得た。
ただ、fieldInfo.FieldType
を直接使うと以下の配列の場合にうまくいかない。
[SerializeField]
InterfaceReference<ITarget>[] references;
なのでfieldInfo.FieldType
が配列なのかを判定し、配列の場合GetElementType()
を使うことで、配列要素の型を取得することで回避している。
課題
Serializable
なオブジェクトの中にInterfaceReference
がある状況、つまりネストが深くなると表示が崩れてしまう。
参考
Discussion