Zenjectの導入

UnityのためのDependency Injection フレームワークにZenjectというものがある。
つい最近知ったものだけれども、
使えるシーンが多いと思うので現状把握している範囲でまとめておこうと思う。

Github
GitHub - modesttree/Zenject: Dependency Injection Framework for Unity3D

Asset store
https://www.assetstore.unity3d.com/jp/#!/content/17758

知ったきっかけはこちらのAmingさんの開発者ブログ
Unity3DのDIフレームワーク、Zenjectの紹介 | Aiming 開発者ブログ
via Unityまとめ

Unityを日頃使っていて、
なんとなく感じている痒いところの解決の助けになりそうに思っていて、
例えば、どうしてもシングルトンを使ってしまっているところであるとか、
管理用のゲームオブジェクトがシーンに配置されていて、
SerializeFieldに必要なオブジェクトを配置していて、それがいつの間にか外れていて
実行時にExceptionで発覚してギャフン、みたいなのを
軽減させる手段に成りえるかと感じた。

Introduction

GithubのIntroductionのおおまかな訳。

ZenjectはUnityのための、軽量なDependency Injectionフレームワークです。
(ただUnity以外で使うこともできます)
これは貴方のアプリケーションを、
とても分断されたレスポンシビリティをもつ疎結合なパーツの集合にすることができます。
Zenjectは、スケーラブルで最高にフレキシブルな方法で、
容易に書けて、再利用でき、リファクタ、テストできる多くのコンフィグレーションにより、
各パーツをのりづけ(関連付け)できます。

すごそうな雰囲気だが、
まだこれだけだと良く分からないかも。

Dependency Injection

そもそもの話として、
僕はDIコンテナというものは使ったことがなく、今回初めて触れた。
なので、Zenjectの機能の範囲でDIの領域からはみ出ている部分に触れていることもあるかもしれない。
また、DIとしてのベストプラクティスを踏み外している部分や、
やや的を外しているかも。

DIについて調べた確認したページなども書いておく。
DIコンテナの本当の使いどころ | 技術トピックス | ウルシステムズ株式会社
2005年なので随分前の記事。DIの適さない使い方などにも触れられている。
分野によっては前から普通に使われていたのだなと、
自分は不勉強だったなと思った。

c# - DIコンテナを使うメリットが分からない - スタック・オーバーフロー
StackOverflowで、回答がわかりやすいなと。

依存性の注入 - Wikipedia
Wikipedia
上記のStackOverflowにもリンクがあった。
DIという用語を作りだしたのはMartin Fowler、そうだったのか。

Inversion of Control Containers and the Dependency Injection pattern
Martin Fowlerのブログ。
検索したらちゃんと出てきた。
2004年の記事。


ある程度見ていくと、既存のDIについてはJavaphp
C#もあるけど、
Unityというゲームエンジンで動作するフレームワークとは
少し前提条件が違うようにも思うが、
パターンとして考え方を整えるために色々読んでおくのは良いかと。

また、ZenjectのReadmeにも、
DIを使う理由については行数を割いて書かれていて、
https://github.com/modesttree/Zenject#theory
https://github.com/modesttree/Zenject#misconceptions
のあたりで書かれている。

サービスを使うにあたって、
直に生成するよりかはコンストラクタなどでインターフェースを受けるほうが柔軟で、
それをさらに推し進めると、その依存性の解決は最初にやっちゃえばいいよね、
という感じだろうか。ざっくり言うと。

Pokémon Goでの事例

ZenjectはポケモンGoでも採用されている。
Unite LA 2016でのトークの動画が上がっていて、
全般的にDIを採用したアーキテクチャについて話している。
www.youtube.com

  • Unityを長く使っていて、いつもとっ散らかっちゃうけど今回はすごくきれいにできた
  • 既存のゲームデザインとは異なるため多くのイテレーションを必要とし、柔軟なアーキテクチャが必要だった
  • Testについて。
  • ゲームステートごとにInstallerがある。
  • InstallPrefabというAttributeを別途作成・使用している
  • Q&Aにて、パフォーマンスに関してはキャッシングが効いてるから大丈夫と思う、みたいなことを言っている

英語なので、少し怪しいが。

サンプル

まず Hello World Exampleを試してみる。

https://github.com/modesttree/Zenject#hello-world-example

ヒエラルキービューで右クリック 
Zenject -> Scene Context
でSceneContextオブジェクトを生成、配置する。
f:id:kurihara-n:20161227000604g:plain

Projectビューの右クリックからCreate -> Zenject -> MonoInstaller
でInstallerとなるスクリプトを作成する。
ファイルダイアログが開くので、TestInstaller.csという名前で保存する。
f:id:kurihara-n:20161227001830g:plain
この作ったTestInstaller.csに、
ZenjectのReadmeに載っているコードをコピペか、同じように記述する。

TestInstallerをシーンに配置する。(ここではSceneContextにAddComponentしちゃう)
Scene ContextのInstallerプロパティに、
TestInstallerを追加する。
f:id:kurihara-n:20161227001853g:plain

エディタ実行をすると、ログにHello Worldと出力される。
また、Ctrl + Shift + Vで、バリデーションがされて、
問題があればエラーを出してくれる。

オブジェクトの生成

いくつかシンプルなケースをいくつか試してみる。
プレハブをもとにオブジェクトを生成したい場合、
従来であればInstantiateを呼び出すことになるが、
ZenjectではFactoryをBindすることができる。

ここではSceneContextにSerializeFieldでプレハブを受けるInstallerを用意する。
f:id:kurihara-n:20161228012822p:plain

サンプル同様にInstallerをSceneContetのプロパティに設定しておく。
Installerのコードは以下のようになっている。

using UnityEngine;
using Zenject;

public class InstancingInstaller : MonoInstaller<InstancingInstaller>
{
    [SerializeField]
    GameObject cubePrefab;

    public override void InstallBindings()
    {
        Container.BindFactory<Cube, Cube.Factory>().FromPrefab(cubePrefab);
        Container.Bind<ITickable>().To<InstancingManager>().AsSingle();
        Container.Bind<InstancingManager>().AsSingle();
    }
}

FromPrefabからFactoryをBindしている。

cubePrefabはCubeコンポーネントを持っていて、そのなかでFactoryを定義している

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Zenject;

public class Cube : MonoBehaviour
{
    public class Factory : Factory<Cube>
    {
    }
    
	void Start () {
        //...
	}
	
	void Update () {
		//...
	}
}

ITickableを実装したInstancingManagerというクラスからFactoryを使う。
Instancingmanagerは以下。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Zenject;

public class InstancingManager : ITickable
{
    Cube.Factory factory;

    public InstancingManager(Cube.Factory factory)
    {
        this.factory = factory;
    }

    void ITickable.Tick()
    {
        //if(someCondition)
        //{
        //factory.Create();
        //}
    }
}

factory.Create()を呼べば、オブジェクトが生成される。

ScriptableObjectInstaller

Readmeに書いてあるものからスクリプタブルオブジェクトの利用について。
https://github.com/modesttree/Zenject#scriptable-object-installer

Scriptable Object InstallerでScriptable Objectを利用できる。
右クリックからCreate -> Zenject -> Scriptable Object Installer
ファイル保存ダイアログがでるので、任意の名前をつける。(この例だとGameSettingsInstaller)
このScriptableObjectInstallerはScriptableObjectを継承しているので、
エディタで設定したいプロパティはここに持たせる。
エディタスクリプトにより、このScriptableObjectが生成できるようになっており、
Create -> Installers -> ScriptableObjectInstaller
で作る。
これをSceneContextに追加れば使用できる。
BindはContainer.BindInstance()で行う。

Scene Bindings

これもReadmeにあるものから。
https://github.com/modesttree/Zenject#scene-bindings

すでにシーンに配置してあるオブジェクト/MonoBehaviourを、Bindingさせる方法。
Unityだとシーンに配置させるケースは多々あるので、
それをBindingして受けれるようにする方法。

オブジェクトにZenject Bindingコンポーネントをアタッチし、
Componentsに、そのオブジェクトが持っているBindしたいコンポーネントを追加。

これだけで、[Inject]であったり,ITickableのコンストラクタなりで受けることができる。
[Inject]はListで受けることもできるので、
Amingさんの開発ブログの例もそうだが、
シーンに配置したいくつかのオブジェクトにZenjectBindingを設定しておくと、
オブジェクトのリストが実行時に作られている状態になる。

これは便利。
同じようなことをやるときにオブジェクトのStartなりでマネージャに渡したりしていたのが手間が省けるし、
場合によってはGameObject.FindObjectsOfTypeなどで取得をしたりしていたものが、
このリストでとれるようになる。
まあ、動的にも生成されるようなものはPrefabから生成したさいに別途登録するような使い方にはなると思う。



いくつかのケースを書いてみた。
ITickableなどを用いていると、
Unityの標準のUpdateと別にタスクが回るようなものなので、
従来の使いかたに慣れ切っていると、
少し導入に障害を感じるかもともおもう。
ただ、色々とスッキリかけるので利点は感じる。
もっと使っていきたいと思う。

利用者が増えて日本語でも情報が増えるとうれしい。