【Unity】Interface型をアウトレット接続したい

Unity では、Interface(インターフェース)型のアウトレット接続は直接サポートされていません。インターフェースは実際のインスタンスではなく、型の契約を定義するものであるため、Unity のインスペクターで直接インターフェース型を扱うことはできません。ただし、いくつかの回避策があります。

期待すること

インターフェース型を実装したスクリプトの依存を登録したい

Unityエディタの仕様では、ドラッグ&ドロップができません

実行動画

スクリプト

public interface IAnimal
{

}
using UnityEngine;

public class Dog : MonoBehaviour, IAnimal
{

}
using UnityEngine;

public class Game : MonoBehaviour
{
    public IAnimal animal;
}

代替え方法

  1. 抽象クラスを使用する: インターフェースの代わりに抽象クラスを使用することができます。抽象クラスはインスペクターで参照として設定することが可能です。この方法で、必要なメソッドを抽象メソッドとして定義し、実装クラスでオーバーライドします。
  2. シリアライズ可能なラッパークラスの作成: インターフェース型の代わりに、そのインターフェースを実装する具体的なクラスをラップするシリアライズ可能なクラスを作成します。このラッパークラスはインターフェースへの参照を内部に保持し、インスペクター上で設定可能です。
  3. カスタムエディタ拡張を使用する: Unity のエディタ拡張機能を利用して、インターフェース型のフィールドに対応するカスタムエディタを作成します。このエディタでは、インターフェースを実装するオブジェクトをドロップダウンリストなどから選択できるようにすることが可能です。
  4. シリアライズフィールドとして非表示にする: SerializeField 属性を使用し、インターフェース型ではなく、具体的なクラス型をインスペクタに表示します。実行時にこれをインターフェース型にキャストして使用します。

これらの方法を使うことで、Unity でインターフェース型を柔軟に扱うことができるようになります。具体的な実装方法やサンプルコードが必要であれば、詳しく説明しますのでお知らせください。

具体的な実装

1. 抽象クラスを使用する

抽象クラスIAnimalとその具体的な実装であるDogクラスの例を以下に示します。

using UnityEngine;

// 抽象クラス定義
public abstract class Animal : MonoBehaviour
{
    public abstract void Speak();
}
// 抽象クラスを継承した具体的なクラス
public class Dog : Animal
{
    public override void Speak()
    {
        Debug.Log("わん!");
    }
}

Unity エディタで、Animal型のフィールドを持つ別のスクリプトを作成し、インスペクターからDogオブジェクトをアサインできます。

using UnityEngine;

public class Game : MonoBehaviour
{
    public Animal myAnimal;

    private void Start()
    {
        myAnimal.Speak();
    }
}

2. シリアライズ可能なラッパークラスの作成

インターフェースIAnimalとそのラッパークラスの例を示します。

// インターフェース定義
public interface IAnimal
{
    void Speak();
}
using UnityEngine;

// インターフェースを実装する具体的なクラス
public class Dog : MonoBehaviour, IAnimal
{
    public void Speak()
    {
        Debug.Log("わん!");
    }
}
// インターフェースのラッパーとなるクラス
[System.Serializable]
public class AnimalWrapper 
{
    public IAnimal animal;
}

この例では、AnimalUser スクリプトの中で MonoBehaviour 型の animalAsMonoBehaviour フィールドを持ち、スタート時に IAnimal インターフェースにキャストして使用します。この方法で、インスペクターから任意の MonoBehaviour を割り当てることができ、実行時に IAnimal インターフェースを実装しているかどうかをチェックします。

using UnityEngine;

public class Game : MonoBehaviour
{
    public MonoBehaviour animalAsMonoBehaviour;

    private void Start()
    {
        IAnimal animal = animalAsMonoBehaviour as IAnimal;

        if (animal != null)
        {
            animal.Speak();
        }
        else
        {
            Debug.LogError("割り当てられたコンポーネントは IAnimal を実装していない。");
        }
    }
}

3. エディタ拡張

// インターフェース定義
public interface IAnimal
{
    void Speak();
}
using UnityEngine;

// インターフェースを実装する具体的なクラス
public class Dog : MonoBehaviour, IAnimal
{
    public void Speak()
    {
        Debug.Log("わん!");
    }
}
using UnityEngine;

public class Game : MonoBehaviour
{
    [SerializeField] private MonoBehaviour animalAsMonoBehaviour; // インスペクターから設定可能にする
    private IAnimal animal; // プライベート変数として扱う

    void Start()
    {
        // MonoBehaviourをIAnimalインターフェースにキャスト
        animal = animalAsMonoBehaviour as IAnimal;

        // キャストが成功しているか確認
        if (animal != null)
        {
            animal.Speak();
        }
        else
        {
            Debug.LogError("The assigned component does not implement IAnimal");
        }
    }
}

Editorフォルダを作成して、保存します

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Game))]
public class AnimalUserEditor : Editor
{
    SerializedProperty animalAsMonoBehaviourProp;

    void OnEnable()
    {
        // SerializedProperty を取得
        animalAsMonoBehaviourProp = serializedObject.FindProperty("animalAsMonoBehaviour");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update(); // オブジェクトの最新情報を読み込み

        EditorGUILayout.PropertyField(animalAsMonoBehaviourProp, new GUIContent("Animal as MonoBehaviour"));

        serializedObject.ApplyModifiedProperties(); // 変更を適用

        // 追加のチェックを行う(オプション)
        if (animalAsMonoBehaviourProp.objectReferenceValue != null && !(animalAsMonoBehaviourProp.objectReferenceValue as MonoBehaviour is IAnimal))
        {
            EditorGUILayout.HelpBox("割り当てられたコンポーネントは IAnimal を実装していない。", MessageType.Error);
        }
    }
}

このエディタスクリプトでは、OnEnable メソッドで SerializedProperty を取得し、OnInspectorGUI でこのプロパティを表示・編集できるようにしています。また、適切なインターフェースを実装しているかどうかの追加チェックも含めることができます。

この方法で、AnimalUser のインスペクタ表示をカスタマイズしながら、private フィールドへのアクセスを安全に管理することができます。

4. シリアライズフィールドとして非表示にする

インターフェースIAnimalを実装するDogクラスと、その使用例を以下に示します。

public interface IAnimal
{
    void Speak();
}
public class Dog : MonoBehaviour, IAnimal
{
    public void Speak()
    {
        Debug.Log("Bark");
    }
}
using UnityEngine;

public class Game : MonoBehaviour
{
    [SerializeField] private MonoBehaviour _animal;
    private IAnimal myAnimal;

    void Start()
    {
        myAnimal = _animal as IAnimal;
        myAnimal?.Speak();
    }
}

これらのサンプルを参考にして、プロジェクトの要件に応じて適切な方法を選択してください。それぞれのアプローチには利点と制限がありますので、プロジェクトのニーズに合わせて選択することが重要です。

ダメージを受けることができるゲームオブジェクトに対応させてみる

ダメージを受ける機能を実装するためのサンプルを、先ほど説明した4つの方法に基づいて提供します。

1. 抽象クラスを使用する

抽象クラスIDamageableとその具体的な実装であるPlayerクラスの例を以下に示します。

using UnityEngine;

// 抽象クラス定義
public abstract class IDamageable : MonoBehaviour
{
    public abstract void TakeDamage(int amount);
}
using UnityEngine;

// 抽象クラスを継承した具体的なクラス
public class Player : IDamageable
{
    public override void TakeDamage(int amount)
    {
        Debug.Log($"プレイヤーは{amount}のダメージを受けた!");
        // ここにダメージ処理のロジックを追加
    }
}
using UnityEngine;

public class Game : MonoBehaviour
{
    public IDamageable damageableTarget;

    void Start()
    {
        damageableTarget.TakeDamage(10);
    }
}

2. シリアライズ可能なラッパークラスの作成

インターフェースIDamageableとそのラッパークラスの例を示します。

// インターフェース定義
public interface IDamageable
{
    void TakeDamage(int amount);
}
using UnityEngine;

// インターフェースを実装する具体的なクラス
public class Player : MonoBehaviour, IDamageable
{
    public void TakeDamage(int amount)
    {
        Debug.Log($"プレイヤーは{amount}のダメージを受けた!");
    }
}
// インターフェースのラッパーとなるクラス
[System.Serializable]
public class DamageableWrapper
{
    public IDamageable damageable;
}

この例では、AnimalUser スクリプトの中で MonoBehaviour 型の animalAsMonoBehaviour フィールドを持ち、スタート時に IAnimal インターフェースにキャストして使用します。この方法で、インスペクターから任意の MonoBehaviour を割り当てることができ、実行時に IAnimal インターフェースを実装しているかどうかをチェックします。

using UnityEngine;

public class Game : MonoBehaviour
{
    public MonoBehaviour animalAsMonoBehaviour;

    private void Start()
    {
        IDamageable damageableTarget = animalAsMonoBehaviour as IDamageable;

        if (damageableTarget != null)
        {
            damageableTarget.TakeDamage(10);
        }
        else
        {
            Debug.LogError("割り当てられたコンポーネントは IAnimal を実装していない。");
        }
    }
}

3. エディタ拡張

// インターフェース定義
public interface IDamageable
{
    void TakeDamage(int amount);
}
using UnityEngine;

// インターフェースを実装する具体的なクラス
public class Player : MonoBehaviour, IDamageable
{
    public void TakeDamage(int amount)
    {
        Debug.Log($"プレイヤーは{amount}のダメージを受けた!");
    }
}
using UnityEngine;

public class Game : MonoBehaviour
{
    [SerializeField] private MonoBehaviour iDamageableAsMonoBehaviour; // インスペクターから設定可能にする
    private IDamageable damageableTarget; // プライベート変数として扱う

    void Start()
    {
        // MonoBehaviourをIDamageableインターフェースにキャスト
        damageableTarget = iDamageableAsMonoBehaviour as IDamageable;

        // キャストが成功しているか確認
        if (damageableTarget != null)
        {
            damageableTarget.TakeDamage(10);
        }
        else
        {
            Debug.LogError("割り当てられたコンポーネントは IAnimal を実装していない。");
        }
    }
}

Editorフォルダを作成して、保存します

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Game))]
public class DamageableEditor : Editor
{
    SerializedProperty iDamageableAsMonoBehaviourProp;

    void OnEnable()
    {
        // SerializedProperty を取得
        iDamageableAsMonoBehaviourProp = serializedObject.FindProperty("iDamageableAsMonoBehaviour");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update(); // オブジェクトの最新情報を読み込み

        EditorGUILayout.PropertyField(iDamageableAsMonoBehaviourProp, new GUIContent("iDamageable As MonoBehaviour"));

        serializedObject.ApplyModifiedProperties(); // 変更を適用

        // 追加のチェックを行う(オプション)
        if (iDamageableAsMonoBehaviourProp.objectReferenceValue != null && !(iDamageableAsMonoBehaviourProp.objectReferenceValue as MonoBehaviour is IDamageable))
        {
            EditorGUILayout.HelpBox("割り当てられたコンポーネントは IDamageable を実装していない。", MessageType.Error);
        }
    }
}

このエディタスクリプトでは、OnEnable メソッドで SerializedProperty を取得し、OnInspectorGUI でこのプロパティを表示・編集できるようにしています。また、適切なインターフェースを実装しているかどうかの追加チェックも含めることができます。

この方法で、DamageableEditor のインスペクタ表示をカスタマイズしながら、private フィールドへのアクセスを安全に管理することができます。

4. シリアライズフィールドとして非表示にする

インターフェースIAnimalを実装するDogクラスと、その使用例を以下に示します。

public interface IDamageable
{
    void TakeDamage(int amount);
}
using UnityEngine;

public class Player : MonoBehaviour, IDamageable
{
    public void TakeDamage(int amount)
    {
        Debug.Log($"プレイヤーは{amount}のダメージを受けた!");
    }
}
using UnityEngine;

public class Game : MonoBehaviour
{
    [SerializeField] private MonoBehaviour _animal;
    private IAnimal myAnimal;

    void Start()
    {
        myAnimal = _animal as IAnimal;
        myAnimal?.Speak();
    }
}

これらのサンプルを参考にして、プロジェクトの要件に応じて適切な方法を選択してください。それぞれのアプローチには利点と制限がありますので、プロジェクトのニーズに合わせて選択することが重要です。

Interface型をインスペクターのアウトレット接続することの利点、欠点

Unityでインターフェース型のアウトレット接続(つまり、インスペクタでインターフェースを直接設定すること)を使うことの利点と欠点を考えてみましょう。

利点

  1. 柔軟性: インターフェースを利用することで、コンポーネント間の依存性を低減し、より柔軟なコード設計が可能になります。異なるクラスが同じインターフェースを実装していれば、それらを簡単に入れ替えることができ、テストや拡張が容易になります。
  2. 再利用性: 同じインターフェースを実装する異なるコンポーネントを、同じ方法で使用できるため、コードの再利用性が向上します。
  3. 抽象化: インターフェースを用いることで、具体的な実装から抽象化を行い、より高いレベルでのプログラミングが可能になります。これにより、実装の詳細を気にすることなく、設計に集中できます。

欠点

  1. Unityの制限: Unityでは、インターフェース型を直接インスペクタに表示させることができないため、カスタムエディタ拡張などを作成する追加の労力が必要になります。
  2. 複雑性の増加: インターフェースを多用することで、プロジェクトの複雑性が増すことがあります。特に、インターフェースの背後にある具体的な実装を理解していないと、デバッグが困難になることがあります。
  3. パフォーマンスの考慮: インターフェースを使用すると、動的な型チェックやキャストが必要になる場合があり、これがパフォーマンスに影響を与えることがあります。特に、ゲームの実行時に頻繁にアクセスされるコードでの影響が顕著になることがあります。

結論

インターフェースを使用するかどうかは、プロジェクトの具体的な要件と、開発チームのスキルレベルに依存します。もしコードの拡張性や再利用性を重視するのであれば、インターフェースの利用を考慮するべきです。一方で、実装の簡潔さやパフォーマンスが重要な場合は、直接的なクラスの利用が望ましいかもしれません。

カスタムエディタ拡張やラッパークラスの利用など、Unityでインターフェースを効率的に扱う方法を探求することも一つの解決策です。プロジェクトのニーズに合わせて、最適なアプローチを選択しましょう。

Unity

Posted by hidepon