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

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

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

#やりたいこと
次のような何らかのコンポーネントがあります。

```cs

public class SomeComponent : MonoBehaviour
{
    public void SomeMethod()
    {
        print("SomeMethodが実行されました！");
    }
    
    //~~~~~~以下他にもいろんな機能がある
    //         ・
    //         ・
    //         ・
}
```
このコンポーネントを他のコンポーネントにインスペクタを通じて渡して、__SomeMethod()だけを呼び出したい__とします。
しかし、__SomeMethod()しか実行しないのに、他の機能にも全てアクセス可能な状態で渡すのは嫌ですよね？__
そこで、次のようなインターフェイスを作成し…

```csharp
public interface ISomeMethodInvokable
{
    void SomeMethod();
}
```
コンポーネントに実装させて…

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

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

```csharp
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のみを返すクラスを作成

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

```csharp

[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を適用します。

```csharp

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

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

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

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

```csharp

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/74733abf-2e2e-4d56-58c5-e854aa11d3c6.png)

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

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

![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/bf5f0a48-a163-8f37-db83-c9a718832b90.png)

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

![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/bb8c3a1c-6a58-455d-5a15-127bf222bd96.png)

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

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

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

![image-3-1024x54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/f247ada5-0a90-eac5-0a18-8abe968806c7.png)

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

```csharp

[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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/944ad258-e737-181e-1dad-b70d3c72deec.png)

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

#まとめ

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

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