ZenjectのGameObjectContextの使い方

ZenjectにおいてSceneContextはとっつきやすいですが、 長い間GameObjectContextってどう使っていいかイマイチだったりしてました。 日本語の解説も少ないですし。

今回はそんなGameObjectContextの使い方を調べたので紹介します。

GameObjectContextの基本とSceneContextとの親子関係、サブコンテナ

GameObjectContextはシーンに配置しておくとで、 そのシーンのSceneContextを親とする、サブのContextとコンテナを作ることができます。

以下のようなSceneInstaller.csとGameObjectInstaller.cs、 ログ出力をするだけのGreeterクラスと、そのクラスを利用するGameObjectSampleクラスを実装してみます。

using UnityEngine;
using Zenject;

public class Greeter
{
    public void Greet()
    {
        Debug.Log("Hello");
    }
}

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Greeter>().AsSingle();
    }
}
using UnityEngine;
using Zenject;

public class GameObjectSample : IInitializable 
{
    [Inject] Greeter greeter;

    public void Initialize()
    {
        greeter.Greet();
    }
}

public class GameObjectInstaller : MonoInstaller<GameObjectInstaller>
{
    public override void InstallBindings()
    {
        Container.BindInterfacesTo<GameObjectSample>().AsSingle();
    }
}

ちなみにGameObjectSampleにはIInitializableインターフェースを付与し、 Installer側でBindInterfacesToでバインドしています。 これによりContextのResolve後にInitialize関数がコールされることになります。

実装したら、シーン上のContextを設定します。 Scene ContextにはSceneInstaller.csをアタッチし、Installersプロパティに追加します(こちらは説明割愛)。

GameObjectContextはScene Contextと同様ですが、 以下のようにCreate Menu -> Zenject -> Game Object Contextから生成できるため、 シーン上に配置し同じようにGameObjectInstaller.csをアタッチ、こちらもInstallerプロパティに追加します。

f:id:miki05:20180805172822p:plain

f:id:miki05:20180805194156p:plain

これで設定が完了です。 シーンを再生すると、Context間の親子関係が設定され SceneContextにバインドされたGreeterクラスのインスタンスが、 GameObjectContext内にあるGameObjectSampleクラス内にInjectされ、 Initialize関数内のgreeter.Greet()を無事コールすることができました。

f:id:miki05:20180805175020p:plain

これが、GameObjectContextの基本です。 今回はGameObjectInstaller内では1つのクラスしかBindしていませんが、 SceneContextと同様に複数の要素をバインドしInjectし合うことができます。

また、親子関係のルールはSceneContextのParentingと同じで、 子供Contextは親のコンテナの中の要素を使ってBindできますが、 親が子供のコンテナの中の要素をつかってBindはできません。

このサブコンテナという概念はそれなりに有用です。 一つの大きなSceneContextにすべてをバインドすると、 どこからでもなんでもBindできる結果になり疎結合なのかどうか不明になります。 サブコンテナを使い、局所、局所で必要なものだけをBindできる状態にしておくことでカプセル化を達成することができます。

なお、サブコンテナの作り方にはGameObjectContextを使わない方法(FromSubContainerResolve)もありますが今回は割愛します。

ZenjectBindingコンポーネントによるMonoBehaviourのBind

GameObjectContextでもZenjectBindingコンポーネントを使って MonoBehaviourをシーン上の設定だけでInstallerを使わずにContextにバインドできます。

たとえば、なんでもいいのでMonoBehaviourのSubObjectというコンポーネントを作り、GameObjectSampleクラス内で利用したい場合を考えます。 つまり、以下のようにGameObjectSample内に[Inject] SubObjectと記述する感じになります。

using UnityEngine;
using Zenject;

public class GameObjectSample : IInitializable 
{
    [Inject] Greeter greeter;
    [Inject] SubObject subject;

    public void Initialize()
    {
        greeter.Greet();
        Debug.Log(subject.name);
    }
}

public class GameObjectInstaller : MonoInstaller<GameObjectInstaller>
{
    public override void InstallBindings()
    {
        Container.BindInterfacesTo<GameObjectSample>().AsSingle();
    }
}

このとき、GameObjectContextをアタッチしてあるGameObjectもしくは、子供のGameObjectにZenjectBindingを配置します。(例として子供のGameObjectを作っています)

f:id:miki05:20180805180236p:plain

このように、設定を行うとGameObjectContext内でSubObjectインスタンスがきちんと解決されます。(SubObjectインスタンスはGameObjectContext内にBindされるので、SceneContextから見えません)

f:id:miki05:20180805180831p:plain

なお、GameObjectContextで親子関係を作ることで階層化させることもでき、 このときZenjectBindingのBinding先のContextは階層で一番近い親のContextになります。

もし、一番近いContext以外にしたい場合はZenjectBindingのContextのプロパティに明示的にContextを指定しておきます。

f:id:miki05:20180805181114p:plain

GameObjectContextの動的生成、そしてFacade

上記の例はGameObjectContextをシーンに事前配置していましたが、 動的に生成することもできます。ただし普通にInstantiateしても動きません。

まずは、第一歩として「シーンの初期化時」に動的にGameObjectContextを生成するようにしてみます。

準備としてGameObjectContextをプレファブ化し、シーンから削除します。 f:id:miki05:20180805182325p:plain

そして、SceneInstallerを以下のように修正します。

using UnityEngine;
using Zenject;

public class Greeter
{
    public void Greet()
    {
        Debug.Log("Hello");
    }
}

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    [SerializeField] GameObject gameObjectContextPrefab;

    public override void InstallBindings()
    {
        Container.Bind<Greeter>().AsSingle();
        Container.Bind<GameObjectSample>()
            .FromSubContainerResolve()
            .ByNewContextPrefab(gameObjectContextPrefab)
            .AsSingle()
            .NonLazy();
    }
}

GameObjectSampleをSceneContextにもBindする流れなのですが、 かなり大仰な感じになりますね...

ここでポイントはFromSubContainerResolveです。 これは、 他のコンテナを現在のContextのコンテナのサブコンテナとして追加し、 Bindで指定した要素をサブコンテナの要素から1つだけ選び、現在のContextにBindするという記法です。

さらに、ByNewContextPrefabでGameObjectContextのプレファブを指定し、NonLazy()つけて起動時に生成させるようにしています。

これにより、FromSubContainerResolve内部にて、ByNewContextPrefabで指定したGameObjectContextのコンテナをSceneContextのサブコンテナとして追加し、その中からGameObjectSampleを探しだしSceneContextにBindします。

また、現状態だと、GameObjectContextにはGameObjectSampleのInterfaceしかBindしていないため、自身もBindするようにGameObjectInstallerを変更します。

using UnityEngine;
using Zenject;

public class GameObjectSample : IInitializable 
{
    [Inject] Greeter greeter;
    [Inject] SubObject subject;

    public void Initialize()
    {
        greeter.Greet();
        Debug.Log(subject.name);
    }
}

public class GameObjectInstaller : MonoInstaller<GameObjectInstaller>
{
    public override void InstallBindings()
    {
        Container.BindInterfacesAndSelfTo<GameObjectSample>().AsSingle();
    }
}

バインドの指定をBindInterfacesAndSelfToに変更しました。 これでGameObjectContext内でGameObjectSampleとIInitializableの両方をResolveすることが可能になりました。

ちょっとここまで意味不明だったかもしれませんが、 SceneContextのgameObjectContextプロパティに先程プレファブ化したGameObjectContextプレファブを指定し、シーンを再生してみます。

f:id:miki05:20180805183958p:plain

シーン開始時にちゃんとGameObjectContextが生成され、 BindやInjectが成功しログが出力されることが確認できます。

さて、ここで現状でGameObjectSampleクラスは GameObjectContextにバインドされているわけですが、 そのContextのFacadeとして、つまりGameObjectContextにBindしたクラスの窓口や代表者として動作していることになります。

今回の例では、GameObjectSampleインスタンスはSceneContextにもBindされることで、SceneContext内の他の要素とお互いにやりとりすることができます。しかし、SceneContext内の他の要素はGameObjectContext内の他の要素をInjectしたり等、直接アクセスすることはできません。つまりGameObjectContextの世界とやり取りしたい場合は、その代表者と定めたGameObjectSampleクラスを介在する必要があります。

f:id:miki05:20180805185540j:plain

FromSubContainerResolveを利用することにより、SceneContextから見るとBindするものは、GameObjectContextなのか単体のコンポーネントなのかは気にせずに柔軟が使い方ができます。

たとえば、Bindしているクラスが予想以上に肥大化してしまった場合に、 GameObjectContextを使ことでDIによる可用性を保ちながら、サブコンポーネントに分割することや、GameObjectContextとして運用しているものを単純なコンポーネントに置き換える場合等でも大きな変更がいりません。

FactoryによるGameObjectContextの動的生成

さらにFactoryを利用することでGameObjectContextを任意タイミングで生成することができます。

以下のようにGameObjectSampleのFactoryをBindする形式に変更し、 GameObjectContextCreaterというFactoryをクリックで利用する適当なクラスを作ります。

using UnityEngine;
using Zenject;

public class Greeter
{
    public void Greet()
    {
        Debug.Log("Hello");
    }
}

public class GameObjectContextCreater : ITickable
{
    [Inject] PlaceholderFactory<GameObjectSample> factory;

    public void Tick()
    {
        if(Input.GetButtonDown("Fire1")) factory.Create();
    }
}

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    [SerializeField] GameObject gameObjectContextPrefab;

    public override void InstallBindings()
    {
        Container.Bind<Greeter>().AsSingle();

        Container.BindFactory<GameObjectSample, PlaceholderFactory<GameObjectSample>>()
            .FromSubContainerResolve()
            .ByNewContextPrefab(gameObjectContextPrefab);

        Container.BindInterfacesTo<GameObjectContextCreater>().AsSingle();
    }
}

GameObjectSampleのFactory利用は直接クラスをBind指定するのではなくContainer.BindFactoryでFactoryのBindを行えばOKです。(最近のZenjectではPlaceholderFactoryとして汎用的なFactoryを利用できます)

このときGameObjectContextCreaterはITickableをつけることでm毎フレームTick関数をZenject側からコールしてもらい、Inputクラスにより入力を検知するようにしています。(サンプルな懐かしい書き方)

これで、クッリクするたびにGameObjectContextが生成されます。

f:id:miki05:20180805191413p:plain

まとめ

GameObjectContextの使い方を紹介しました。

  • GameObjectContextはSceneContextの子となりサブコンテナを形成できる
  • ZenjectBindingは一番近いContextにBindしにいく
  • FromSubContainerResolveを使うと親のContextにFacadeとしてGameObjextContextのコンポーネントをBindできる 
  • Factoryを使うことでGameObjectContextを動的に生成することができる

GameObjectContextはSceneContextと違い、より手軽にContextを生み出すことや、破棄することができます。また親子関係の管理も楽です(Sceneが破棄されると同時に破棄される)。取り回しがしやすいのでぜひ使って行きたいです。おそらくこの後はどれをSceneContextとし、どれをGameObjectContextにするのかという議論も必要そうですが。