11
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

UnityAdvent Calendar 2021

Day 6

【UnityC#】MonoBehaviourでもインターフェイスを利用して疎結合を実現する2つの方法

Last updated at Posted at 2021-12-05

はじめに

C#をはじめとするオブジェクト指向言語では、クラス同士が直接アクセスするのを避け、インターフェイスを介してアクセスさせることで疎結合が実現されることは周知の事実です。そのため、__UnityC#でもMonoBehaviour同士をインターフェイスを介して依存させたい!でもMonoBehaviourの制約のせいでうまくいかない!__って方は割と多いのではないでしょうか。

例えば、以下のようにFugaComponentHogeComponentのインターフェイス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をフィールドに持つことでHogeComponentFugaComponentとの結合度を弱めているのですが、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しても…

image.png
▲インスペクタ上には表示されない

ところが、__Odin Inspectorというアセットに含まれているSerializedMonoBehaviourクラス__を使うと、インスペクタ上でインターフェイスを引き渡すことができるようになります。

public class FugaComponent : SerialiedMonoBehaviour
{
    [SerializeField]
    private IHogeComponent m_hoge;
}

MonoBehaviourの代わりにSerializedMonoBehaviourを継承させると…

image.png
▲インターフェイスもインスペクタ上に現れる!

この方法であれば、__インスペクタ上でも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をクリックします。任意の名前をつけてファイルを保存します。
image.png

作成されたファイルには、次のようなコードが記載されています。

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回目以降はヒエラルキー上から持ってくるのではなく、キャッシュされたオブジェクトをそのまま注入します。
他にもAsSingleAsTransientなどがあります。詳しくはこちらのサイトが参考になります。

以上で、ひとまず依存関係の設定は完了しました。

Contextを用意してInstallerをセットする

最後に、作成したInstallerの影響範囲を設定します。これには、__Context__というものを使用します。
Contextにも様々ありますが、ここではひとつのシーン全体に影響する__SceneContext__を使用します。

もし、Contextに対して詳しく知りたい場合は、以下のようなサイトが参考になります。

ヒエラルキービューのプラスボタンをクリックして、Zenject→Scene Contextをクリックします。

image.png

すると、シーン上にSceneContextコンポーネントがアタッチされたGameObjectが作成されると思います。
image.png

SceneContextコンポーネントのMonoInstallersの部分に、先ほど作成したInstallerをセットします。
Add Componentボタンを押して先ほどのInstallerをアタッチし、それをMonoInstallersにドラッグしてセットできます。
image.png

以上で、SceneContextに対してInstallerのセットが完了しました。
これで晴れて、このSceneContextの影響範囲に対して、セットしたInstallerの設定内容に従って、オブジェクトの注入が実行されるようになります。

Scene上に注入を要求するコンポーネントを配置する

SceneContextの影響範囲内(同一Scene内)に、オブジェクト注入を要求するコンポーネントを配置します。また、今回の例ではFromComponentInHierarchyに設定しているため、ヒエラルキ上に注入されるオブジェクト(コンポーネント)も配置されている必要があります。

次のように、Scene上に注入されるHogeComponent、注入を要求するFugaComponentをそれぞれアタッチしたGameObjectを配置します。

image.png

動作確認用に、HogeComponentIHogeComponentを以下のように実装しました。

public interface IHogeComponent
{
    void SayHoge();
}
public class HogeComponent : MonoBehaviour, IHogeComponent
{
    public void SayHoge()
    {
        print("Hoge");
    }
}

FugaComponentIHogeComponentの注入を要求し、SayHoge()を実行します。

public class FugaComponent : SerializedMonoBehaviour
{
    [Inject]
    private IHogeComponent m_hoge;

    void Start()
    {
        m_hoge.SayHoge();    
    }
}

この状態でPlayすると、コード上ではm_hogeに対してオブジェクトを代入していないのに、きちんとSayHoge()が呼べていることが分かります。Zenjectによるオブジェクトの注入が効いている証拠です。

image.png

OdinとZenjectの使い分け

以上、UnityC#においてインターフェイスを引き渡す方法を2つ紹介しました。
OdinとZenjectは、片方だけでもインターフェイスを引き渡すという目的は達成できるのですが、どちらにも得意な場面・不得意な場面があると考えています。

例えば、Odinの場合はインスペクタ上で視覚的にオブジェクトの注入ができるため、__同じインターフェイスを実装していても実体が異なる複数のインスタンスを注入したい場合__に向いています。

image.png
▲異なる複数のインスタンスを視覚的に注入できる
これをZenjectで行うには、Installerの設定時にIDを使った注入設定を細かく行う必要があるため、設定と管理が煩雑になります。

反対にゲームのManager的なクラスなど、__ただひとつのインスタンスしか持たないオブジェクトを注入したい__場合、インスペクタ上で手動で注入するよりも、依存関係を設定してZenjectにやってもらったほうが楽です。

このように、OdinとZenjectは得意不得意があると思いますので、__結局は両方とも導入して、適材適所で使い分ける__のが一番良いのではないかと考えています。
Odinはインターフェイス注入以外にもインスペクタを使いやすくする機能が揃ってますし、DIはUnityだけではなく様々な場面で使われるデザインパターンです。どちらも勉強しておいて損はないかと思います(自分も勉強中)。

番外編. どうしてもOdinを買わずにインスペクタでインターフェイスを渡したい場合

昔の自分がOdinを使わずに無理矢理インスペクタでインターフェイスを(擬似的に)渡す方法を考えてました。
ご興味ある方は下記参照ください。

  1. この記事はZenject, DI(依存性の注入)について詳しく解説する記事ではありません!正確性に欠ける表現がある可能性があることをご了承ください。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?