適当おじさんの適当ブログ

技術のことやゲーム開発のことやゲームのことなど自由に雑多に書き連ねます

Zenjectでシングルトンかつ破棄されないGameObjectのDI

概要

Zenject は言わずと知れた有名なDIフレームワークです。Zenjectの ProjectContext を使ってタイトルに書いてあることを実現したので、そのときのメモです。

GitHub - modesttree/Zenject: Dependency Injection Framework for Unity3D

ProjectContext とは

Zenjectでは、シーンごとに適した依存関係を SceneContext として定義するのが普通です。一方で、すべてのシーンに共通して、永続的な依存関係を定義したい場合があります。それらの依存関係は ProjectContext として定義します。

SceneContextの存在するシーンが読み込まれた場合に、ProjectContextのInstallerが先に実行され、依存関係がバインディングされます。そのため、ProjectContextを作るだけではダメで、SceneContextが存在しないと依存関係はバインディングされません。

そして、ProjectContextのゲームオブジェクトは、DontDestroyOnLoad なゲームオブジェクトとしてシーンに存在することになります。これも重要な点です。

ProjectContextは、ProjectContextのPrefabをResourcesフォルダ直下に配置しておくことで自動的に読み込まれます。

Installer とは

Installerとは、ある依存関係のまとまりを定義したクラスのことです。Installerとして定義しておくことで、依存関係をグルーピングでき、再利用もしやすくなりします。Installerには以下のような種類があります。ScriptableObjectInstallerというものもありますが、ここでは割愛します。

種類 内容
MonoInstaller MonoBehaviourとしてのInstaller
GameObjectにアタッチするので、inspectorで値を設定できる
Installer MonoBehaivourとしての振る舞いが必要ない場合に用いるInstaller

Installer、もしくは、MonoInstallerを継承したクラスを作成し、InstallBindings() をオーバーライドすることで実装できます。

public class TestInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // Binding
    }
}

実装したInstallerは、Contextの対応するフィールドにアタッチすることで InstallBindings が自動的に実行され、バインディングされます。

ZenjectのProjectContextでDIするために必要なもの

ProjectContextに必要なものをまとめると、以下になります。

  1. ProjectContext をPrefabとして作成する
    • Edit → Zenject → Create Project Context でResources直下に生成されます。
  2. Installer を実装し、ProjectContextに設定する
  3. 対象のSceneに、SceneContext を配置しておく
    • ヒエラルキー上で右クリック、Zenject → Scene Context で作成できます

どう使ったのか

よくある「シングルトン、かつ、シーン切り替え時にも破棄されないゲームオブジェクト」を生成するために Zenject を使用しました。

前者は、Injectされるインスタンスをシングルトンとして扱う AsSingle() で、後者は、ProjectContextとしてバインディングすることで実現できます。以下に具体例を示します。

具体例

ゴール

Scene遷移関連の操作をする「SceneManager」をすべてのSceneで使えるようにすることがゴールです。Zenject導入前は、別のエントリ に記載されているように、Singleton かつ DontDestroyOnLoad になるように自身で「SceneManager」を実装していました。Zenjectを使えば、それらを自身で実装せずに同じことを実現できます。

InstallerとCustomSceneManagerを実装する

using UnityEngine;
using Zenject;

public class CommonManagerInstaller : MonoInstaller
{
    // あらかじめPrefabを作成しておいてアタッチする
    [SerializeField]
    private CustomSceneManager customSceneManagerPrefab;

    public override void InstallBindings()
    {
        // FromComponentInNewPrefab() で、GameObjectを生成する
        // AsSingle() として、同じインスタンスが再利用されるようにする
        Container.Bind<CustomSceneManager>().FromComponentInNewPrefab(customSceneManagerPrefab).AsSingle();
    }
}
// フェードイン・フェードアウトを伴うシーン遷移を実現するクラス
// MonoBehaviourを継承して、通常のGameObjectとして作成するだけで良い
public class CustomSceneManager : MonoBehaviour {
    // 詳細な処理は別エントリを参照
}

Installer を ProjectContext に設定する

作成した CommonManagerInstaller を Prefabにしておきます。また、ProjectContextの「Prefab Installers」の項目にCommonManagerInstallerのPrefabをアタッチしておきます。

最終的に以下のような形になっていれば、シーン読み込み時に CommonManagerInstallers の依存関係がバインディングされます。

f:id:subarunari:20180108014624p:plain

デバッグ実行してヒエラルキーを確認すると、以下のように DontDestoryOnLoad なGameObjectが生成されていることが確認できると思います。

f:id:subarunari:20180108015412p:plain:w300

Injectする

あとは必要に応じてInjectするだけです。

[Inject]
private CustomSceneManager customSceneManager

まとめ

以上、ProjectContextを使って、Singleton かつ DontDestroyOnLoad な GameObject を生成したときのメモでした。

ほとんどのシーンで使うことが確実、かつ、GameObjectが破棄されると困る場合であれば、今回の例のようにProjectContextを使うと良いと思います。CommonManagerInstallerに、CustomSceneManagerと同じようにバインディングすることで、任意のGameObjectをDIできます。一方で、特定のシーンでしか使わないようなものであれば、SceneContextにしておいたほうがよいでしょう。不必要なバインディングは混乱の元となってしまうと思います。