C#
Unity
DI
Zenject

ZenjectでDIの真理を悟る

More than 1 year has passed since last update.

現在グレンジでUnityを用いたゲーム開発を行っているみくりやと申します。
グレンジ Advent Calendar 2017 3日目の記事になります。
Unityについてのホットな話題を探していたところ Zenject にたどり着き、面白そうだったので
少し触ってみようと思います。
Zenject Dependency Injection IOC (AssetStore)

Zenjectとは

一言で言うと「 Unityを対象としたDIフレームワーク 」となります。そして無料(MITライセンス)。
ポケモンGOでも採用されているようです。
詳しい説明はgithubのREADMEを読めばわかりやすいかもしれません。
github

DIとは

Dependency Injection。しばしば日本語で「 依存性の注入 」と訳されます。
正直DIという単語自体「入社直後の研修でやったな」くらいの記憶しかなく、依存性の注入というワードのわかりにくさもあいまってあっちいけと敬遠したくなります。
改めて調べてみましたが、簡単に言うと「 部品同士を疎結合化し依存性を無くす 」ということみたいです。疎結合化って言われると途端にわかりやすく感じます。
その結果メンテしやすくなったり再利用しやすくなったりテストしやすくなったりするようですね。
なんだか良いやつっぽいです。君とは仲良くなれそうだ。

触ってみる

簡単な例

まずDIパターンでない場合。

public class ClassA : MonoBehaviour {
    private ClassB _classB;

    void Start () {
        _classB = new ClassB ();
        _classB.CreateLog ();
    }
}

public class ClassB : MonoBehaviour {
    public void CreateLog () {
        Debug.Log ("hoge");
    }
}

このときClassAはClassBをnewしなければ動作できないのでClassAはClassBに依存している状態となります。
この例をZenjectを用いて疎結合化します。

SceneContextオブジェクトの生成
9b63c51b-62ff-6af1-c1dd-6883271429db.png

Installerスクリプトの生成
2.png

SceneContextのInstallerプロパティにInstallerを追加
3.png

Installerスクリプトには以下を記述します。

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public override void InstallBindings() {
        Container.Bind<ClassA>().AsSingle();
    }
}

ClassAを以下のように修正します。

public class ClassA : MonoBehaviour {
    [Inject]
    private ClassB _classB;

    void Start () {
        _classB.CreateLog ();
    }
}

ClassBオブジェクトにZenjectBindingをAddComponentしComponentsプロパティにClassBを追加します
4.png

するとZenjectがごにょごにょしてくれてClassBをnewしなくてもログを吐き出すことができるようになります。
5.png

Singletonで書いてたところに使ってみる

以下のようなシングルトンのGameManagerを

public class GameManager : Singleton<GameManager> {

    public int hoge0;

    public int hoge1;

    public int hoge2;
}

単なるMonoBehaviour継承クラスにしてしまい

public class GameManager : MonoBehaviour {

    public int hoge0;

    public int hoge1;

    public int hoge2;
}

ZenjectBindingをAddComponentします。
6.png

先ほど作成したClassAを改変します。

public class ClassA : MonoBehaviour {
    [Inject]
    private ClassB _classB;
    [Inject]
    private GameManager _manager;

    void Start () {
        _classB.CreateLog ();
        Debug.Log (_manager.hoge0 + " " + _manager.hoge1 + " " + _manager.hoge2);
    }
}

Installerは改変しなくて大丈夫です。
すると
7.png

Installerをprefab化してみる

InstallerのprefabをSceneContextに追加できるのでシーン上に配置しなくても使用することができ、シーンをキレイにすることができます。
PrefabInstallersに追加するだけ。
8.png

ScriptableObjectで使ってみる

ScriptableObjectを扱うことも可能です。

SciptableObjectInstallerを作成
9.png

SciptableObjectInstallerが生成されます。
10.png

SciptableObjectInstallerを編集

[CreateAssetMenu(fileName = "SettingInstaller", menuName = "Installers/SettingInstaller")]
public class SettingInstaller : ScriptableObjectInstaller<SettingInstaller> {
    public Setting setting;

    public override void InstallBindings () {
        Container.BindInstances(setting);
    }
}

[Serializable]
public class Setting {
    public int hoge3;
}

ProjectのメニューにInstallers/SettingInstallerが増えているのでそこからScriptableObjectをAssetsファイルとして出力
11.png

適当な値を入れます。
12.png

SceneContextに追加
13.png

ClassAを改変します。

public class ClassA : MonoBehaviour {
    [Inject]
    private ClassB _classB;
    [Inject]
    private GameManager _manager;
    [Inject]
    private Setting _setting;

    void Start () {
        _classB.CreateLog ();
        Debug.Log (_manager.hoge0 + " " + _manager.hoge1 + " " + _manager.hoge2);
        Debug.Log (_setting.hoge3);
    }
}

すると
14.png

まとめ

Unityでルールを決めず自由にコードを書いてしまうとしっちゃかめっちゃか
してしまいがちなので今回とりあげたZenjectのようなフレームワークは闇を生まないためにも有効な手段だと感じました。Zenjectを使うとかなりシンプルにまとめあげることができそうな気がします。
まだまだ簡単なことしかできていないので引き続きZenjectを触ってDIの真理へと近づいて行きたいと思います。