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をアタッチすると、次のようになります。
GameObjectが指定できるようになっています。
では、このフィールドにISomeMethodInvokableインターフェイスを実装したコンポーネント(がアタッチされたGameObject)をアタッチしてみます。
アタッチしました。実行してみます。
きちんとインターフェイスを通じて別のコンポーネントのメンバにアクセスすることができました!
###指定のインターフェイスを実装していない場合
指定のインターフェイスを実装していないコンポーネントを渡して実行すると、次のようなエラーが発生します。
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;
}
}
}
より的確なエラーになり分かりやすくなりました!
#まとめ
以上から、面倒ではありますが自力でもインスペクタでインターフェイスを渡すことができると分かりました!
ただし、__SerializeInterfaceの非ジェネリクス版を毎回毎回泣きながら定義__しなければならなかったり、例外で落とすんじゃなくて__そもそもインスペクタでインターフェイス実装してないのを弾いて欲しい__とか色々不満はありますが、とりあえずこれで頑張ってみたいと思います。
最後までありがとうございました!