はじめに
Unityを使って開発をしているときに、独自の属性を付与したシリアル化されたフィールドを一括処理したいと考えたため、シリアル化されたフィールドのフィールド情報を取得する方法を考えます。
実装について
方針を立てる
UnityEditorでシリアル化されたフィールドはSerializedPropertyとして取得したり扱ったりすることになりますが、SerializedPropertyが外部に公開する機能の中にはフィールド情報を取得する機能がないため、何か別の方法を考えます。
ここで、SerializedProperty.boxedValueというシリアル化されるほとんどの型の値を取得できるインスタンスプロパティに着目し、SerializedProperty.boxedValueがどのようにフィールドの値を取得しているのかという部分から手法を構築することを試行します。
SerializedProperty.boxedValueの実装を確認する
幸いなことに、UnityではエディターコードのC#部分の一部がGitHubに公開されているため、ここからSerializedPropertyの実装を確認していきます。
UnityCsReference/Editor/Mono/SerialiedProperty.bindings.cs
このソースファイルの301行目にSerializedProperty.boxedValueの実装があります。
そのうち、ゲッター(get)がSerializedPropertyType.Genericの型(配列やリスト、独自で実装したシリアル化可能なクラスや構造体など)に対して行う取得処理に注目してみると、
public System.Object boxedValue
{
get
{
switch (propertyType)
{
case SerializedPropertyType.Generic: return structValue;
// 以下省略
となっていて、structValueなるものから値を取得しているようです。
SerializedProperty.structValueの実装を辿る
structValueの実装は、同ソースファイルの1378行目にあります。そのゲッター(get)を見てみましょう。
internal object structValue
{
get
{
if (isArray)
throw new System.InvalidOperationException($"'{propertyPath}' is an array so it cannot be read with boxedValue.");
// Unlike managed references, the precise type for a struct or by-value class field is easier to determine in C#
// rather than at the native level, so we pass that info in.
UnityEditor.ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromProperty(this, out Type type);
var nameSpace = type.Namespace;
string typeName = type.FullName.Replace("+", "/");
if (!string.IsNullOrEmpty(nameSpace))
typeName = typeName.Substring(nameSpace.Length + 1);
return GetStructValueInternal(type.Assembly.GetName().Name, nameSpace, typeName);
}
// 以下省略
どうやら、UnityEditor.ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromPropertyでSerializedPropertyから型を取得して、それをもとに内部値を取得しているようです。
ここでGetFieldInfoAndStaticTypeFromPropertyに着目します。なんとなくフィールド情報を取得できそうな名前をしていますね。
ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromPropertyの実装を辿る
GetFieldInfoAndStaticTypeFromPropertyの実装はUnityCsReference/Editor/Mono/Inspector/Core/ScriptAttributeGUI/ScriptAttributeUtility.csの423行目にあります。
ここで、このメソッドのシグネチャを確認してみると、
internal static FieldInfo GetFieldInfoAndStaticTypeFromProperty(SerializedProperty property, out Type type)
となっていて、参照渡し引数でプロパティの型を、戻り値でフィールド情報を取得することができそうです。
SerializedPropertyからフィールド情報を取得できるようにする
取り回しをよくするためにSerializedPropertyExtension.csというソースファイルを作成して拡張メソッドとして実装していきます。
ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromPropertyをユーザーコードから呼び出す
// ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromPropertyメソッドのシグネチャを定義したデリゲート型
private delegate FieldInfo GetFieldInfoAndStaticTypeFromProperty(SerializedProperty property, out Type type);
// UnityEditor.dllのアセンブリ名のフルネーム
private const string UnityEditorDllAsmFullName = "UnityEditor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";
// ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromPropertyメソッドのデリゲート
private static readonly GetFieldInfoAndStaticTypeFromProperty fieldInfoAndTypeGetter;
// 初期化処理
static SerializedPropertyExtension()
{
// 1. UnityEditor.dllのアセンブリを探す
var unityEditorDll = AppDomain.CurrentDomain.GetAssemblies()
.Where(x => x.FullName == UnityEditorDllAsmFullName).FirstOrDefault();
if (unityEditorDll != null)
{
// 2. UnityEditor.dllからScriptAttributeUtilityクラスを探す
var scriptAttributeUtil = unityEditorDll.GetType("UnityEditor.ScriptAttributeUtility");
if (scriptAttributeUtil != null)
{
// 3. ScriptAttributeUtilityクラスからGetFieldInfoAndStaticTypeFromPropertyを探し、デリゲートを生成する
fieldInfoAndTypeGetter = scriptAttributeUtil
.GetMethod("GetFieldInfoAndStaticTypeFromProperty", BindingFlags.NonPublic | BindingFlags.Static)
?.CreateDelegate(typeof(GetFieldInfoAndStaticTypeFromProperty)) as GetFieldInfoAndStaticTypeFromProperty;
}
}
}
今回は拡張メソッドを実装するクラスの静的コンストラクタに取得処理を記述しています。
やっていることとしては、
- 呼び出したい処理が
UnityEditor.dllにあるため、このアセンブリを探す
(確実に見つけ出すためにAppDomain.CurrentDomainから取得する) - 取得した
UnityEditor.dllのアセンブリから、UnityEditor.ScriptAttributeUtilityクラスを探して型情報を取得する
(internalでstaticなメソッドであるため、bindingAttr引数にBindingFlags.NonPublicとBindingFlags.Staticのフラグを指定する) - 取得した型情報から、
GetFieldInfoAndStaticTypeFromPropertyメソッドを探してデリゲートを生成する
となります。ScriptAttributeUtilityがinternalなクラスであるため、今回はリフレクションで取得しています。特定のアセンブリ名のAssemblyDefinitionを作って、その中から直接呼び出す方法も一応存在するため、そこらへんはお好みで。
これで、今回実装する拡張メソッドを使う直前にデリゲートを生成する処理が実行されるようになりました。
SerializedPropertyからフィールド情報を取得できるようにする
// プロパティのフィールド情報を取得する
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetFieldInfo(this SerializedProperty property, out FieldInfo fieldInfo)
{
if (property == null || fieldInfoAndTypeGetter == null)
{
// nullが渡されたり、デリゲートの処理がうまくいかなかった場合は失敗
fieldInfo = default;
return false;
}
// プロパティのフィールド情報を取得する
fieldInfo = fieldInfoAndTypeGetter.Invoke(property, out _);
return fieldInfo != null;
}
先ほど取得したScriptAttributeUtility.GetFieldInfoAndStaticTypeFromPropertyのデリゲートを使用してフィールド情報を取得します。
また、プロパティの型を取得する拡張メソッドは以下の通りになります。また、GetFieldInfoAndStaticTypeFromPropertyを単純にラップする拡張メソッドを実装しても便利かもしれません。
// プロパティの値の型を取得する
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetPropertyType(this SerializedProperty property, out Type type)
{
if (property == null || fieldInfoAndTypeGetter == null)
{
// nullが渡されたり、デリゲートの処理がうまくいかなかった場合は失敗
type = default;
return false;
}
// プロパティの型を取得する(失敗時は戻り値にnullが返される)
if (fieldInfoAndTypeGetter.Invoke(property, out type) == null)
{
type = null;
return false;
}
return true;
}
なぜScriptAttributeUtility.GetFieldInfoAndStaticTypeFromPropertyからフィールド情報と型が別々に取得できるようになっているかというと、SerializedPropertyは配列やリストの要素単体を指すことがあるためです。その場合、フィールド情報からはフィールド自体の型(配列型やリスト型)が返されてしまうためです。
参照渡しの型情報はその要素自体の型のものが渡されます。
使用例
以下のように、シンプルに使うことができるようになっています。
// 目的のSerializedPropertyを取得する
using (var zokuseiProp = serializedObject.FindProperty("zokuseiTsuki"))
{
// SerializedPropertyからフィールド情報を取得する
if (zokuseiProp.GetFieldInfo(out var fieldInfo))
{
// フィールド情報を使ってフィールドに付与された属性の取得を試行する
var nankaAttribute = fieldInfo.GetCustomAttribute<CustomNankaAttribute>()
// 以下、それらを使って何かしらを行う
// ...
}
}