ZenjectのConstructionMethodたち
ZenjectでBindするとき、そのインスタンスをどのように用意するかという設定が必要です。その場で初期化するのか、既存のインスタンスを引っ張ってくるのかなど。これらはコンストラクションメソッドと呼ばれており、READMEに全部書かれています。
割と数が多いのでまとめてみました。間違っているものがあればコメントなどもらえるとありがたいです。
- FromNew()
- FromInstance(T instance)
- FromMethod(Func<T> method)
- FromMethod(Func<InjectContext, T> method)
- FromMethodMultiple(Func<IEnumerable<T>> method)
- FromMethodMultiple(Func<InjectContext, IEnumerable<T>> method)
- FromFactory<IFactory<T>>()
- FromIFactory(Action<ConcreteBinderGeneric<IFactory<T>>> factoryBindGenerator)
- FromComponentInNewPrefab(UnityEngine.Object prefab)
- FromComponentsInNewPrefab(UnityEngine.Object prefab)
- FromComponentInNewPrefabResource(string path)
- FromComponentsInNewPrefabResource(string path)
- FromNewComponentOnNewGameObject()
- FromNewComponentInNewPrefab(UnityEngine.Object prefab)
- FromNewComponentInNewPrefabResource(string path)
- FromNewComponentOn(GameObject gameObject)
- FromNewComponentOn(Func<InjectContext, GameObject> gameObjectGetter)
- FromNewComponentSibling()
- FromNewComponentOnRoot()
- FromResource(string path)
- FromScriptableObjectResource(string path)
- FromNewScriptableObjectResource(string path)
- FromComponentInHierarchy()
- FromComponentsInHierarchy()
- FromComponentSibling()
- FromComponentsSibling()
- FromComponentInParents()
- FromComponentsInParents()
- FromComponentInChildren()
- FromComponentsInChildren()
- FromResolve()
- FromResolveAll()
- FromResolveGetter<T>(Func<T, T2> getter)
- FromResolveAllGetter<T>(Func<T, T2> getter)
- FromSubContainerResolve()
- FromSubContainerResolveAll()
- SubContainerのConstruction Method
- ByNewPrefabMethod(UnityEngine.Object prefab, Action<DiContainer> installerMethod)
- ByNewPrefabInstaller<TInstaller>(UnityEngine.Object prefab)
- ByNewPrefabResourceMethod(string resourcePath, Action<DiContainer> installerMethod)
- ByNewPrefabResourceInstaller<TInstaller>(string resourcePath)
- ByNewGameObjectInstaller<TInstaller>()
- ByNewGameObjectMethod(Action<DiContainer> installerMethod)
- ByMethod(Action<DiContainer> installerMethod)
- ByNewGameObjectInstaller<TInstaller>()
- ByNewContextPrefab(UnityEngine.Object prefab)
- ByNewContextPrefabResource(string resourcePath)
- ByInstance(DiContainer subContainer)
- おわりに
- 追記
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パターンについてはこちらも合わせてどうぞ。
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がアタッチされているのがポイントです。IInitializable
やITickable
なども適切に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で書いた記事を転載したものです。
UNIBOOK10、いい話がいっぱいあるから買ってね!
Zenject Factory
ZenjectはUnityで動作するDIライブラリです。煩わしい依存関係の解決を助ける非常に強力な武器となりますが、 強力かつ多機能であるためどこから学んでいくべきか悩むことも多いです。公式のドキュメントはとても充実していますが、前提となる知識が多かったり、基本的なBindingの話以外は日本語での資料が少ないというのも理由の1つでしょう。
この記事では、ZenjectがもつFactoryによる動的なオブジェクト生成について簡単な解説を行います。
動的に生成したインスタンスへのインジェクション
シーン読み込み時にInstallerを実行して、オブジェクトをバインドし依存関係を解決していくというのがZenjectのスタンダードな使い方でしょう。Containerはシーン開始時の1回のみインジェクションしてくれます。
しかし、実際の開発においてすべてのインスタンスが初期化時に存在するとは限りません。たとえば、アクションゲームで敵キャラが動的に生成されることは十分に考えられるでしょう。
次の例で考えてみましょう。
- Enemyは動的に生成される
- Enemyはプレイヤーに依存している
- PlayerはContainerにバインドされている
- 敵の種類は今後増える可能性がある
最低限のコードで書くとこうなります。
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
閑話:空のFactoryは必要か
毎回PlaceholderFactory<T>
を継承しただけのFactoryクラスを定義していますが、厳密には定義しなくてもバインドはできます。
次のようにPlaceholderFactoryを直接バインドすることも可能です。
Container.BindFactory<Enemy, PlaceholderFactory<Enemy>>();
メンバ変数で持つときも同様に直接PlaceholderFactory
しかし、ドキュメントではこれを非推奨としています。理由は次の二点です。
- パラメータの変更があった場合にコンパイルエラーが発生しないので気づくのが遅くなる
PlaceholderFactry<T>
よりT.Factory
の方が可読性が高いと考えている。
たとえば、PlaceholderFactory
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
上記の理由から、基本的にはEnemy.FactoryのようにネストしたFactoryを作ることが推奨されています。 特に強い理由がなければその都度定義した方がよいでしょう。
終わりに
Zenjectは強力ですがその分難易度も高いと言われています。特に動的なインスタンスのInjectはやり方をしっかり考えないとContainerを各所に引きずり回してインジェクションなどをやってしまいがちです。これはコードをより難解なものにしてしまうので、Factoryを用いた設計を覚えてシンプルな仕組みに書き直していくのがよいでしょう。
Zenject Decorator Binding
ZenjectにはDecoratorパターンをサポートするDecorator Bindingなる機能があったりします。あんまりメインで使うような機能ではありませんが覚えておくと便利かもしれません。
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>
といった感じで一行で実装できます。便利。この場合ResolveAttackUpDecorator
です。
どう使うの?
上記のような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内部ではKernel内部の各種Managerが混ざらないようにInjectLocalで分けてるっぽい。
サブコンテナ使ったりProjectContextでBindしてるインスタンスと同一のものが必要になったりとかしたら明示的に分けていいかもしれませんね。スコープが狭まることはいいこと。
2018年ふりかえり
とはいえ、先月ふりかえったばっかりなのであまり書くことはないかもしれない。
若干被ることも多いけど年末の行事なので書くことにした。なのでアニメとかゲームの話書こう。
仕事
一番はやはり転職が大きかった気がする
WEB&ネイティブアプリからゲーム作るエンジニアになりました。果たして生きていけるのかと思ったけど、割といい感じにやってます。趣味での開発は案外有用だったので、RxとかZenjectとか導入したり、逆に業務やコンシューマでのアレコレを教えてもらったりしてます。まだ頭は追いついてないです。
また、初めてチームビルディング周りを勉強したりし始めました。前職でやってた人たちを思い出して見よう見まねから始まり、本を読んだり勉強会で話を聞いたりするなど。
あと、社内のLT大会のにぎやかしとして小ネタを喋ったりしていた。
イベント
社外でのLT、登壇は三つだけだった
- Zenjectとテスト - Gotanda.unity #5
- Unityプロダクトにテストを導入していくまで - Unityテスト完全に理解した
- Zenject Optionalアレコレ -【年末だよ!】UnityおとなのLT大会
あと、夏コミと技術書店でUnity関連の本を少し書いたりしていた。
上のイベントふりかえると全部Zenjectの話してる気がする。
イベントに関しては自分が登壇したのはそんなにないけど、会場の誘致をしたりなどしていた。
- Unity テスト完全に理解した
- Gotanda.unity #7
- Unity Zenject完全に理解した
- 大八耐2018 in 東京
- Unity ECS完全に理解した
- Unity Network 完全に理解した
会場提供おじさん業を繰り返し行った結果、社内の人も気軽に勉強会見に来るようになったのでよい。
ゲーム
クリアしたものだけ列挙
スパロボXは久々の異世界系だったけど普通に楽しめた。ヴィルキスとビルバインの二大切り込み隊長にはお世話になりました。個人的にはストーリーは暗めのVが好きだけど、ワタルのおかげ基本明るいのでそれはそれでよかった。Tは予約します。 世界樹はとにかくボリュームが多くて三か月くらいは時間かかったけど満足。基本脳筋戦術が好きなのでプリンスなどがバフかけまくってインペリアルの全力ドライブが最高に気持ちよかった。Vからだけど裏ボスも割と好きなパーティで戦えるようになったので楽しい。次回作はどうなるか。
あと、友人から勧められたアルトネリコ2が面白かった。システムが斬新で覚えるまでが苦労したけどリズムとRPGがいい感じに混ざっていてよかった。アドベンチャーパートは濃厚というかこれがやりたかったからRPG要素入れたんだなという勢いだった。クローシェルートクリアしたので二週目やろうと思ったけど引継ぎがないのが残念。
メタルマックスゼノは5週クリアしました。難易度ゴッドの最強の賞金首を倒したあたりで一旦ストップ。
アニメ・映画
一か月の無職期間にNetflix契約したのもあってだいぶ観ることが増えた。覚えてる奴だけ抜粋。
- キングコング髑髏島の巨人
- インターステラー
- キャビン
- マッキー
- バーフバリ~王の凱旋~
- 殿、利息でござる
- ポプテピピック
- キルラキル
- 王立宇宙軍 オネアミスの翼
- トップをねらえ!1&2合体版
- マジンカイザーSKL
- バキ
- 逆シャア
- アニゴジ三部作
- 未確認で進行形
- Devilman Crybaby
- NEW GAME!2期
- クレヨンしんちゃん逆襲のロボとーちゃん
- クレヨンしんちゃんモーレツオトナ帝国
- クレヨンしんちゃんアッパレ戦国大合戦
- クレヨンしんちゃん嵐を呼ぶジャングル
- ゾンビランド・サガ
- SSSS.GRIDMAN
劇場で見て一番良かったのはバーフバリでした。2時間越えの長丁場だけど最後までバリバリバリバーフバリできるやつだった。アニゴジは賛否両論凄いけど個人的には全編ふりかえると悪くないなあと思った。怪獣プロレスはないけどそれはそれで。実はリアルタイムで見たのはゾンビランドサガとGRIDMANの二つだけ。これらは途中で評判聞いて観始めたら面白くて、気づいたら毎週楽しみにしていた。改めて見直したら、設定が奇抜でも結局王道な展開が好きなのかもしれない。GRIDMANもゾンビランドサガも最後は読めてるけど楽しめるやつだったし。
でも今期もやはりガルパンが最高でしたね。
大洗
5月と11月に二回行ってきた。最高だった。
まとめ
今年の頭に絵とか音楽とかやってみるぞ!と意気込んでみたが、蓋を開けてみればDIだの設計だのテストだの去年よりエンジニアよりのことに頭を使う一年でした。ただ楽しかったので来年はさらに特化していいかもしれない。ブログとかLTとかは引き続きやって行くけど、縁と機会があるなら他所にそういうZenjectアドバイスするおじさんとしてお邪魔しに行くのもありかもしれませんね。楽しそう。
今年もお疲れさまでした。
【年末だよ】Unity お・と・なのLT大会 2018」でLTしてきた
ポロリはしませんでした。
毎年年末に酒を飲みながらLTするイベントが行われているということでプレモル3杯目を飲みながらLTしてきた。
発表内容
発表資料はこちらでございます。
ZenjectにはOptionalExtrasと呼ばれる便利機能がいっぱいあるので触ってみようぜ!という内容。
本文中でも書いてるけど、テストの件は以前にも発表しているのでこちらをご査収ください。
Signalsはこっち
個人的にはSignalは可能性が広がるので積極的に使ってほしい。MemoryPoolはまだ必要な場面に遭遇したことが無いのであまり言及できないのだった。所有アイテム一覧みたいな相当数のViewが必要な場合に活躍するかも?くらいのお気持ち。ReflectionBakingに関してはみなさんの検証をお待ちしております。
ZenjectのOptionは本当に悲しいくらい日本語記事無いので増やしていきたい所存。自分で書いていくでもいいけど寂しいのでこうやって普及させていく。 ちなみに僕の発表は後半で、すでに酒が入ってることもあったからか頭回ってるか怪しいところがいくつかありましたがご了承ください。Unity Material Learningに載るらしいので動画として酔っぱらった光景が保存されています。ご了承ください。
おまけ
LT会後の居酒屋での二次会でやっていきが高まった結果の所信表明です。
来年はMicrosoft MVP目指すかーという気持ちになった
— いも@efb~相手は死ぬ~ (@adarapata) December 15, 2018
Zenject Signals
本記事は Unity Advent Calendar 2018 の 13日目 の記事です。
普段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
のイベントを購読すれば解決するでしょう。この状態を図で示すと以下の通りです。
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行追加すれば実装できるでしょう。この場合の依存の状態を図示すると次のような感じです。
ちょっと雲行きが怪しくなってきました。ここからデバッグ用のクラスも作るとなるともう一つ依存が増えるので組み合わせ爆発がえらいことになりそうですね。一時停止可能なクラスが増えるたびに手を加えることになってしまいます。
Pub/Subメッセージングモデル
今回の要件において、Player
はGameInputManager
に依存すべきなのでしょうか。(プレイヤーなら他のキー入力もあるから必要だろ!というのは一旦置いといてください・・)
実際欲しいのは一時停止が起きたというイベントだけで、ユーザが一時停止ボタンを押したから止まったというGameInputManager
のコンテキストは特に求めていません。デバッグ時の停止も演出での停止も同様で、それらの何故発生したかという文脈は今回必要としていません。今回のような不特定多数が発行するイベントを不特定多数が購読するPub/Subの場合、直接依存関係を持つと拡張しづらいという問題が発生します。
なので、このようなPub/Subを実現するときは中央に仲介人(Broker)を構えることになります。
イベント発行側はBrokerにメッセージを送る(Publish)して、購読側はBrokerからイベントを受け取る(Subscribe)仕組みです。この構成は購読側が発行側をまったく意識しなくてよくなるのでスケーラブルな点がメリットです。今後どれだけイベントの発行側が増えても購読側に影響はありません。
Unityにおいては、UniRxのMessageBrokerが有名です。
サッと導入できて非常に便利なのでおススメです。
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(); } }
基本的には以下の流れです。
- Signalを利用するために
SignalBusInstaller
をインストールする - 利用したいSignalを
DeclareSignal<T>
で定義する 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.Subscribe
、SignalBus.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のオプションたちは便利なものが多いので興味があれば調べてみてください。そしてブログを書いてください。僕が喜びます。