Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

※ 2020/05/23 @makihiro_dev 様のコメントを参考に再度コード修正
※ 2020/03/26 @makihiro_dev 様のコメントを参考にコード修正
※ 2019/12/01 @baba_s 様のコメントを参考にコード修正

本題

Unity 2019.3 から新たに [SerializeRefence] という attribute が追加されました。

皆さんご存知ですか?
なんと、非Monoクラス限定ですが、この attribute を使えば、インターフェイスや子クラスも Serialize 出来るようになり、
エディターから直接編集出来るようになります!

もう本当に、大興奮ですよ。この情報を初めて知った自分は小一時間ずっと飛び跳ねていました。
このattributeがあると何が嬉しいかというと、これで使い方がめっちゃ広がるし、interfaceを実装したクラスを一々 ScriptableObject で包まなくて済みます。

実際に使ってみたのですが

この機能を試すべく2019.3.0a7をダウンロードして、このattributeを早速使ってみました。
書いたコードはこれです:

[SerializeReference] 
ICommand command;

しかし、出てきたのはこれです:
empty-command.png
ご覧の通り、インスペクターにフィールドの名前以外、何も表示されませんでした……

せっかくインターフェイスをシリアライズ出来るようになったのに、工夫してEditor拡張をしなければ何も出来ません。依然として敷居が高いです。

これじゃイケない! と思ったので、SerializeReference を補助する
自動でサブクラス検索し、インスペクターから選択生成できる attribute [SubclassSelector] 作りました。

[SubclassSelector]

使い方:

[SerializeReference, SubclassSelector] 
ISomeItnterface someInterface;
// Monobehaviourも含めて検索したい場合:
[SerializeReference, SubclassSelector(true)] 
ISomeItnterface someInterface;

追記:コメントで @baba_s 様から typeof(ISomeItnterface) を使わないで、
PropertyDrawerから直接フィールドのTypeを取得する関数を教えて頂きました。ありがとうございます!

導入

以下の2ファイルをコピペし、プロジェクトに入れる。(厳密に言うと、Drawer.cs のほうは任意の Editor/ フォルダーの下に入れる)
また、@baba_s 様のサンプル付きプロジェクトもここからダウンロード出来ます:
https://github.com/baba-s/Unity-SerializeReferenceExtensions

SubclassSelectorAttribute.cs
using System;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class SubclassSelectorAttribute : PropertyAttribute
{
    private bool m_includeMono;

    public SubclassSelectorAttribute(bool includeMono = false)
    {
        m_includeMono = includeMono;
    }

    public bool IsIncludeMono()
    {
        return m_includeMono;
    }
}
SubclassSelectorDrawer.cs
#if UNITY_2019_3_OR_NEWER
using System;
using System.Reflection;
using System.Linq;
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SubclassSelectorAttribute))]
public class SubclassSelectorDrawer : PropertyDrawer
{
    bool initialized = false;
    Type[] inheritedTypes;
    string[] typePopupNameArray;
    string[] typeFullNameArray;
    int currentTypeIndex;

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (property.propertyType != SerializedPropertyType.ManagedReference) return;
        if(!initialized) {
            Initialize(property);
            initialized = true;
        }
        GetCurrentTypeIndex(property.managedReferenceFullTypename);
        int selectedTypeIndex = EditorGUI.Popup(GetPopupPosition(position), currentTypeIndex, typePopupNameArray);
        UpdatePropertyToSelectedTypeIndex(property, selectedTypeIndex);
        EditorGUI.PropertyField(position, property, label, true);
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return EditorGUI.GetPropertyHeight(property, true);
    }

    private void Initialize(SerializedProperty property)
    {
        SubclassSelectorAttribute utility = (SubclassSelectorAttribute)attribute;
        GetAllInheritedTypes(GetFieldType(property), utility.IsIncludeMono());
        GetInheritedTypeNameArrays();
    }

    private void GetCurrentTypeIndex(string typeFullName)
    {
        currentTypeIndex = Array.IndexOf(typeFullNameArray, typeFullName);
    }

    private void GetAllInheritedTypes(Type baseType, bool includeMono)
    {
        Type monoType = typeof(MonoBehaviour);
        inheritedTypes = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(s => s.GetTypes())
            .Where(p => baseType.IsAssignableFrom(p) && p.IsClass && (!monoType.IsAssignableFrom(p) || includeMono))
            .Prepend(null)
            .ToArray();
    }

    private void GetInheritedTypeNameArrays()
    {
        typePopupNameArray = inheritedTypes.Select(type => type == null ? "<null>" : type.ToString()).ToArray();
        typeFullNameArray = inheritedTypes.Select(type => type == null ? "" : string.Format("{0} {1}", type.Assembly.ToString().Split(',')[0], type.FullName)).ToArray();
    }

    public static Type GetFieldType(SerializedProperty property)
    {
        string[] fieldTypename = property.managedReferenceFieldTypename.Split(' ');
        var assembly = Assembly.Load(fieldTypename[0]);
        return assembly.GetType(fieldTypename[1]);
    }

    private void UpdatePropertyToSelectedTypeIndex(SerializedProperty property, int selectedTypeIndex)
    {
        if (currentTypeIndex == selectedTypeIndex) return;
        currentTypeIndex = selectedTypeIndex;
        Type selectedType = inheritedTypes[selectedTypeIndex];
        property.managedReferenceValue =
            selectedType == null ? null : Activator.CreateInstance(selectedType);
    }

    private Rect GetPopupPosition(Rect currentPosition)
    {
        Rect popupPosition = new Rect(currentPosition);
        popupPosition.width -= EditorGUIUtility.labelWidth;
        popupPosition.x += EditorGUIUtility.labelWidth;
        popupPosition.height = EditorGUIUtility.singleLineHeight;
        return popupPosition;
    }
}
#endif

コード内容を簡単に説明すると

  • Assemblyを検索して、インターフェイスや親クラスを実装、継承した子クラスのリストを取得
  • property.managedReferenceFullTypenameでシリアライズされたobjectのTypeを取得
  • ポップアップリストを作り、違うTypeのクラスが選択されたらActivator.CreateInstance()で該当classを生成してproperty.managedReferenceValueに入れる
  • PropertyField()でchildren込みで表示させる

結果

[SubclassSelector]attributeを入れると、
インスペクターから全てのサブクラスを選択できて、
2.png
再生してもちゃんとサブクラスの動きをしています。
3.png
これで簡単にインターフェイス・ライフが始められます!

大したことはしていないですけれど、Unity 2019.3はまだbeta版だったため情報があまり出回らず、調べるのに苦労をしました。
でも、これで便利になるなら、時間をかけて調べる価値はあります。Unityのシリアライズについてめっちゃ勉強になりましたし、とても楽しかったです。

参考

2019.3.0f1(正式リリース候補)にてバグが殆ど解消されました。

今既知の問題は2つあります:

  1. [SerializeReference] List<ISomething>で prefab-asset と prefab-instanceのsizeが違う時、unityがcrashする場合がある
    一時的な解決方法として、prefab上のScriptに直接SerializeReferenceをつけずに、新たにScriptableObjectを作ってField入れるのは良いかなと思います

  2. クラスにシリアライズ可能なフィールドが無いと上手くデシリアライズできない(公式発表済み)
     例えば、下記のクラスはシリアライズ出来ません。

[System.Serializable]
class Someclass : ISomeInterface {
    public DoSomething() { }
}

テスト時に使ったクラスはこちらです。

CommandTestClass.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CommandTestClass : MonoBehaviour
{
    [SerializeReference, SubclassSelector(typeof(ICommand))]
    ICommand command;

    [SerializeReference, SubclassSelector(typeof(ICommand))]
    List<ICommand> commandList = new List<ICommand>();

    void Start()
    {
        command?.Execute();
        foreach(ICommand c in commandList)
            c?.Execute();
    }
}

public interface ICommand
{
    void Execute();
}

[System.Serializable]
public class Command_Empty : ICommand
{
    public void Execute() { }
}

[System.Serializable]
public class Command_Log : ICommand
{
    [SerializeField]
    string text;
    public Command_Log() { text = "default text"; }
    public Command_Log(string logText) { text = logText; }

    public void Execute()
    {
        Debug.Log(text);
    }
}

[System.Serializable]
public class Command_InstantiatePrefab : ICommand
{
    [SerializeField]
    GameObject prefab;
    [SerializeField]
    Vector3 position;

    public void Execute()
    {
        GameObject.Instantiate(prefab, position, Quaternion.identity);
    }
}

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