[Unity] Zenject入門



UniRx,UniTask,DOTweenなどUnityには便利ライブラリが多く存在します．その中の１つである**Zenject**についての初歩のまとめ．
## Zenjectとは．

[GitHubの公式ページ](https://github.com/modesttree/zenject)によると

>　Zenject is a lightweight highly performant dependency injection framework

Zenjectは軽量で高性能な **Dependency Injection** のフレームワークとのことです．


#### Dependency Injection
説明で出てきた **Dependency Injection** (DI) は「依存性の注入」と訳されます．具体的に言うと,未定義な箇所(依存する箇所)に適する物を外部から定義する(注入)します．
言葉では分かりにくいですね．簡単なコードに書き起こしてみます． 
```csharp
interface IInputHandler{
    public Vector3 GetInputDirection();
}
```

```csharp
// Player.cs
public class Player{
    IInputHandler inputHander;
    void Update(){
        Vector3 dir = inputHandler.GetInputDirection();
        Move(dir);
    }
    void Move(Vector3 dir){
        // 省略
    }
}
```
**Player**というクラスでは，**何か入力を受け取るIInputHandler**というクラス(interface)をフィールドとして持っています．このフィールドから入力を受け取りPlayerはMove()することができます．ここで重要なのが，**IInputHandler**が依存される場所(未定義)になっていると言うことです．どこからも代入がされていません．

#### フィールドへの代入

`inputHandler`フィールドでの代入は,`Awake()`や`Start()`関数で代入する．または`[SerirlaizeField]`にしてインスペクター上で設定するなどがあります．
```csharp
// 代入例
    void Start(){
        inputHandler = new HogeInputHandler();
    }
```

しかし，これはPlayer内部でどのクラスを使うかを定義してしまい，テストのために別の入力方法を取りたい時に変更が大変になります．

#### 注入(Injection)

そこで **Dependency Injection** の考え方は，Playerクラス内部で`inputHandler`に代入するクラスを決めるのではなく， 外部から代入(**注入**)しようと言うものになります．

この外部から代入の操作をUnity上でいい感じにしてくれるのが， 「**Zenject**」になります．

## Zenjectのセットアップ

 1.ZenjectをAssetStoreからMyAssetに追加する．
今は**Extenject Dependency Injection IOC**としてAssetStoreに公開されているので，ダウンロードしてきます．

https://assetstore.unity.com/packages/tools/utilities/extenject-dependency-injection-ioc-157735#description

 2.新規プロジェクトを作成し， `Window => PackageManager` のMy Assetから Extenject をインポートする．
![スクリーンショット 2022-04-06 11.36.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1113136/36842332-9654-9cc3-5475-6066b35ac6c1.png)

これでZenject(Etenject)が使えるようになったため，実際に使用してみましょう．

----

## 今回作成するもの

Playerの**IInputHandler**に対して，
- WASDで動かす : **InputFromWASD**
- 矢印キーで動かす : **InputFromArrow**
- 自動で動かす : **InputTest**

3つのクラスを外部から注入してみます．設計は図にすると分かりやすい．
![UML.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1113136/e8e83b66-992d-e1b6-78fc-39cb16ff2009.jpeg)

Playerに対する入力方法を変えていこうと思います．

---- 
## Zenjectを試しに使ってみる

さて本題として実際に使ってみましょう．初めに上のクラス図で定義したファイルを全て作ります(Sampleプロジェクトなのでフォルダ構成は考えていません)．
![スクリーンショット 2022-04-06 12.22.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1113136/3d9232cf-d39c-dded-fa4a-59c770461498.png)


IInputHandlerのInterfaceを定義して3つのクラスで実装します．
InputTestでは120回関数がよばれたら逆方向を返の入力に変わるようにしました． その時々のテストを書くといいでしょう．
```csharp
// IInputHandler.cs
using UnityEngine;
namespace ZenjectSample
{
    public interface IInputHandler
    {
        Vector3 GetInputDirection();
    }
}
```

<details>
<summary>InputFromArrow.cs</summary><div>

```csharp
using UnityEngine;

namespace ZenjectSample
{
    public class InputFromArrow : IInputHandler
    {
        public Vector3 GetInputDirection()
        {
            if (Input.GetKey(KeyCode.UpArrow))
                return Vector3.up;
            if (Input.GetKey(KeyCode.LeftArrow))
                return Vector3.left;
            if (Input.GetKey(KeyCode.DownArrow))
                return Vector3.down;
            if (Input.GetKey(KeyCode.RightArrow))
                return Vector3.right;
            return Vector3.zero;
        }
    }
}
```

</div>
</details>

<details>
<summary>InputFromWASD.cs</summary><div>

```csharp
using UnityEngine;

namespace ZenjectSample
{
    public class InputFromWASD : IInputHandler
    {
        public Vector3 GetInputDirection()
        {
            if (Input.GetKey(KeyCode.W))
                return Vector3.up;
            if (Input.GetKey(KeyCode.A))
                return Vector3.left;
            if (Input.GetKey(KeyCode.S))
                return Vector3.down;
            if (Input.GetKey(KeyCode.D))
                return Vector3.right;
            return Vector3.zero;
        }
    }
}
```

</div>
</details>

<details>
<summary>InputTest.cs</summary><div>

```csharp
using UnityEngine;

namespace ZenjectSample
{
    public class InputTest : IInputHandler
    {
        Vector3 dir = Vector3.right;
        int calledCount = 0;
        readonly int changeDirCount = 120;

        public Vector3 GetInputDirection()
        {
            calledCount++;
            if (calledCount > changeDirCount)
            {
                dir *= -1;
                calledCount = 0;
            }
            return dir;
        }
    }
}
```

</div>
</details>

最後にPlayerのクラスです．

```csharp
// Player.cs
using UnityEngine;

namespace ZenjectSample
{
    public class Player : MonoBehaviour
    {
        [Zenject.Inject] IInputHandler inputHandler;

        void Update()
        {
            if (inputHandler == null) return;
            var direction = inputHandler.GetInputDirection();
            transform.position += direction * Time.deltaTime;
        }
    }
}

```

inputHandlerフィールドに対してアノテーションがついています．
```csharp
    [Zenject.Inject] IInputHandler inputHandler;
```
このように書くこともできます．
```csharp
// usingを最初に書いておく．
using Zenject;
    [Inject] IInputHandler inputHandler;
```

[**Zenect.Inject**] アノテーションがこのフィールドは依存性箇所で，別の場所から注入(代入)します．と表しています．Playerクラス内では，`inputHandler = ** `のように代入する式が1つも出てきていません．


#### どのように注入(代入)するか

どのInputHandlerを使うか定義するファイルを生成します．
`ProjectView => + => Zenject => MonoInstaller`を選択します．

![スクリーンショット 2022-04-06 12.57.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1113136/a1152a75-9314-9c44-523d-a00238c79c83.png)

InputInstallerという名前で保存しました．
![スクリーンショット 2022-04-06 13.01.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1113136/9c4fcc86-921b-4c6f-b6b5-9db8c51b4807.png)
保存すると`MonoInstaller`を継承したInputInstallerのクラスが出来上がります．出来上がった際にすでに`InstallBindings()`関数があるので中身を書いていきます．始めはWASDで入力出来るようにしてみます．

```csharp
using Zenject;

namespace ZenjectSample
{
    public class InputInstaller : MonoInstaller
    {
        public override void InstallBindings()
        {
            Container
                .Bind<IInputHandler>()
                .To<InputFromWASD>()
                .AsSingle();
        }
    }
}
```

Containerに`Bind<>()`でInterfaceを指定し，`To<>()`でどのクラスを注入するか指定します．
AsSingleの部分にはインスタンスをどのように扱うかを記載します．他にもパターンがあり,[こちら](https://qiita.com/4_mio_11/items/4306732bc47780802b74#:~:text=%E6%B3%A8%E5%85%A5%E3%81%97%E3%81%BE%E3%81%99%E3%80%82-,AsSingle,-%E3%80%8C%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%AE%E7%94%9F%E6%88%90)で紹介されていました．

#### Contextの作成
Installerで，どこに何をバインドするかを決めたら最後は`Context`で，適用範囲を決めます．
プログラムで言うスコープのようなもので，開発プロジェクト全体に及ぼすのか，シーン内だけに及ぼすのか設定できます．
`Hierarchy -> +(or 右クリック) => Zenject => Scene Context`を押すとSceneContextのスクリプトを持つGameObjectが出来上がります．
![スクリーンショット 2022-04-06 14.21.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1113136/06275b4f-0718-683b-0603-95882c84b29f.png)
次にInstallerをContextに設定します．今作ったSceneContextのGameObjectに，`InputInstaller.cs`を取り付けます．インスペクタ上で，SceneContextコンポーネントのMonoInstallerに，InputInstallerをアタッチします．

![スクリーンショット 2022-04-06 14.28.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1113136/f22b0d05-664f-dc71-dcf8-03ae77ad31d1.png)

このようにすることで，このSceneContextを持ったゲームオブジェクトが存在するシーン内では， InputInstallerがBindしたものが適用されます．クラス部分だけ再掲します． シーン内の`IinputHandler`インターフェース部分に`InputFromWASD`クラスが適用され， 入力がWASDでの入力となります．
```csharp
    public class InputInstaller : MonoInstaller
    {
        public override void InstallBindings()
        {
            Container
                .Bind<IInputHandler>()
                .To<InputFromWASD>()
                .AsSingle();
        }
    }
```

今回はInnputInstallerに一つのインターフェースしか記載しませんでしたが，複数Containerに定義して，デバッグ時には一括して使用するクラスを変更するなど幅広い使い方が出来るようになります．今UnityでDIを使おうとしたらZenject一択になりそうなので是非つかってみましょう．

----


参考
- https://www.slideshare.net/torisoup/unityzenject
- https://qiita.com/toRisouP/items/b3d3c43db40857ca4ad4