インスペクターでインターフェースを割り当てる方法

~ 汎用エディターコードを活用したチュートリアル ~


1. はじめに

Unity の標準シリアライズでは、インターフェースや抽象クラスのフィールドは直接扱えません。
しかし、[SerializeReference] 属性を利用することで、これらのフィールドもシリアライズ可能となり、柔軟なポリモーフィズムが実現できます。
本チュートリアルでは、ハイスコア保存機能を例に、複数のクラスで共通に利用できる汎用エディターコードを作成し、インスペクター上でインターフェースの実装クラスを動的に割り当てる方法を解説します。


2. システム構成

本チュートリアルは、以下の 3 つのパートで構成されています。

  1. インターフェースと実装クラスの作成
    • IHighScoreSaver インターフェース
    • 具体的な実装クラス(例:FileHighScoreSaverPlayerPrefsHighScoreSaver
  2. HiScoreManager の作成
    • [SerializeReference] 属性を用いてインターフェースフィールドをシリアライズ
    • ハイスコア保存処理を実装
  3. 汎用エディターコードの作成と適用
    • ジェネリックなカスタムエディター基底クラスを作成
    • 対象クラス(例:HiScoreManager)でフィールド名とインターフェース型を指定するだけで利用可能に

3. ステップバイステップ実装

3.1 インターフェースと実装クラスの作成

まず、ハイスコア保存のためのインターフェースとその実装クラスを作成します。
各実装クラスには [Serializable] 属性を付与して、[SerializeReference] 対象にします。

// IHighScoreSaver.cs
using System;

public interface IHighScoreSaver
{
    void SaveHighScore(int score);
}
// FileHighScoreSaver.cs
using System;
using UnityEngine;

[Serializable]
public class FileHighScoreSaver : IHighScoreSaver
{
    public void SaveHighScore(int score)
    {
        Debug.Log("Score saved to file: " + score);
    }
}
// PlayerPrefsHighScoreSaver.cs
using System;
using UnityEngine;

[Serializable]
public class PlayerPrefsHighScoreSaver : IHighScoreSaver
{
    public void SaveHighScore(int score)
    {
        Debug.Log("Score saved to PlayerPrefs: " + score);
    }
}

3.2 HiScoreManager の作成

次に、[SerializeReference] 属性を利用して、インターフェース型のフィールドをシリアライズする HiScoreManager クラスを作成します。

// HiScoreManager.cs
using UnityEngine;

public class HiScoreManager : MonoBehaviour
{
    [SerializeReference]
    public IHighScoreSaver highScoreSaver;

    public int score = 100;

    // ハイスコア保存を実行するメソッド
    public void SaveScore()
    {
        if (highScoreSaver != null)
        {
            highScoreSaver.SaveHighScore(score);
        }
        else
        {
            Debug.LogWarning("Saver が設定されていません。");
        }
    }
}

3.3 汎用エディターコードの作成

次に、汎用的なカスタムエディター基底クラスを作成します。
このクラスを継承することで、対象クラスのフィールド名とインターフェース型を指定するだけで、インスペクター上に実装クラスの追加・削除 UI を自動生成できます。

using UnityEngine;
using UnityEditor;
using System;
using System.Linq;
using System.Reflection;

// 汎用エディター基底クラス
public abstract class GenericInterfaceEditor<T> : Editor where T : MonoBehaviour
{
    // 派生クラスで対象のフィールド名を指定する
    protected abstract string InterfaceFieldName { get; }
    // 派生クラスで対象のインターフェース型を指定する
    protected abstract Type InterfaceType { get; }

    public override void OnInspectorGUI()
    {
        // デフォルトのインスペクター描画
        DrawDefaultInspector();

        T targetObject = (T)target;
        // 対象のフィールド情報を取得(public/private 両方)
        FieldInfo field = typeof(T).GetField(InterfaceFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        if (field == null)
        {
            EditorGUILayout.HelpBox($"フィールド '{InterfaceFieldName}' が見つかりません。", MessageType.Error);
            return;
        }

        object fieldValue = field.GetValue(targetObject);

        EditorGUILayout.Space();
        EditorGUILayout.LabelField("Interface Assignment", EditorStyles.boldLabel);

        if (fieldValue == null)
        {
            EditorGUILayout.HelpBox("実装クラスが割り当てられていません。", MessageType.Info);
            if (GUILayout.Button("Add Implementation"))
            {
                GenericMenu menu = CreateGenericMenu(InterfaceType, selectedInstance =>
                {
                    field.SetValue(targetObject, selectedInstance);
                    EditorUtility.SetDirty(targetObject);
                });
                menu.ShowAsContext();
            }
        }
        else
        {
            EditorGUILayout.LabelField("Current Implementation:", fieldValue.GetType().Name);
            if (GUILayout.Button("Remove Implementation"))
            {
                field.SetValue(targetObject, null);
                EditorUtility.SetDirty(targetObject);
            }
        }
    }

    // 指定したインターフェースを実装している具象クラスの一覧を GenericMenu として生成
    private GenericMenu CreateGenericMenu(Type interfaceType, Action<object> onSelect)
    {
        GenericMenu menu = new GenericMenu();
        var types = AppDomain.CurrentDomain.GetAssemblies()
                         .SelectMany(assembly => assembly.GetTypes())
                         .Where(type => interfaceType.IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
                         .OrderBy(type => type.Name);

        foreach (var type in types)
        {
            menu.AddItem(new GUIContent(type.Name), false, () => onSelect(Activator.CreateInstance(type)));
        }
        return menu;
    }
}

3.4 HiScoreManager 用のカスタムエディター作成

上記の汎用エディター基底クラスを継承して、HiScoreManager に対するカスタムエディターを作成します。
ここでは、対象フィールド名とインターフェース型を指定するだけです。

using UnityEngine;
using UnityEditor;
using System;

[CustomEditor(typeof(HighScoreManager))]
public class HiScoreManagerEditor : GenericInterfaceEditor<HighScoreManager>
{
    // HiScoreManager の highScoreSaver フィールドを対象とする
    protected override string InterfaceFieldName => "highScoreSaver";
    // 対象のインターフェース型を指定する(例:IHighScoreSaver)
    protected override Type InterfaceType => typeof(IHighScoreSaver);
}

4. 使用方法

4.1 スクリプトの配置

  • Runtime 用スクリプト
    以下のファイルを Assets/Scripts/ などのフォルダに配置します。
    • IHighScoreSaver.cs
    • FileHighScoreSaver.cs
    • PlayerPrefsHighScoreSaver.cs
    • HiScoreManager.cs
  • エディター用スクリプト
    以下のファイルを Assets/Editor/ フォルダに配置します。
    • GenericInterfaceEditor.cs
    • HiScoreManagerEditor.cs

4.2 シーンへの配置と操作

  1. シーンにオブジェクト配置
    空の GameObject を作成し、HiScoreManager コンポーネントを追加します。
  2. インスペクターで実装の割り当て
    • HiScoreManager のインスペクターに「Interface Assignment」セクションが表示されます。
    • 「Add Implementation」ボタンをクリックすると、利用可能な Saver クラス(例:FileHighScoreSaver、PlayerPrefsHighScoreSaver)の一覧がポップアップ表示されます。
    • 任意の Saver を選択すると、そのインスタンスが生成され、highScoreSaver フィールドに割り当てられます。
    • 割り当て済みの場合は、現在の実装クラス名が表示され、「Remove Implementation」ボタンで解除できます。
  3. 動作確認
    ゲーム実行中に HiScoreManager の SaveScore() メソッドを呼び出すことで、設定された Saver によるハイスコア保存処理が実行され、 Console にログが表示されます。

5. まとめ

本チュートリアルでは、汎用エディターコードを活用して、[SerializeReference] 属性でシリアライズされたインターフェースフィールドに対し、
インスペクター上で実装クラスを動的に割り当てる方法を解説しました。

  • 共通処理の再利用: 汎用エディター基底クラスにより、複数のクラスで同じ実装割り当て処理を利用可能
  • リフレクションの活用: 利用可能な具象クラスを動的に取得し、ユーザーに選択させる仕組みを実現
  • 実用例の確認: HiScoreManager を例に、シーンへの配置から実行時の保存処理までを詳細に解説

この手法を活用することで、柔軟なポリモーフィズムやプラグイン形式の拡張が容易となり、プロジェクト全体のコードの再利用性が向上します。