はじめに
C#をはじめとするオブジェクト指向言語では、クラス同士が直接アクセスするのを避け、インターフェイスを介してアクセスさせることで疎結合が実現されることは周知の事実です。そのため、__UnityC#でもMonoBehaviour
同士をインターフェイスを介して依存させたい!でもMonoBehaviour
の制約のせいでうまくいかない!__って方は割と多いのではないでしょうか。
例えば、以下のようにFugaComponent
がHogeComponent
のインターフェイスIHogeComponent
に依存したいとき。
public interface IHogeComponent
{
void SayHoge();
}
public class HogeComponent : MonoBehaviour, IHogeComponent
{
public void SayHoge()
{
print("Hoge");
}
}
public class FugaComponent : MonoBehaviour
{
// 疎結合のためインターフェイスを通じてアクセスさせたい。
private IHogeComponent m_hoge; //←このm_hoge、どこから持ってくる!?
void Start()
{
m_hoge.SayHoge();
}
}
FugaComponent
はインターフェイスIHogeComponent
をフィールドに持つことでHogeComponent
とFugaComponent
との結合度を弱めているのですが、m_hoge
の中身はどうやって代入するのかはなかなか難しい問題です。というのも、
-
SerializeField
属性をつけてインスペクタから代入しては?
→インターフェイスはシリアライズできません! -
コンストラクタから渡す?
→MonoBehaviour
なクラスの場合はコンストラクタ作れません! -
GetComponent<T>
からインターフェイス取得しては?
→同じGameObjectにアタッチされているコンポーネント同士ならOKだけど、違う場合はGameObjectへの参照が必要になって美しくないし安全性も落ちる!
このような問題が発生するからです。
そこで本記事では、上記のような問題を解決するべく、MonoBehaviour
なクラスにインターフェイスを引き渡す方法を2つ紹介します。
方法1. Odin Inspectorを使ってインターフェイスをシリアライズする
通常、次のようにインターフェイスをSerializeFieldしてもインスペクタ上には表示されません。
public class FugaComponent : MonoBehaviour
{
[SerializeField]
private IHogeComponent m_hoge;
}
▲インターフェイスをSerializeFieldしても…
ところが、__Odin Inspectorというアセットに含まれているSerializedMonoBehaviour
クラス__を使うと、インスペクタ上でインターフェイスを引き渡すことができるようになります。
public class FugaComponent : SerialiedMonoBehaviour
{
[SerializeField]
private IHogeComponent m_hoge;
}
▲MonoBehaviour
の代わりにSerializedMonoBehaviour
を継承させると…
この方法であれば、__インスペクタ上でもIHogeComponent
を実装したコンポーネントがアタッチされたGameObjectしかセットできない__ですし、ソースコード上にも余計なメンバが現れないため、安全かつ美しいです。
ただし、Odin Inspectorは__60.50ドルの有料アセット__です(たまに半額セールもある)。
お金をかけたくない場合は、次に紹介するまったく異なるアプローチを取ることもできます。
方法2. Zenjectを使ってインターフェイスを注入する
Zenject(Extenject)は、UnityC#で__Dependency Injection(DI、依存性の注入)1__を実現する__無料アセット__です。
DI(依存性の注入)は簡単に言うと、クラス同士の依存関係を設定しておくと、必要な時に必要なオブジェクトを引き渡してくれる(=注入してくれる)仕組みです。
Zenjectの使い方は複雑なので、順を追って説明します。
注入してほしいインターフェイスにInject
属性をつける
インスペクタからオブジェクトを引き渡す際はSerializeField
属性を付けますが、Zenjectによってオブジェクトを注入する際は__Inject
属性__を付けます。
public class FugaComponent : MonoBehaviour
{
[Inject]
private IHogeComponent m_hoge;
}
ちょうど、SerializeField
の代わりにInject
を付けるイメージです。こうすることで、__Zenjectに対してIHogeComponent
を注入して欲しいです!という意思表示__ができます。
依存関係定義ファイルMonoInstaller
を作成する
インターフェイスを注入すると言っても、実際に注入されるのは具体的なオブジェクトのインスタンスです。とあるインターフェイスの注入を要求されたとき、__具体的にはどんなオブジェクトを注入すれば良いのか__をZenjectに対して設定する必要があります。この設定ファイルを__Installer__と言います。
Projectビューのプラスボタンをクリックして、Zenject→Mono Installerをクリックします。任意の名前をつけてファイルを保存します。
作成されたファイルには、次のようなコードが記載されています。
using UnityEngine;
using Zenject;
public class UntitledInstaller : MonoInstaller
{
public override void InstallBindings()
{
}
}
このInstallBindings
メソッド内に、「どのインターフェイスが要求されたらどのオブジェクトを注入するか」を定義していきます。
例えば、IHogeComponent
インターフェイスが要求されたらHogeComponent
を注入するように設定する場合は、次のようにします。※あくまで一例です。
using UnityEngine;
using Zenject;
public class UntitledInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container
.Bind<IHogeComponent>()
.To<HogeComponent>()
.FromComponentInHierarchy()
.AsCached();
}
}
まずメソッド内1行目のContainer
ですが、これはDiContainer
クラスのインスタンスです。DiContainer
は設定通りにオブジェクトを注入してくれる核となる存在です。これからこのDiContainer
に対してさまざまな依存関係の設定を行っていきます。
2行目のBind<T>
メソッドは「このインターフェイスが要求されたら」という意味です。DiContainer
に対して、IHogeComponent
が要求されたら(Inject
されたら)、なんらかのオブジェクトを注入してね、と設定します。
3行目のTo<T>
メソッドはBind<T>
で設定されたインターフェイスが要求されたときに、__実際に注入するオブジェクト__を設定します。この例では、IHogeComponent
が要求されたときにHogeComponent
を注入するように設定しています。
4行目のFromComponentInHierarchy
メソッドは、「注入するオブジェクトはヒエラルキー上から持ってきてね」と設定しています。
もちろんこれは一例であり、注入するオブジェクトはさまざまな場所から持ってくることができます。詳しくはこちらのサイトが参考になります。
5行目のAsCached
メソッドは、注入オブジェクトをキャッシュして再利用するように設定しています。例えば、IHogeComponent
が複数のクラスから要求された場合、2回目以降はヒエラルキー上から持ってくるのではなく、キャッシュされたオブジェクトをそのまま注入します。
他にもAsSingle
やAsTransient
などがあります。詳しくはこちらのサイトが参考になります。
以上で、ひとまず依存関係の設定は完了しました。
Contextを用意してInstallerをセットする
最後に、作成したInstallerの影響範囲を設定します。これには、__Context__というものを使用します。
Contextにも様々ありますが、ここではひとつのシーン全体に影響する__SceneContext__を使用します。
もし、Contextに対して詳しく知りたい場合は、以下のようなサイトが参考になります。
ヒエラルキービューのプラスボタンをクリックして、Zenject→Scene Contextをクリックします。
すると、シーン上にSceneContextコンポーネントがアタッチされたGameObjectが作成されると思います。
SceneContextコンポーネントのMonoInstallers
の部分に、先ほど作成したInstallerをセットします。
Add Componentボタンを押して先ほどのInstallerをアタッチし、それをMonoInstallersにドラッグしてセットできます。
以上で、SceneContextに対してInstallerのセットが完了しました。
これで晴れて、このSceneContextの影響範囲に対して、セットしたInstallerの設定内容に従って、オブジェクトの注入が実行されるようになります。
Scene上に注入を要求するコンポーネントを配置する
SceneContextの影響範囲内(同一Scene内)に、オブジェクト注入を要求するコンポーネントを配置します。また、今回の例ではFromComponentInHierarchy
に設定しているため、ヒエラルキ上に注入されるオブジェクト(コンポーネント)も配置されている必要があります。
次のように、Scene上に注入されるHogeComponent
、注入を要求するFugaComponent
をそれぞれアタッチしたGameObjectを配置します。
動作確認用に、HogeComponent
とIHogeComponent
を以下のように実装しました。
public interface IHogeComponent
{
void SayHoge();
}
public class HogeComponent : MonoBehaviour, IHogeComponent
{
public void SayHoge()
{
print("Hoge");
}
}
FugaComponent
はIHogeComponent
の注入を要求し、SayHoge()
を実行します。
public class FugaComponent : SerializedMonoBehaviour
{
[Inject]
private IHogeComponent m_hoge;
void Start()
{
m_hoge.SayHoge();
}
}
この状態でPlayすると、コード上ではm_hoge
に対してオブジェクトを代入していないのに、きちんとSayHoge()
が呼べていることが分かります。Zenjectによるオブジェクトの注入が効いている証拠です。
OdinとZenjectの使い分け
以上、UnityC#においてインターフェイスを引き渡す方法を2つ紹介しました。
OdinとZenjectは、片方だけでもインターフェイスを引き渡すという目的は達成できるのですが、どちらにも得意な場面・不得意な場面があると考えています。
例えば、Odinの場合はインスペクタ上で視覚的にオブジェクトの注入ができるため、__同じインターフェイスを実装していても実体が異なる複数のインスタンスを注入したい場合__に向いています。
▲異なる複数のインスタンスを視覚的に注入できる
これをZenjectで行うには、Installerの設定時にIDを使った注入設定を細かく行う必要があるため、設定と管理が煩雑になります。
反対にゲームのManager的なクラスなど、__ただひとつのインスタンスしか持たないオブジェクトを注入したい__場合、インスペクタ上で手動で注入するよりも、依存関係を設定してZenjectにやってもらったほうが楽です。
このように、OdinとZenjectは得意不得意があると思いますので、__結局は両方とも導入して、適材適所で使い分ける__のが一番良いのではないかと考えています。
Odinはインターフェイス注入以外にもインスペクタを使いやすくする機能が揃ってますし、DIはUnityだけではなく様々な場面で使われるデザインパターンです。どちらも勉強しておいて損はないかと思います(自分も勉強中)。
番外編. どうしてもOdinを買わずにインスペクタでインターフェイスを渡したい場合
昔の自分がOdinを使わずに無理矢理インスペクタでインターフェイスを(擬似的に)渡す方法を考えてました。
ご興味ある方は下記参照ください。
-
この記事はZenject, DI(依存性の注入)について詳しく解説する記事ではありません!正確性に欠ける表現がある可能性があることをご了承ください。 ↩