imog

主にUnityとかの事を書いています

ZenjectのConstructionMethodたち

ZenjectでBindするとき、そのインスタンスをどのように用意するかという設定が必要です。その場で初期化するのか、既存のインスタンスを引っ張ってくるのかなど。これらはコンストラクションメソッドと呼ばれており、READMEに全部書かれています。

github.com

割と数が多いのでまとめてみました。間違っているものがあればコメントなどもらえるとありがたいです。

FromNew()

コンストラクタを呼び出します。特にConstruction Methodを定義しない場合デフォルトでFromNewが呼ばれます

Container.Bind<Foo>().FromNew().AsCached();
Container.Bind<Foo>().AsCached(); // FromNew()は省略できる

コンストラクタが複数あった場合、最初に引数なしコンストラクタを呼ぼうとします。見つからなかった場合引数ありコンストラクタを呼びます。

FromInstance(T instance)

既存のインスタンスを渡してBindします。

Container.Bind<Foo>().FromInstance(new Foo());
Container.BindInstance(new Foo());  // 同じ挙動

ユニークなインスタンスを明示的に渡すので、ScopeをAsTransientにしても効果はありません。実質強制的なAsCached指定?

FromMethod(Func<T> method)

FromMethod(Func<InjectContext, T> method)

インスタンスの生成をメソッドに移譲します。

private Foo GetFoo() => new Foo();

Container.Bind<Foo>().FromMethod(GetFoo).AsCached();
Container.Bind<Foo>().FromMethod(() => new Foo()).AsCached();
Container.Bind<Foo>().FromMethod(injectContext => new Foo()).AsCached();

InjectContext はInjectに関するメタ情報が詰まったクラスですが、基本的には使うことはないと思います。

FromMethodMultiple(Func<IEnumerable<T>> method)

FromMethodMultiple(Func<InjectContext, IEnumerable<T>> method)

配列やリストの生成をメソッドに移譲します。

private IEnumerable<Foo> GetFoos() => new List<Foo>();

Container.Bind<Foo>().FromMethodMultiple(GetFoos).AsCached();
Container.Bind<Foo>().FromMethodMultiple(injectContext => new List<Foo>()).AsCached();

複数形である以外は FromMethod と同様です。

FromFactory<IFactory<T>>()

任意のIFactoryクラスで生成します。

public class Foo {
    public class Factory : PlaceholderFactory<Foo> { }
}

Container.Bind<Foo>().FromFactory<Foo.Factory>();

Zenject Factoryパターンについてはこちらも合わせてどうぞ。

adarapata.hatenablog.com

FromIFactory(Action<ConcreteBinderGeneric<IFactory<T>>> factoryBindGenerator)

カスタムファクトリで生成します。

public class Foo {
    public Foo(string hoge) { }

    public class Factory : IFactory<Foo> {
        private string _arg;
        public Factory(string arg) {
            _arg = arg;
        }
        public Foo Create() => new Foo(_arg);
    }
}
Container.Bind<Foo>().FromIFactory(x => x.To<Foo.Factory>().WithArguments("hoge")).AsCached();

FromFactoryとの違いは、Factory生成の自由度です。 FromFactoryは暗黙的にFromNew()で生成されるためFactory自体の生成方法に制限がかかりますが、こちらは自由に定義できます。引数つけてもいいしSubContainerからとってきてもよい。

公式ドキュメントだとScriptableObjectからFactoryを引っ張ったりしてます。

class FooFactory : ScriptableObject, IFactory<Foo>
{
    public Foo Create()
    {
        // ...
        return new Foo();
    }
}
// ScriptableObjectから生成したFooFactoryからFooを生成してBindする
Container.Bind<Foo>().FromIFactory(x => x.To<FooFactory>().FromScriptableObjectResource("FooFactory")).AsSingle();

FromComponentInNewPrefab(UnityEngine.Object prefab)

InstantiateしたPrefabからComponentをBindします。

Container.Bind<FooBehavior>().FromComponentInNewPrefab(fooPrefab).AsCached();

内部的にはGetComponentInChildrenで探すので、複数あった場合は先に見つかった方をBindします。

FromComponentsInNewPrefab(UnityEngine.Object prefab)

FromComponentInNewPrefabの複数版です。

Container.Bind<FooBehavior>().FromComponentsInNewPrefab(fooPrefab).AsCached();

こちらはGetComponentsInChildrenで探します。

FromComponentInNewPrefabResource(string path)

パスからPrefabをInstantiateしてBindします。

Container.Bind<FooBehavior>().FromComponentInNewPrefabResource("some/foo").AsCached();

内部的にはResources.Loadが走ります

FromComponentsInNewPrefabResource(string path)

FromComponentInNewPrefabResourceの複数版です。

Container.Bind<FooBehavior>().FromComponentsInNewPrefabResource("some/foo").AsCached();

FromNewComponentOnNewGameObject()

空のGameObjectを生成してAddComponentしたものをBindします。

Container.Bind<FooBehavior>().FromNewComponentOnNewGameObject().AsCached();

FromNewComponentInNewPrefab(UnityEngine.Object prefab)

PrefabをInstantiateしてAddComponentしたものをBindします。

Container.Bind<FooBehavior>().FromNewComponentInNewPrefab(fooPrefab).AsCached();

FromNewComponentInNewPrefabResource(string path)

パスからPrefabをInstantiateしてAddComponentしたものをBindします。

Container.Bind<FooBehavior>().FromNewComponentInNewPrefabResource("some/foo").AsCached();

パスから読み込むのでResources.Loadが呼ばれます。

FromNewComponentOn(GameObject gameObject)

FromNewComponentOn(Func<InjectContext, GameObject> gameObjectGetter)

既存のGameObjectにAddComponentしたものをBindします。

Container.Bind<FooBehavior>().FromNewComponentOn(fooGameObject);

FromNewComponentSibling()

依存しているComponentと同階層にAddComponentしたものをBindします。

public class BarBehavior : MonoBehavior {
    [Inject]
    private FooBehavior _foo;
}

Container.Bind<FooBehavior>().FromNewComponentSibling();

この場合、BarBehaviorがアタッチされているGameObjectにFooBehaviorがAddComponentされます。 この特性から、Injectされる側がComponentでないと使えません。また、複数依存しているオブジェクトがあった場合それぞれの同階層にAddComponentされます。よって強制的にAsTransient指定されるイメージです。AsSingleやAsCachedを明示的に呼んでもエラーは出ませんが適用はされません。

FromNewComponentOnRoot()

現在のContextと同じ階層にAddComponentしたものをBindします。

Container.Bind<FooBehavior>().FromNewComponentOnRoot().AsCached();

例えばSceneContextでInstallerを呼んだ場合はSceneContextと同じ階層にFooBehaviorが作成されます。基本的にSceneContextで使うことはなく、GameObjectContextなどのSubContainerで使うのがメインになるでしょう。

FromResource(string path)

Resourceディレクトリのファイルを読み込んでBindします。

Container.Bind<Texture>().FromResource("some/texture").AsCached();

Resources.Loadでロードできるファイルは全部対応してます。

FromScriptableObjectResource(string path)

ResourceディレクトリのScriptable Objectを読み込んでBindします。

Container.Bind<FooScriptable>().FromScriptableObjectResource("some/foo").AsCached();

直接元データをBindするので、動的に値を更新するとファイルが書き換わってしまうので注意

FromNewScriptableObjectResource(string path)

ResourceディレクトリのScriptable Objectから読み込んでコピーしたインスタンスをBindします。

Container.Bind<FooScriptable>().FromNewScriptableObjectResource("some/foo").AsCached();

元データを書き換えたくない場合はこちらを使いましょう。

FromComponentInHierarchy()

シーンのヒエラルキーを辿ってコンポーネントを探してBindします。

Container.Bind<FooBehavior>().FromComponentInHierarchy();

GetComponentOnChildren で検索します。ParentContractがある場合親のシーンも辿ると書いてるけど検証できていない・・

FromComponentsInHierarchy()

FromComponentInHierarchyの複数版です。

Container.Bind<FooBehavior>().FromComponentInHierarchy()

こちらはGetComponentsOnChildrenで検索します。

FromComponentSibling()

依存しているComponentの同階層を検索してBindします。

public class BarBehavior : MonoBehavior {
    [Inject]
    private FooBehavior _foo;
}

Container.Bind<FooBehavior>().FromComponentSibling();

上記の場合、BarBehaviorと同じ階層にアタッチされているFooBehaviorをInjectします。 この特性から、Injectされる側(今回はFooBehavior)がComponentでないと使えません。

内部的にはGetComponentを呼んでいます。

FromComponentsSibling()

FromComponentSiblingの複数版です。

Container.Bind<FooBehavior>().FromComponentsSibling();

内部的にはGetComponentsを呼んでいます。

FromComponentInParents()

依存しているComponentの同階層と、親を検索してBindします。

Container.Bind<FooBehavior>().FromComponentInParents();

内部的にはGetComponentInParentが呼ばれています。

FromComponentsInParents()

FromComponentInParentsの複数版です。

FromComponentInChildren()

依存しているComponentの同階層と子を検索してBindします。

Container.Bind<FooBehavior>().FromComponentInChildren();

内部的にはGetComponentInChildrenが呼ばれています。

FromComponentsInChildren()

FromComponentInChildrenの複数版です。内部的にはGetComponentInChildrenが呼ばれています。

FromResolve()

Containerから検索して再度Bindします。基本的に使うことはありませんが、インタフェースを別のインタフェースにBindしたいときなどに使えます。

public interface IFoo
{
}

public interface IBar : IFoo
{
}

public class Foo : IBar
{
}

Container.Bind<IFoo>().To<IBar>().FromResolve();
Container.Bind<IBar>().To<Foo>();

上記の場合、IBarとしてBindされたFooをIFooとしてもBindします。

FromResolveAll()

FromResolveの複数版です。

FromResolveGetter<T>(Func<T, T2> getter)

Containerから取得したオブジェクトから取得してBindします。

Container.Bind<Foo>().FromResolveGetter<Bar>(bar => bar.GetFoo());

階層的な構造になっている場合は結構便利です。

FromResolveAllGetter<T>(Func<T, T2> getter)

FromResolveGetterの複数版です。

FromSubContainerResolve()

SubContainerから検索してBindします。使う場合SubContainerを作成する必要があるので、このメソッドの後にさらにSubContainerのConstruction Methodを呼ぶことになります。

FromSubContainerResolveAll()

FromSubContainerResolve()の複数版です。

SubContainerのConstruction Method

ByNewPrefabMethod(UnityEngine.Object prefab, Action<DiContainer> installerMethod)

Prefabからインスタンスを生成し、それにSubContainerを持たせてメソッドで初期化します。

public GameObject SubContainerPrefab;
public override void InstallBindings()
{
    Container.Bind<Foo>().FromSubContainerResolve()
            .ByNewPrefabMethod(SubContainerPrefab, InstallSubContainer).AsCached();
}

private void InstallSubContainer(DiContainer container)
{
    container.Bind<Foo>().AsCached();
}

この方法で生成されたインスタンスは自動でGameObjectContextがアタッチされます。なのでどんなPrefabでもSubContainerを持つことができます。

ByNewPrefabInstaller<TInstaller>(UnityEngine.Object prefab)

Prefabからインスタンスを生成し、それにSubContainerを持たせてInstallerで初期化します。

Container.Bind<Foo>().FromSubContainerResolve().ByNewPrefabInstaller<FooInstaller>(SubContainerPrefab);

class FooInstaller : Installer
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>();
    }
}

基本的な挙動はByNewPrefabMethodと同じで、SubContainerへのBindがInstallerクラスに変わっただけです。

ByNewPrefabResourceMethod(string resourcePath, Action<DiContainer> installerMethod)

Resourcesからインスタンスを生成し、それにSubContainerを持たせてメソッドで初期化します。

Container.Bind<Foo>().FromSubContainerResolve().ByNewPrefabResourceMethod("Path/To/Prefab", InstallFoo);

void InstallFoo(DiContainer subContainer)
{
    subContainer.Bind<Foo>();
}

Resources.Loadが走ります。

ByNewPrefabResourceInstaller<TInstaller>(string resourcePath)

Resourcesからインスタンスを生成し、それにSubContainerを持たせてInstallerでBindします。

Container.Bind<Foo>().FromSubContainerResolve().ByNewPrefabResourceInstaller<FooInstaller>("Path/To/MyPrefab");

class FooInstaller : Installer<FooInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>();
    }
}

TInstaller : InstallerBaseなので、Installer<T>を継承したもののみ使えます。 MonoInstallerはできないので注意

ByNewGameObjectInstaller<TInstaller>()

空のGameObjectを生成し、それにSubContainerを持たせてInstallerでBindします。

Container.Bind<Foo>().FromSubContainerResolve().ByNewGameObjectInstaller<FooInstaller>();

class FooInstaller : Installer<FooInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>();
    }
}

後述のByInstaller<TInstaller>とほぼ同じ動きをしますが、空のGameObjectにGameObjectContextがアタッチされているのがポイントです。IInitializableITickableなども適切にBindされるし、GameObjectを破棄すればSubContainerは破棄されます。

ByNewGameObjectMethod(Action<DiContainer> installerMethod)

空のGameObjectを生成し、それにSubContainerを持たせてメソッドでBindします。

Container.Bind<Foo>().FromSubContainerResolve()
                     .ByNewGameObjectMethod(InstallSubContainer);

void InstallSubContainer(DiContainer subContainer)
{
    subContainer.Bind<Foo>();
}

メソッドでBindする以外はByNewGameObjectInstallerと同じです。

ByMethod(Action<DiContainer> installerMethod)

メソッドでSubContainerを初期化します。

Container.Bind<Foo>().FromSubContainerResolve()
                     .ByMethod(InstallSubContainer);

void InstallSubContainer(DiContainer subContainer)
{
    subContainer.Bind<Foo>();
}

こちらはITickable IInitializable IDisposableといったインタフェースを適切にBindできません。それらを処理するKernelクラスがいないからです。もし必要ならWithKernel()を合わせて呼びます。

Container.Bind<Foo>().FromSubContainerResolve().ByMethod(InstallSubContainer).WithKernel();
ByNewGameObjectInstaller<TInstaller>()

InstallerでSubContainerを初期化します。

Container.Bind<Foo>().FromSubContainerResolve().ByInstaller<FooInstaller>();

class FooInstaller : Installer<FooInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>();
    }
}

こちらもByMethod同様に、Kernel不在のためITickableを処理できません。WithKernel()を呼ぶとよいでしょう。

ByNewContextPrefab(UnityEngine.Object prefab)

GameObjectContextをアタッチしたPrefabでSubContainerを初期化します。

Container.Bind<Foo>().FromSubContainerResolve().ByNewContextPrefab(MyPrefab);

class FooFacadeInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>();
    }
}

もちろんPrefab側にはGameObjectContextがアタッチされている必要があるので注意。

ByNewContextPrefabResource(string resourcePath)

GameObjectContextをアタッチしたPrefabをLoadしてSubContainerを初期化します。

Container.Bind<Foo>().FromSubContainerResolve().ByNewContextPrefabResource("Path/To/MyPrefab");

Resources.Loadが呼ばれます。

ByInstance(DiContainer subContainer)

直接SubContainerとなるDiContainerを渡します。

おわりに

多すぎる。

追記

この記事を書くにあたってドキュメント見てたら間違いっぽいのを見つけたのでPR出した。 github.com

ブログ書くのはいいぞ。

Zenject Factoryの話

はじめに

このエントリはUNIBOOK10で書いた記事を転載したものです。

unity-bu.booth.pm

UNIBOOK10、いい話がいっぱいあるから買ってね!

Zenject Factory


ZenjectはUnityで動作するDIライブラリです。煩わしい依存関係の解決を助ける非常に強力な武器となりますが、 強力かつ多機能であるためどこから学んでいくべきか悩むことも多いです。公式のドキュメントはとても充実していますが、前提となる知識が多かったり、基本的なBindingの話以外は日本語での資料が少ないというのも理由の1つでしょう。

この記事では、ZenjectがもつFactoryによる動的なオブジェクト生成について簡単な解説を行います。

github.com

動的に生成したインスタンスへのインジェクション

シーン読み込み時にInstallerを実行して、オブジェクトをバインドし依存関係を解決していくというのがZenjectのスタンダードな使い方でしょう。Containerはシーン開始時の1回のみインジェクションしてくれます。

しかし、実際の開発においてすべてのインスタンスが初期化時に存在するとは限りません。たとえば、アクションゲームで敵キャラが動的に生成されることは十分に考えられるでしょう。

次の例で考えてみましょう。

  1. Enemyは動的に生成される
  2. Enemyはプレイヤーに依存している
  3. PlayerはContainerにバインドされている
  4. 敵の種類は今後増える可能性がある

最低限のコードで書くとこうなります。

public class Player { }
public interface IEnemy { }
public class Enemy : IEnemy
{
    Player _player;
    public Enemy(Player player)
    {
        _player = player;
    }
}

public class MainGameInstaller : MonoInstaller<MainGameInstaller> {
    public override void InstallBinding()
    {
        Container.Bind<Player>().AsCached();
    }
}

「今後増える可能性がある」という前提があるのでIEnemyインタフェースを定義して複数に対応できるようにしています。 このEnemyを扱いたい場合どのように書けばよいでしょう。EnemyはPlayerに依存しているので、生成する側はコンストラクタで渡すためのPlayerを知っている必要があります。

Enemyを生成するクラスEnemySpawnerを素直に書くと次のようになるでしょう。

public class EnemySpawner
{
    [Inject]
    Player _player;
    List<IEnemy> _enemies = new List<IEnemy>();

    public void Spawn()
    {
        var enemy = new Enemy(_player);
        _enemies.Add(enemy);
    }
}

これにより、使う側はSpawnerさえ持っておけばPlayerのことは知らずに済みます。 しかしこの方針だと、Enemyの更新に伴いSpawnerのもつオブジェクトが増えていきます。 たとえばEnemyがPlayerだけでなくて難易度情報なども必要になってくるとどうなるでしょう。

public class Enemy : IEnemy
{
    Player _player;
    Difficulty _difficulty;

    public Enemy(Player player, Difficulty difficulty)
    {
        _player = player;
        _difficulty = difficulty;
    }
}

public class EnemySpawner {
    [Inject]
    Player _player;
    [Inject]
    Difficulty _difficulty;
    List<IEnemy> _enemies = new List<IEnemy>();
    public void Spawn(){
        var enemy = new Enemy(_player, _difficulty);
        _enemies.Add(enemy);
    }
}

Enemyが膨れ上がるにつれEnemySpanwerも修正が必要になり管理コストが上がっていきます。 EnemySpawnerはEnemyを生成したいだけあり、どうやって生成するかなどのEnemyの内部実装をそこまで知る必要はありません。Spanwer自体はプレイヤーのことも難易度のことも関心がないのです。 今後別のIEnemyが実装されたときにまた別の引数が必要になるかもしれません。それらをSpawnerが把握するのは大変です。

一応、DiContainerを渡せばどれだけ引数が増えても対応は可能です。

public class Enemy : IEnemy
{
    Player _player;
    Difficulty _difficulty;

    public Enemy(DiContainer container)
    {
        _player = container.Resolve<Player>();
        _difficulty = container.Resolve<Difficulty>();
    }
}

public class EnemySpawner {
    [Inject]
    DiCotainer _container;
    List<IEnemy> _enemies = new List<IEnemy>();
    public void Spawn(){
        var enemy = new Enemy(_container);
        _enemies.Add(enemy);
    }
}

しかしこれは、次の問題を生み出してしまいます。

  • Player,Difficultyが存在するのか実行時まで気づきにくい
  • DiContainerへの依存が発生し、結果的に依存先が1つ増えている

仮にDifficultyがバインドされていない場合、それに気づくことができるのはSpawnを呼び出したタイミングになってからです。Zenjectはシーン内の依存関係を解決するバリデーション機能がエディタ上で実行できて大変便利なのですが、それらの恩恵に預かれないのはもったいないです。

また、コード量は減りましたが「コンテナそのものに依存する」という新たな依存が発生しています。これはサービスロケータと呼ばれるデザインパターンでDIとはまた異なってきます。

これらの問題を解決するためには、Enemyを生成したいEnemySpawnerから、生成のために必要な関心事を切り出してあげたほうがよさそうです。このような場合Fatoryパターンが効果的です。

Factoryパターン

関心事を切り出すために、Enemyの生成とそれに必要な情報を集約したFactoryクラスを定義します。

public class Enemy : IEnemy
{
    Player _player;
    public Enemy(Player player)
    {
        _player = player;
    }

    public class Factory
    {
        readonly Player _player;
        public Factory(Player player)
        {
            _player = player;
        }

        public Enemy Create()
        {
            return new Enemy(_player);
        }
    }
}

public class EnemySpawner
{
    [Inject]
    Enemy.Factory _enemyFactory;
    List<IEnemy> _enemies = new List<IEnemy>();

    public void Spawn()
    {
        var enemy = _enemyFactory.Create();
        _enemies.Add(enemy);
        // 何かする
    }
}

public class MainGameInstaller : MonoInstaller<MainGameInstaller> {
    public override void InstallBinding()
    {
        Container.Bind<Player>().AsCached();
        Container.Bind<Enemy.Factory>().AsCached();
    }
}

先ほどとの違いは2点です。

  • Enemy.Factoryクラスが定義された
  • EnemySpawnerからEnemyに関連する要素がなくなった

Enemy.FactoryはEnemyインスタンスを作るだけのクラスです。インスタンスを作るために必要な要素をすべて持っており、外部から引数を渡さずともCreate()を呼ぶだけでEnemyを返してくれます。これを事前にバインドしています。

これにより、EnemySpawnerはEnemyではなくEnemy.Factoryに依存します。Enemyのコンストラクタを呼びだす必要がなくなったので不要なメンバ変数が消えてFactory.CreateするだけでEnemyを取得できるようになりました。 仮に今後IEnemyの種類が増えても、実態を知らずともそれぞれのFactoryを呼びだすだけで取得できるので差し替えが以前より簡単になりました。

Factoryパターンのメリットは依存度を下げられることにあります。直接実態を生成しようとするとどうしても依存関係を知る必要があるため、コードのあちこちにクラスの役割とは直接の関係がないコンストラクタ呼び出しのためのメンバ変数が増えてしまいます。これは避けられないので、ならば生成処理を一か所に集約することで依存関係を把握しなければいけないクラスを減らしてしまおうというやり方です。

なお、現状は特定のFactoryに依存しているのでIEnemyの実体を動的に切り替えということはできません。

BindFactory

先ほどは自前でFactoryパターンの実装を行いましたが、Zenjectでは、Factoryパターンを簡単に実装できるような仕組みがあります。 PlaceholderFactoryを実装してBindFactoryでバインドするだけです。

public class Enemy
{
    Player _player;
    public Enemy(Player player)
    {
        _player = player;
    }

    public class Factory :  PlaceholderFactory<Enemy>
    {
    }
}

public class MainGameInstaller : MonoInstaller<MainGameInstaller> {
    public override void InstallBinding()
    {
        Container.Bind<Player>().AsCached();
        Container.BindFactory<Enemy, Enemy.Factory>();
    }
}

挙動は先ほどと同じですが、かなりコードがシンプルになりました。変更したのは次の二点です。

  • Enemy.FactoryにPlaceholderFactoryを継承させた
  • 通常のBindをやめてContainer.BindFactory<Enemy, Enemy.Factory>()を呼んだ

元々作っていたfactoryの実装は「Enemyのコンストラクタに必要なインスタンスの保持」と「インスタンスの生成」でしたが、PlaceholderFactory<T>は先述した2つの機能を備えています。 なので単純な依存関係の管理と生成だけなら継承するだけで全く同じことができます。

BindFactory<Enemy, Enemy.Factory>() は、Enemyの生成を行うFactoryクラスをバインドします。FactoryはEnemyの依存関係を見て、コンテナから引数に該当するオブジェクトをよしなに渡してくれます。 今回の場合だとバインドされたPlayerをコンストラクタとして渡してくれます。

また、PlaceholderFactoryは動的にパラメータを渡すこともできます。たとえば、敵の移動速度だけ外部から流し込めるようにしたいときは次のとおりです。

public class Enemy : IEnemy
{
    readonly Player _player;
    readonly float _speed;

    public Enemy(float speed, Player player)
    {
        _player = player;
        _speed = speed;
    }

    public class Factory : PlaceholderFactory<float, Enemy>
    {
    }
}

public class EnemySpawner
{
    [Inject]
    Enemy.Factory _enemyFactory;
    List<IEnemy> _enemies = new List<IEnemy>();

    public void Spawn()
    {
        var enemy = _enemyFactory.Create(Random.Range(1F, 100F));
        _enemies.Add(enemy);
    }
}

public class MainGameInstaller : MonoInstaller<MainGameInstaller> {
    public override void InstallBinding()
    {
        Container.Bind<Player>().AsCached();
        Container.BindFactory<float, Enemy, Enemy.Factory>();
    }
}

PlaceholderFactory<float, Enemy> のように、先頭に渡したい型を指定します。 Bind時も同様にBindFactoryの先頭に型を指定するだけです。 これでEnemy.Factory.Createに引数を持たせれるので動的にパラメータを渡せます。

ちなみに、コンストラクタを呼べないMonoBehaviourの場合は次のとおりです。

public class Enemy : MonoBehaviour, IEnemy
{
    Player _player;
    [Inject]
    public void Construct(Player player)
    {
        _player = player;
    }

    public class Factory : PlaceholderFactory<Enemy>
    {
    }
}

public class MainGameInstaller : MonoInstaller
{
    public GameObject EnemyPrefab;

    public override void InstallBindings()
    {
        Container.Bind<Player>().AsSingle();
        Container.BindFactory<Enemy, Enemy.Factory>()
                        .FromComponentInNewPrefab(EnemyPrefab);
    }
}

コンストラクタがメソッドインジェクションに変わるだけでほとんど同じです。

先ほどの例だとIEnemyは一種類でしたが、数が増えてきたらどう扱うべきでしょうか。

public interface IEnemy {
}
public class Slime : IEnemy {
}
public class Orc : IEnemy {
}

SlimeとOrcのどちらが生成されるか、実行時に確定させたい場合どうしたらいいでしょうか。この場合はBindFactory時に型を指定するだけでよいです。

public class EnemyFactory : PlaceholderFactory<IEnemy>
{
}

public class EnemyInstaller : MonoInstaller
{
    public bool UseOrc;

    public override void InstallBindings()
    {
        if (UseOrc)
        {
            Container.BindFactory<IEnemy, EnemyFactory>().To<Orc>();
        }
        else
        {
            Container.BindFactory<IEnemy, EnemyFactory>().To<Slime>();
        }
    }
}

public class EnemySpawner {
    EnemyFactory _factory
    List<IEnemy> _enemies = new List<IEnemy>();
    public EnemySpawner(EnemyFactory factory) {
        _factory = factory;
    }

    public void Spawn() {
        var enemy = _factory.Create();
        _enemies.Add(enemy);
    }
}

Toで明示的に型を指定すれば任意のインスタンスを生成するEnemyFactoryになります。EnemySpawner側はもはや一切の実態を気にすることなくIEnemyの管理に集中できます。

カスタムファクトリ

上記の例ではInstallerでFactoryの種類を決めていました。しかし場合によっては実行中に動的にFactoryを切り替えたい場合があります。 その場合はどうやって切り替えるか、というロジックもFactoryに内包させられると使う側は楽になるでしょう。しかしPlaceholderFactoryにそのような機能はありません。

ここは、大元であるIFactoryインタフェースを実装した自前のカスタムファクトリを作る必要があります。

今回は「難易度で生成する敵を変更する」カスタムファクトリを作ります。

public enum Difficulty
{
    Noraml,
    Hard
}
public interface IEnemy { }

public class Orc : IEnemy {
    public class Factory : PlaceholderFactory<Orc> { }
}

public class Slime : IEnemy {
    public class Factory : PlaceholderFactory<Slime> { }
}

public class EnemyFactory : PlaceholderFactory<IEnemy> { }

public class CustomEnemyFactory : IFactory<IEnemy>
{
    Orc.Factory _orcFactory;
    Slime.Factory _slimeFactory;
    Difficulty _difficulty;

    public CustomEnemyFactory(Orc.Factory orcFactory,
                              Slime.Factory slimeFactory,
                              Difficulty difficulty)
    {
        _orcFactory = orcFactory;
        _slimeFactory = slimeFactory;
        _difficulty = difficulty;
    }

    public IEnemy Create()
    {
        if(_difficulty == Difficulty.Hard)
        {
            return _orcFactory.Create();
        }
        return _slimeFactory.Create();
    }
}

public class EnemySpawner
{
    EnemyFactory _enemyFactory;
    List<IEnemy> _enemies = new List<IEnemy>();
    public EnemySpawner(EnemyFactory enemyFactory)
    {
        _enemyFactory = enemyFactory;
    }

    public void Spawn()
    {
        var enemy = _enemyFactory.Create();
        _enemies.Add(enemy);
    }

}

public class EnemyInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Difficulty>().FromInstance(Difficulty.Noraml).AsCached();
        Container.BindFactory<Orc, Orc.Factory>();
        Container.BindFactory<Slime, Slime.Factory>();
        Container.BindFactory<IEnemy, EnemyFactory>()
                                            .FromFactory<CustomEnemyFactory>();
    }
}

ロジックはすべてIFactoryを実装したCustomEnemyFactoryに集約されています。このクラスはそれぞれ実際にインスタンス生成を行うFactoryを所持しており条件に応じて切り替えています。 FromFactoryメソッドを指定することで、EnemyFactoryのCreateが呼ばれたときに処理を委譲できます。 EnemyFactoryへの影響は全くなく、ロジックの差し替えも容易です。

閑話:空のFactoryは必要か

毎回PlaceholderFactory<T>を継承しただけのFactoryクラスを定義していますが、厳密には定義しなくてもバインドはできます。 次のようにPlaceholderFactoryを直接バインドすることも可能です。

Container.BindFactory<Enemy, PlaceholderFactory<Enemy>>();

メンバ変数で持つときも同様に直接PlaceholderFactoryを持ってしまえばFactoryクラスを作る必要はありません。コードを書く手間が省けるのでよさそうに思えますね。

しかし、ドキュメントではこれを非推奨としています。理由は次の二点です。

  • パラメータの変更があった場合にコンパイルエラーが発生しないので気づくのが遅くなる
  • PlaceholderFactry<T> より T.Factory の方が可読性が高いと考えている。

たとえば、PlaceholderFactoryをそのまま使って、あとからパラメータを1つ追加したとしましょう。 次のコードのようになります。

public class Enemy : IEnemy
{
    readonly Player _player;
    readonly float _speed;

    public Enemy(float speed, Player player)
    {
        _player = player;
        _speed = speed;
    }
}

public class EnemySpawner
{
    [Inject]
    PlaceholderFactory<float, Enemy> _enemyFactory;
    List<IEnemy> _enemies = new List<IEnemy>();

    public void Spawn()
    {
        var enemy = _enemyFactory.Create(Random.Range(1F, 100F));
        _enemies.Add(enemy);
    }
}

public class MainGameInstaller : MonoInstaller<MainGameInstaller> {
    public override void InstallBinding()
    {
        Container.Bind<Player>().AsCached();
        Container.BindFactory<Enemy, PlaceholderFactory<Enemy>>();
    }
}

このコードはコンパイルは通りますが、実行時にエラーを出してしまいます。理由は単純で、バインド処理のところにfloat型を追加するのを忘れているからです。

PlaceholderFactoryはバインドされているがEnemySpawnerが参照しているPlaceholderFactory<float, Enemy>がInjectできずにエラーが発生します。些細なことですがよくありがちなエラーです。これがFactoryクラスを定義しているならば変更時点でコンパイルエラーが教えてくれたはずです。 また、パラメータが増えるほどPlaceholderFactoryという名前が何のFactoryなのか難しくなってきます。PlaceholderFactory<Player, Difficulty, Enemy>という名前のFactoryは理解に時間がかかってしまうでしょう。たった数行のコードを減らすことで得られるメリットはあまり大きくありません。

上記の理由から、基本的にはEnemy.FactoryのようにネストしたFactoryを作ることが推奨されています。 特に強い理由がなければその都度定義した方がよいでしょう。

終わりに

Zenjectは強力ですがその分難易度も高いと言われています。特に動的なインスタンスのInjectはやり方をしっかり考えないとContainerを各所に引きずり回してインジェクションなどをやってしまいがちです。これはコードをより難解なものにしてしまうので、Factoryを用いた設計を覚えてシンプルな仕組みに書き直していくのがよいでしょう。

Zenject Decorator Binding

ZenjectにはDecoratorパターンをサポートするDecorator Bindingなる機能があったりします。あんまりメインで使うような機能ではありませんが覚えておくと便利かもしれません。

github.com

Decorator パターン

DecoratorパターンはGoFで定義されてるデザインパターンの一つです。既存のオブジェクトに対して動的な機能追加を行う場合などに効果を発揮します。 例えば超絶シンプルなRPGを作るときに、攻撃力とHPを持つクラスがあるとします。ステータス部分は共通のインタフェースとして、戦士と魔法使いの二種類を作ります。

public interface IStatus
{
    int Life { get; }
    int Attack { get; }
}

public class Knight : IStatus
{
    public int Life => 20;
    public int Attack => 10;
}

public class Wizard : IStatus
{
    public int Life => 15;
    public int Attack => 2;
}

これらはIStatusで抽象化できるので、攻撃の数値部分などは同じように扱えます。ところでRPGにはやはり攻撃力アップなどバフ効果も欲しいですね。その場合はAttackの補正処理をかけることになるのですがどこに実装しましょう。例えばIStatusを持った抽象クラスのBaseCharacterなどが生まれて、そこにバフクラスを渡すような処理にするのもありだと思います。

public abstruct class BaseCharacter : IStatus
{
    public virtual int Life { get; }
    public virtual int Attack { get; }

    public void SetBuffStatus(IStatusBuffer buff) {
        // 内部パラメータをあれこれする処理
    }
}

var knight = new Knight();
knight.SetBuffStatus(new AttackUpBuffer()); // バフをかける

この場合、バフの種類が増えていくとちょっと大変そうですね。メソッド内に変更の実装を書くならSetBuffStatusが肥大化します。じゃあBuff側に実装を書いてそのメソッドを呼ぶだけ、という形にすると今度はBuff側から更新できるようにBaseCharacterがパラメータ更新メソッドを外部に公開することになります。どちらにせよ、既存への影響を与えがちです。気合でなんとかしましょう。

Decoratorパターンは、上からインスタンスを被せることで変更を少なくします。

// 攻撃力を高めるバフ
public class AttackUpDecorator : IStatus
{
    private readonly IStatus _decorator;

    public int Life => _decorator.Life;
    public int Attack => _decorator.Attack * 2; // 強い!

    public AttackUpDecorator(IStatus decorator)
    {
        _decorator = decorator;
    }
}

var knight = new Knight();
var buffKnight = new AttackUpDecorator(knight); // バフをかける

同じinterfaceを持ったDecoratorクラスを用意することで、同じ振る舞いを持たせたまま拡張ができるようになります。

この実装方法のメリットは以下です。

  • 既存のオブジェクトへの変更が少ない
  • 呼び出し側が変化を気にしなくてよい

すでに動いているプロジェクトに対して効果的だったりします。

Zenjectでの実装方法

Zenjectでバインドしているインスタンスに同様のDecorateを行いたい場合は以下で実現できます。

public class StatusInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // いつものバインド
        Container.Bind<IStatus>().To<Knight>().AsCached();
        // IStatusをAttackUpDecoratorでDecorateする
        Container.Decorate<IStatus>().With<AttackUpDecorator>().AsCached();
    }
}

Decorate<T>().With<InheritedT>といった感じで一行で実装できます。便利。この場合Resolveして返ってくるのはAttackUpDecoratorです。

どう使うの?

上記のようなRPGはかなり極端な例なので実際使えるかという推敲すべきですが、デバッグ、プロファイリングの処理を書くには有効ではないかなと思います。あとゲームで言うとローディング中の表示をDecorateで切り出すのももしかしたらありかもしれないですね。

public interface IAsyncLoadable<T> {
    Task<T> Load();
}

public class FooLoader : IAsyncLoadable<Foo> {
    public async Task<Foo> Load() {
        // なんか読み込みロジック
    }
}

public class LoadingIconDecorator : IAsyncLoadable<T> {
    private readonly IAsyncLoadable<T> _decorator;
    public LoadingIconDecorator(IAsyncLoadable<T> decorator) {
        _decorator = decorator;
    }
    public async Task<T> Load() {
        ShowLoadIcon(); // ローディングアイコン出すなどの処理
        await _decorator.Load();
        CloseLoadIcon(); // 閉じるなど
    }
}

Container.BindInterfacesAndSelfTo<FooLoader>().AsCached();
Container.Decorate<IAsyncLoadable<Foo>>()
         .With<LoadingIconDecorator>()  // FooLoader実行時にローディングアイコンを出すようにDecorate
         .AsCached();

実際便利かはわからないので誰かやってみて!

性質上、DecoratorContextとの相性はよいのでこちらも併せて使っていくといい感じに分離できておススメです。

https://github.com/svermeulen/Zenject#scene-decorators

ただ、Bind終わった後に動的なDecorateしたいなあという気持ちが個人的には強かったりする。シーン開始時は何もないけど、ゲーム中に動的にFactoryから生成されたものにDecorateされたりとか。その辺うまくやれないか考えてみよ。

InjectとInjectLocal attribute

Zenjectのソース読んでたらInjectLocalというAttributeが内部にあったのでなんだろうと調べたメモ。実用的なやつではない。

どのContainerからInjectされるのか

そもそも、Bindされたオブジェクトはどこから依存しているオブジェクトをもらえるのでしょう。これは基本的には自身がBindされているContainerになります。次のコードを見てみましょう。Fooに依存しているBarを生成するコードです。

public class Foo {
    public ContainerName { get; private set; }
    public Foo(string containerName) {
        ContainerName = containerName;
    }
}
public class Bar { 
    [Inject] Foo _foo;
    public void DoBar() => Debug.Log(_foo.ContainerName);
}
public class FooInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // 現在のContainerとProjectContextの両方でFooを用意しておく
        ProjectContext.Instance.Container.Bind<Foo>().AsCached().WithArguments("ProjectContext");
        Container.Bind<Foo>().AsCached().WithArguments("Scene");
        var subContainer = Container.CreateSubContainer();
        subContainer.Bind<Foo>().AsCached().WithArguments("Sub");

        // 現在のContainerにBarをBindする
        Container.Bind<Bar>();
        Container.Resolve<Bar>().DoBar();
    }
}
=> Scene

この場合、Barに渡されるFooインスタンスは同じContainerに所属しているインスタンスを渡されます。 同じContainerにFooが存在しない場合、親のContainerを探しに行きます。親も持っていなかったらさらにその親・・と最終的にProjectContextまで辿っていくことになります。逆に自身のSubContainerを見に行くなど下っていくことはありません。

上を辿っていくのが基本的なルールとして、どのくらいまで辿っていくのかという部分はAttributeやOptionで制御できたりします。

InjectLocal

InjectLocalをつけると親を辿りません。

public class Bar { 
    // 同じContainerからしか探さない
    [InjectLocal] Foo _foo;
    public void DoBar() => Debug.Log(_foo.ContainerName);
}
public class FooInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // 現在のContainerとProjectContextの両方でFooを用意しておく
        ProjectContext.Instance.Container.Bind<Foo>().AsCached().WithArguments("ProjectContext");
        // BarがいるContainerにはFooをBindしない
        //Container.Bind<Foo>().AsCached().WithArguments("Scene");
        var subContainer = Container.CreateSubContainer();
        subContainer.Bind<Foo>().AsCached().WithArguments("Sub");

        // 現在のContainerにBarをBindする
        Container.Bind<Bar>();
        Container.Resolve<Bar>().DoBar();
    }
}
=> ZenjectException: Unable to resolve 'Foo' while building object with type 'Bar'. Object graph:
Bar

自身の所属するContainerにはないのでそこでエラーが出てしまいます。

こんな感じで実は細かい設定ができたりします。Attributeの書き換えでもいいですし、Inject(Source = InjectSources.Local)といったInjectSources列挙体で渡すやりかたでも同じことができます。列挙体は以下の四つです。

  • Any
  • Local
  • Parent
  • AnyParent

InjectSources.Any

自身から探して、見つからなければ親を延々と辿っていきます。

通常のInjectがAnyに設定されているのでいつものやつです。

InjectSources.Local

上記の通り、自身のContainerだけを探して見つからなければエラーを吐きます。 InjectLocalの設定と同じです。

InjectSources.Parent

自身の直接の親のContainerを探しに行きます。自身と同じContainerに存在していても参照されません。親が持っていない場合エラーを吐きます。

InjectSources.AnyParent

自身の親を延々と辿っていきます。自身のContainerは参照しません。

参考

InjectSourcesのテストコード見るとなるほど感あります。

Zenject/TestInjectSources.cs at 0ea9690c9ec8ec8edeff7a65a202f7220ace839d · svermeulen/Zenject · GitHub

ちなみにZenject内部ではKernel内部の各種Managerが混ざらないようにInjectLocalで分けてるっぽい。

サブコンテナ使ったりProjectContextでBindしてるインスタンスと同一のものが必要になったりとかしたら明示的に分けていいかもしれませんね。スコープが狭まることはいいこと。

2018年ふりかえり

とはいえ、先月ふりかえったばっかりなのであまり書くことはないかもしれない。

adarapata.hatenablog.com

若干被ることも多いけど年末の行事なので書くことにした。なのでアニメとかゲームの話書こう。

仕事

一番はやはり転職が大きかった気がする

adarapata.hatenablog.com

WEB&ネイティブアプリからゲーム作るエンジニアになりました。果たして生きていけるのかと思ったけど、割といい感じにやってます。趣味での開発は案外有用だったので、RxとかZenjectとか導入したり、逆に業務やコンシューマでのアレコレを教えてもらったりしてます。まだ頭は追いついてないです。

また、初めてチームビルディング周りを勉強したりし始めました。前職でやってた人たちを思い出して見よう見まねから始まり、本を読んだり勉強会で話を聞いたりするなど。

あと、社内のLT大会のにぎやかしとして小ネタを喋ったりしていた。

イベント

社外でのLT、登壇は三つだけだった

あと、夏コミと技術書店でUnity関連の本を少し書いたりしていた。

上のイベントふりかえると全部Zenjectの話してる気がする。

イベントに関しては自分が登壇したのはそんなにないけど、会場の誘致をしたりなどしていた。

会場提供おじさん業を繰り返し行った結果、社内の人も気軽に勉強会見に来るようになったのでよい。

ゲーム

クリアしたものだけ列挙

スパロボXは久々の異世界系だったけど普通に楽しめた。ヴィルキスビルバインの二大切り込み隊長にはお世話になりました。個人的にはストーリーは暗めのVが好きだけど、ワタルのおかげ基本明るいのでそれはそれでよかった。Tは予約します。 世界樹はとにかくボリュームが多くて三か月くらいは時間かかったけど満足。基本脳筋戦術が好きなのでプリンスなどがバフかけまくってインペリアルの全力ドライブが最高に気持ちよかった。Vからだけど裏ボスも割と好きなパーティで戦えるようになったので楽しい。次回作はどうなるか。

あと、友人から勧められたアルトネリコ2が面白かった。システムが斬新で覚えるまでが苦労したけどリズムとRPGがいい感じに混ざっていてよかった。アドベンチャーパートは濃厚というかこれがやりたかったからRPG要素入れたんだなという勢いだった。クローシェルートクリアしたので二週目やろうと思ったけど引継ぎがないのが残念。

メタルマックスゼノは5週クリアしました。難易度ゴッドの最強の賞金首を倒したあたりで一旦ストップ。

アニメ・映画

一か月の無職期間にNetflix契約したのもあってだいぶ観ることが増えた。覚えてる奴だけ抜粋。

劇場で見て一番良かったのはバーフバリでした。2時間越えの長丁場だけど最後までバリバリバリバーフバリできるやつだった。アニゴジは賛否両論凄いけど個人的には全編ふりかえると悪くないなあと思った。怪獣プロレスはないけどそれはそれで。実はリアルタイムで見たのはゾンビランドサガとGRIDMANの二つだけ。これらは途中で評判聞いて観始めたら面白くて、気づいたら毎週楽しみにしていた。改めて見直したら、設定が奇抜でも結局王道な展開が好きなのかもしれない。GRIDMANもゾンビランドサガも最後は読めてるけど楽しめるやつだったし。

でも今期もやはりガルパンが最高でしたね。

大洗

5月と11月に二回行ってきた。最高だった。

まとめ

今年の頭に絵とか音楽とかやってみるぞ!と意気込んでみたが、蓋を開けてみればDIだの設計だのテストだの去年よりエンジニアよりのことに頭を使う一年でした。ただ楽しかったので来年はさらに特化していいかもしれない。ブログとかLTとかは引き続きやって行くけど、縁と機会があるなら他所にそういうZenjectアドバイスするおじさんとしてお邪魔しに行くのもありかもしれませんね。楽しそう。

今年もお疲れさまでした。

【年末だよ】Unity お・と・なのLT大会 2018」でLTしてきた

ポロリはしませんでした。

meetup.unity3d.jp

毎年年末に酒を飲みながらLTするイベントが行われているということでプレモル3杯目を飲みながらLTしてきた。

発表内容

発表資料はこちらでございます。

speakerdeck.com

ZenjectにはOptionalExtrasと呼ばれる便利機能がいっぱいあるので触ってみようぜ!という内容。

本文中でも書いてるけど、テストの件は以前にも発表しているのでこちらをご査収ください。

speakerdeck.com

Signalsはこっち

adarapata.hatenablog.com

個人的にはSignalは可能性が広がるので積極的に使ってほしい。MemoryPoolはまだ必要な場面に遭遇したことが無いのであまり言及できないのだった。所有アイテム一覧みたいな相当数のViewが必要な場合に活躍するかも?くらいのお気持ち。ReflectionBakingに関してはみなさんの検証をお待ちしております。

ZenjectのOptionは本当に悲しいくらい日本語記事無いので増やしていきたい所存。自分で書いていくでもいいけど寂しいのでこうやって普及させていく。 ちなみに僕の発表は後半で、すでに酒が入ってることもあったからか頭回ってるか怪しいところがいくつかありましたがご了承ください。Unity Material Learningに載るらしいので動画として酔っぱらった光景が保存されています。ご了承ください。

おまけ

LT会後の居酒屋での二次会でやっていきが高まった結果の所信表明です。

Zenject Signals

本記事は Unity Advent Calendar 2018 の 13日目 の記事です。

qiita.com

普段Zenjectを趣味で使っているのですが、Zenjectは基本的なDIContainer機能の他に、ちょっとDIから離れた便利機能をいくつか提供しています。MemoryPool TestFrameWork Reflection Baking Signal などです。今回はこれらの中で Signal の使い方について解説していきます。

Pub/Subの話

Signalsの話をする前に、Signalsの考え方であるPub/Subについて軽く解説していきます。

メッセージング

ゲームはイベント駆動が多いので、状態の変更を通知したくなるときが結構あります。ゲーム開始したらキャラクタを動かし始めたいとか、時間切れを知らせてプレイヤーの操作を受け付けなくしたりとか。そんなとき、通知元と通知先のクラスが必ずしも強い関係性を持っているとは限らなかったりします。

一時停止を実装しよう

例えば、マリオのようなアクションゲームにおいて一時停止を実装したくなったときのことを考えてみましょう。一時停止の概要は次の通りです。

  • 任意のボタンを押すと発動する
  • 一時停止状態はプレイヤーの動きが止まる。操作も受け付けない
  • 敵キャラも止まる
  • 残り時間のカウントも止まる
  • 一時停止が解除されると上記三つは再度動き始める
  • 画面描画はそのまま。キャラが見えなくなったりはしない

アクションゲームを作るうえで割と必須な機能です。ヒエラルキーのオブジェクトたちを一気にSetActiveして解決したくなりますが、それだと描画も消えてしまうのでここはちゃんとコードで実装することにしましょう。

一時停止を実装するにあたって、なにはともあれ「今が停止しているのか?」という情報を監視して取ってくる必要があります。今回は雑にゲーム中の入力を管理する GameInputManager と停止対象の Player を作ってみましょう。

public class GameInputManager
{
    public event Action Pause = delegate {  };
    public event Action Resume = delegate {  };
    private bool _isPause = false;    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            if (_isPause)
            {
                Resume();
            }
            else
            {
                Pause();
            }
            _isPause = !_isPause;
        }
    }
}

public class Player : MonoBehaviour
{
    [Inject] private GameInputManager _inputManager;

    private bool _isPause;
    void Start()
    {
        _inputManager.Pause += () => _isPause = true;
        _inputManager.Resume += () => _isPause = false;
    }

    void Update()
    {
        if (!_isPause)
        {
            // いつもの処理
        }
    }
}

GameInputManagerは内部で任意の条件(今回はAが押されたかどうか)で停止と再開を切り替えます。外部のオブジェクトはイベントにコールバックを登録することでそれらを検知し、自身の状態を更新します。ひとまずはこれで一時停止達成です。実装は割愛しますが敵キャラEnemyや時間管理クラスGameTimerも同様にGameInputManagerのイベントを購読すれば解決するでしょう。この状態を図で示すと以下の通りです。

Untitled-2

GameInputManagerのモテ期ですが、まだいける。

増えていくイベント

ところがこの一時停止するという概念、ボタンを押して停止したいという場合以外にも使いたくなってきたのでした。

  • ゲーム開始時なども一定時間止めたい
  • デバッグとして無条件にいつでも止めれるようにしたい

現在の一時停止は「ユーザが任意のタイミングで実行できる機能」ですが、今回追加される二つはゲーム側が自動で実行する機能だったり、開発者のみ使える機能だったりとタイミングもコンテキストも大きく異なります。これらを実装するとしたらどうすべきでしょうか。

一番手っ取り早いのは上記二つの実装をGameInputManagerに突っ込むことなのですが、これはそもそもユーザ入力でもないしクラスの責務が多くなってしまい最強のManagerクラスが爆誕してしまうでしょう。それはつらい。

素直に各クラスを作るのが一つの手でしょう。試しにゲーム開始時の一時停止を入れてみましょう。何秒止めるとかは割と演出の都合にもなりそうなのでGameDirectorとかそのあたりに書かれるかもしれません。

public class Player : MonoBehaviour
{
    [Inject] private GameInputManager _inputManager;
    // ゲームの演出管理してるやつ
    [Inject] private GameDirector     _director;
    private bool _isPause;
    void Start()
    {
        _inputManager.Pause += () => _isPause = true;
        _inputManager.Resume += () => _isPause = false;
        // 演出の一時停止も購読だ!
        _director.Pause += () => _isPause = true;
        _director.Resume += () => _isPause = false;
    }

    void Update()
    {
        if (!_isPause)
        {
            // いつもの処理
        }
    }
}

3行追加で済んだのでまだ傷は浅そうですね。EnemyもGameTimerも同様に3行追加すれば実装できるでしょう。この場合の依存の状態を図示すると次のような感じです。

Untitled-2

ちょっと雲行きが怪しくなってきました。ここからデバッグ用のクラスも作るとなるともう一つ依存が増えるので組み合わせ爆発がえらいことになりそうですね。一時停止可能なクラスが増えるたびに手を加えることになってしまいます。

Pub/Subメッセージングモデル

今回の要件において、PlayerGameInputManagerに依存すべきなのでしょうか。(プレイヤーなら他のキー入力もあるから必要だろ!というのは一旦置いといてください・・) 実際欲しいのは一時停止が起きたというイベントだけで、ユーザが一時停止ボタンを押したから止まったというGameInputManagerのコンテキストは特に求めていません。デバッグ時の停止も演出での停止も同様で、それらの何故発生したかという文脈は今回必要としていません。今回のような不特定多数が発行するイベントを不特定多数が購読するPub/Subの場合、直接依存関係を持つと拡張しづらいという問題が発生します。

なので、このようなPub/Subを実現するときは中央に仲介人(Broker)を構えることになります。

image

イベント発行側はBrokerにメッセージを送る(Publish)して、購読側はBrokerからイベントを受け取る(Subscribe)仕組みです。この構成は購読側が発行側をまったく意識しなくてよくなるのでスケーラブルな点がメリットです。今後どれだけイベントの発行側が増えても購読側に影響はありません。

Unityにおいては、UniRxのMessageBrokerが有名です。

github.com

サッと導入できて非常に便利なのでおススメです。

Zenject Signal

前置きが長くなりましたが、ZenjectではSignalという機能を提供しています。

Zenject/Signals.md at master · svermeulen/Zenject · GitHub

これはZenjectのDIを利用している場合に使えるPub/Sub機構です。

とりあえず書き換えてみる

最短ルートでサッと書いてみましょう。 Zenjectを利用するので、登場人物は全てZenject BindingなどでBindされているものとします。

購読側

// イベントの種類をクラスにしてラベリング
public class PauseSignal { }
public class ResumeSignal { }

public class Player : MonoBehaviour
{
    private bool _isPause;
    // Signalを受け取るメソッド
    public void OnPause(PauseSignal signal) => _isPause = true;
    public void OnResume(ResumeSignal signal) => _isPause = false;

    void Update()
    {
        if (!_isPause)
        {
            // いつもの処理
        }
    }
}

まず、今回からPlayerはイベントがどこから来るのかわからなくなります。なのでイベント自体をクラスにして明示的にしてあげます。今回は停止イベントをPauseSignal、再開イベントをResumeSignalとしました。 PlayerはManagerなどの依存がなくなる代わりに、イベントの受け皿としてOnPause OnResume メソッドを定義してあげます。

発行側

public class GameInputManager
{
    // Zenjectが用意したBroker
    [Inject] private SignalBus _signalBus;

    private bool _isPause = false;
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            if (_isPause)
            {
                // PauseEventを発行する
                _signalBus.Fire<PauseSignal>();
            }
            else
            {
                _signalBus.Fire<ResumeSignal>();
            }
            _isPause = !_isPause;
        }
    }
}

変更点としては、eventがなくなり SignalBus というものが現れます。これはZenjectが提供するBrokerで、このインスタンスを通してイベントを発行したり購読ができるようになります。コールバックを呼んでいた部分はFire<T>で任意のイベントをSignalBusに発行する処理に変わります。

Installer

public class SignalInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // SignalBusを利用する
        SignalBusInstaller.Install(Container);
        // Signalを定義する
        Container.DeclareSignal<PauseSignal>();
        Container.DeclareSignal<ResumeSignal>();
        // Signalを受け取った際の処理
        Container.BindSignal<PauseSignal>().ToMethod<Player>(p => p.OnPause).FromResolve();
        Container.BindSignal<ResumeSignal>().ToMethod<Player>(p => p.OnResume).FromResolve();
    }
}

基本的には以下の流れです。

  1. Signalを利用するためにSignalBusInstallerをインストールする
  2. 利用したいSignalをDeclareSignal<T>で定義する
  3. BindSignal<T>().ToMethod<T2>でSignalを受け取った場合のT2の処理を定義

1,2は基本的に毎回同じです。3は場合によって異なりますが、だいたいはメソッドを呼んでそのままSignalを渡すことになるでしょう。また、今回はPlayerがBind済みなので FromResolve() でBind済みのPlayerに流すようにしています。これにより、発行側がSignalBusにFireしてくれればPlayerは全て受け取ることができるようになりました。

このままEnemyもGameTimerもBindSignal.ToMethodすればいいのですが、それだと結局何回も定義することになり面倒です。せっかくZenjectを使っているのでインタフェースでまとめてあげるとだいぶ楽ができます。

public interface IPauseable
{
    void OnPause(PauseSignal signal);
    void OnResume(ResumeSignal signal);
}

BindSignalは以下に書き換えます。

Container.BindSignal<PauseSignal>().ToMethod<IPauseable>(p => p.OnPause).FromResolveAll();
Container.BindSignal<ResumeSignal>().ToMethod<IPauseable>(p => p.OnResume).FromResolveAll();

FromResolveAllは任意の型のバインドオブジェクト全てを取ってくるので、PlayerやEnemyにIPauseableを実装してあげれば自動的にSignalを受け取れるようになります。

動的な購読

先ほどの例は最初からバインドされている状態なので、動的に生成されるインスタンスには適用できません。その場合はSignalBusを受け取って手動で購読する必要があります。

public class DynamicSpawnEnemy : MonoBehaviour
{
    private SignalBus _signalBus;

    [Inject]
    void Initialize(SignalBus signalBus)
    {
        _signalBus = signalBus;
        _signalBus.Subscribe<PauseSignal>(OnPause);        
    }

    void OnDestroy()
    {
        // 明示的に購読解除しないと残りっぱなしになる
        _signalBus.UnSubscribe<PauseSignal>(OnPause);
    }

    private void OnPause(PauseSignal signal) => print("Pause!!");
}

SignalBus.SubscribeSignalBus.UnSubscribeで購読及び解除ができます。手動で破棄が必要なのでうっかり忘れないようにしましょう。

また、ZenjectはUniRxとの連携ができます。その場合、IObservableへの変換が可能です。

_signalBus.GetStream<PauseSignal>() // IObservable<PauseSignal>に変換
          .Subscribe(OnPause)
          .AddTo(gameObject);

UniRx使ってる場合はこっちの方が管理しやすいかもしれません。

Zenject Signalsの特徴

SignalsはDIしているという条件はありますが、割と細かく面白い機能がついてます。

Subscribeを必須にできる

シグナルを発行したときに購読者の有無で挙動を変えられます。

// イベント発行時に誰もSubscribeしていないなら例外を吐く
Container.DeclareSignal<PauseSignal>().RequireSubscriber()
// 誰もSubscribeしてなくても動作する(デフォルト設定)
Container.DeclareSignal<PauseSignal>().OptionalSubscriber()
// 誰もSubscribeしてない場合Warningを出す
Container.DeclareSignal<PauseSignal>().OptionalSubscriberWithWarning()

必ずSubscriberがいるなら積極的にRequireSubscriberにしておくと早期にエラー発見できて便利

定義してないSignalを発行したときの挙動

原則として、DeclareSignalで定義してないシグナルを発行することはできず、Fireを呼んだときに例外を吐きます。もし状況に応じて定義されるかわからないシグナルが存在する場合、例外を無視するTryFireを代わりに使うと良いでしょう。

// 例外を吐く
_signalBus.Fire<NoDeclareSignal>(); 
// 定義されていない場合Warningを吐いて無視する
_signalBus.TryFire<NoDeclareSignal>();

Signalにpriorityを設定できる

通常、同一フレーム内に複数のSignalが発行された場合、それらは順番に処理されていきます。

Container.DeclareSignal<FooSignal>();
Container.DeclareSignal<BarSignal>();
// Signalが来たらLogに吐く
Container.BindSignal<FooSignal>().ToMethod(_ => Debug.Log("foo"));
Container.BindSignal<BarSignal>().ToMethod(_ => Debug.Log("bar"));

var bus = Container.Resolve<SignalBus>();
// 雑に交互に発行する
bus.Fire<FooSignal>();
bus.Fire<BarSignal>();
bus.Fire<FooSignal>();
bus.Fire<BarSignal>();

実行結果は以下です。

foo
bar
foo
bar

これらは同期的に処理されるため、Fireされた順番にログが吐かれています。Zenject Signalsはこの処理を非同期に、且つSignalに優先順位を付けてさばくことができます。

// FooSignalの優先度は0
Container.DeclareSignal<FooSignal>().RunAsync().WithTickPriority(0);
// BarSignalの優先度は1(FooSignalより優先度が低い)
Container.DeclareSignal<BarSignal>().RunAsync().WithTickPriority(1);
// Signalが来たらLogに吐く
Container.BindSignal<FooSignal>().ToMethod(_ => Debug.Log("foo"));
Container.BindSignal<BarSignal>().ToMethod(_ => Debug.Log("bar"));

var bus = Container.Resolve<SignalBus>();
// 雑に交互に発行する
bus.Fire<FooSignal>();
bus.Fire<BarSignal>();
bus.Fire<FooSignal>();
bus.Fire<BarSignal>();

結果は以下の通りです。

foo
foo
bar
bar

WithTickPriorityで設定したとおり先にFooSignalが処理されています。これは個人的には面白い機能だと思っていて、必ず実行順が固定されなければならない場合に効果を発揮します。例えばキャラが死んだイベントと、それとは別でキャラにアクセスするようなイベントが同時に発生したとき、実行順番によってはすでにDestroyしたインスタンスにアクセスしてエラーでちゃうなんてことがたまーーーにあったりして、0.1msずらしDestroyしたりとかyield return nullしたりRxでNextFrame.Subscribeしたりしてお茶を濁すようなことをしなくて済みます。

Signalsのメリット

個人的にはスコープが狭まることが大きいかなと考えてます。

Pub/Subのメリットは先に述べたとおりですが、一方で疎結合故に「どこからイベントが発行されるのか把握しにくい」という問題もあります。最初は問題ないけど、プロダクトの規模が大きくなるにつれあらゆる個所からイベントが発生する可能性を考慮することになります。とある1画面を改修して問題ないと思ったら、全く想定していないバックエンドで動いていた何かから突然イベントが飛んできて謎・・・みたいなことになりがちです。且つ、便利故に「データの渡し方に困ったらとりあえずBrokerに送ってSubscribeでいいか」みたいな判断を取ってしまう危険性もあります。実質グローバルな何かに依存しているみたいな状況です。 Zenject Signalsの良いところは影響範囲が同一のDIContainerだけだという点です。Containerに登録されているSignalしか発行できず、Containerはシーン単位、またはサブコンテナなど細かい粒度で構築できるため、遠いところからSignalを発行するのは難易度が高いです。この制限のおかげで安心して使っていけるのかなと思います。 もちろん、ProjectContextにSignalを定義してしまえば超便利になる代わりに完全にグローバルになって問題が再燃してしまうので使いどころはよく見極めましょう。

最後に

Zenject Signalは色々条件はありますが割と面白い機能です。Zenjectを使っていて通知周りに困っていたらぜひ使ってみてください。 それ以外にもZenjectのオプションたちは便利なものが多いので興味があれば調べてみてください。そしてブログを書いてください。僕が喜びます。