11
9

More than 3 years have passed since last update.

SerializeFieldでインターフェイスを渡す方法を考えた【MonoBehaviourもOK】

Last updated at Posted at 2020-08-25

UnityのSerializeFieldは便利ですが、普通の方法ではインターフェイスを渡すことができないんですよね。
これではせっかくインターフェイスを作ってもクラス直渡しになってしまい悲しいです。
この記事では、SerializeFieldでインターフェイスを無理やり渡す方法を紹介します!

※Qiita初投稿です!
もともと個人ブログに書いてた内容なのですが、ニッチな内容はQiitaに書くことにしました。
これからどんどんQiitaに記事移行していきますのでよろしくおねがいします。

やりたいこと

次のような何らかのコンポーネントがあります。


public class SomeComponent : MonoBehaviour
{
    public void SomeMethod()
    {
        print("SomeMethodが実行されました!");
    }

    //~~~~~~以下他にもいろんな機能がある
    //         ・
    //         ・
    //         ・
}

このコンポーネントを他のコンポーネントにインスペクタを通じて渡して、SomeMethod()だけを呼び出したいとします。
しかし、SomeMethod()しか実行しないのに、他の機能にも全てアクセス可能な状態で渡すのは嫌ですよね?
そこで、次のようなインターフェイスを作成し…

public interface ISomeMethodInvokable
{
    void SomeMethod();
}

コンポーネントに実装させて…

public class SomeComponent : MonoBehaviour, ISomeMethodInvokable { /*省略*/ }

インターフェイスを通じてコンポーネントの参照を渡したいです。

public class SomeMethodInvoker : MonoBehaviour
{
    [SerializeField]
    ISomeMethodInvokable m_someMethodInvokable;

    private void Start() 
    {
        m_someMethodInvokable.SomeMethod();
    }
}

しかし、みなさんご存知の通りこれは通常の方法ではできません。
ではどうするか?と、検索すると主に次の2つの方法が出てきます。

検索すると出てくる「インスペクタでInterfaceを渡す方法」

インスペクタからインターフェイスを渡す方法は、検索すれば主に次の2つの方法が出てきます。

  • SerializeReference属性を使う方法
  • 有料アセット「Odin」を使う方法

SerializeReference属性を使う方法

一つ目は、SerializeReference属性を使う方法です。

SerializeField属性の代わりに、このSerializeReference属性を使うと、インターフェイスや抽象クラスなどもシリアライズできるそうなのですが、、、
なんとMonoBehaviourを継承したクラス(つまりコンポーネント)をシリアライズすることはできません。

今回はコンポーネントに実装したインターフェイスを渡すことが目的なので、これはボツに。

有料アセット「Odin」を使う方法

二つ目は、有料アセット「Odin」を使う方法です。

有料アセット「Odin」を使えば、インスペクタからMonoBehaviourを継承したクラスをインターフェイス経由で渡すことができるそうです。
つまり、このアセットさえ購入すれば問題は万事解決します。

ところが、お値段が55ドル程度。うーん、微妙に高い…。

どちらも微妙…

というわけで、どちらも微妙です。
なんとか自力で実現できないかと考えた結果、以下の方法を生み出しました。

自力で考えた「インスペクタでInterfaceを渡す方法」

通常の方法ではやっぱり何をやってもインスペクタからインターフェイスを渡すことはできません。

しかし、そもそもの目的を考えてみると、したいことは「インスペクタでインターフェイスを渡すこと」ではなくて「インターフェイス経由でコンポーネントにアクセスしてもらうこと」ですよね?
であれば、インターフェイスを実装したコンポーネントをラップし、インターフェイス型でしか公開しないクラスを渡せば良いのでは?という発想に至りました。

以下に手順を示します。

GameObjectをラップしてInterfaceのみを返すクラスを作成

次のような汎用クラスを作成します。


[Serializable]
public class SerializeInterface<TInterface>
{
    [SerializeField]
    private GameObject m_gameobject;

    private TInterface m_interface;

    public TInterface Interface
    {
        get
        {
            if(m_interface == null)
            {
                m_interface = m_gameobject.GetComponent<TInterface>();
            }
            return m_interface;
        }
    }
}

SerializeFieldがついているGameObject型のprivateフィールドが、実際にインスペクタと紐付けされる部分になります。

インターフェイスはSerializeFieldで渡すことができないため、結局はGameObjectを渡すことになってしまうのですが、ジェネリクスでインターフェイスを指定するようになっていて、クラス外部に公開されるのはこのインターフェイス型となります。GameObjectそのものは内部に隠蔽されるところがミソですね。

作成したクラスを使ってインターフェイスをインスペクタから渡す

では、先ほど作成したクラスにSerializeFieldを適用して、実際にインスペクタからインターフェイスを渡してみましょう。

ところが、残念ながらジェネリクスクラスはSerializeFieldしても無視されてしまいます。そこで、非ジェネリクス版のクラスを内部クラスとして定義し、そのクラスに対してSerializeFieldを適用します。


public class SomeMethodInvoker : MonoBehaviour
{
    [SerializeField]
    SerializeISomeMethodInvokable m_someMethodInvokable;

    [Serializable]
    private class SerializeISomeMethodInvokable : SerializeInterface<ISomeMethodInvokable> { }
}

繰り返しになりますが、先ほど作成したSerializeInterfaceクラスはGameObjectを内部に持ちますが、インターフェイスのみを外部に公開します。したがって、使う側のコードはインターフェイスからしかSerializeFieldで引き渡されたGameObjectにアクセスすることができません

実際にインターフェイスにアクセスしてみましょう。


public class SomeMethodInvoker : MonoBehaviour
{
    [SerializeField]
    SerializeISomeMethodInvokable m_someMethodInvokable;

    private void Start() 
    {
        m_someMethodInvokable.Interface.SomeMethod();
    }

    [Serializable]
    private class SerializeISomeMethodInvokable : SerializeInterface<ISomeMethodInvokable> { }
}

Startメソッド内でインターフェイスにアクセスしています。Interfaceプロパティからしか内部にアクセスできないため、必ずインターフェイスを通じてアクセスすることになります
インターフェイスそのものをSerializeFieldで渡したわけではありませんが、「インターフェイスを通じてアクセスする」という当初の目的は達成されたのではないでしょうか。

使ってみる

では、実際にGameObjectにアタッチしてインスペクタからインターフェイスを渡してみましょう。

まずはインターフェイスを受け取る側の準備をします。
GameObjectにSomeMethodInvokerをアタッチすると、次のようになります。

image.png

GameObjectが指定できるようになっています。

では、このフィールドにISomeMethodInvokableインターフェイスを実装したコンポーネント(がアタッチされたGameObject)をアタッチしてみます。

image-1.png

アタッチしました。実行してみます。

image-2.png

きちんとインターフェイスを通じて別のコンポーネントのメンバにアクセスすることができました!

指定のインターフェイスを実装していない場合

指定のインターフェイスを実装していないコンポーネントを渡して実行すると、次のようなエラーが発生します。

image-3-1024x54.png

SerializeInterfaceクラス内でインターフェイスをGetComponentしている部分でnullが返るためですね。
以下のようなコードにすると次のような的確なエラーメッセージを表示させることができます。


[Serializable]
public class SerializeInterface<TInterface>
{
    [SerializeField]
    private GameObject m_gameobject;

    private TInterface m_interface;

    public TInterface Interface
    {
        get
        {
            if(m_interface == null)
            {
                m_interface = m_gameobject.GetComponent<TInterface>();
                if (m_interface == null)
                {
                    throw new Exception($"GameObject\"{m_gameobject.name}\"は{typeof(TInterface).Name}を実装したコンポーネントをアタッチしていません");
                }
            }
            return m_interface;
        }
    }
}

image-5-1024x52.png

より的確なエラーになり分かりやすくなりました!

まとめ

以上から、面倒ではありますが自力でもインスペクタでインターフェイスを渡すことができると分かりました!

ただし、SerializeInterfaceの非ジェネリクス版を毎回毎回泣きながら定義しなければならなかったり、例外で落とすんじゃなくてそもそもインスペクタでインターフェイス実装してないのを弾いて欲しいとか色々不満はありますが、とりあえずこれで頑張ってみたいと思います。
最後までありがとうございました!

11
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9