【Unity(C#)】Extenject(Zenject)使って入力機能を切り離したものを管理してみた

##はじめに
Extenject(Zenject)、設計、インターフェース等々、理解が浅い部分が多いので
誤りがあれば遠慮なくご指摘ください。

##Extenject(Zenject)って？

依存性の注入のためのフレームワークです。

【参考リンク】：[Zenject入門その1　疎結合とDI Container](https://qiita.com/toRisouP/items/b3d3c43db40857ca4ad4#zenject%E3%81%A8%E3%81%AF)

疎結合な設計をする際に発生する問題に対して、
いろいろと手助けをしてくれる最強ライブラリです。

ExtenjectはZenjectのメンテナンス用として作られたそうですが、
なんやかんやあって今はExtenjectらしいです。

>Extenject is the continuation of the Zenject Project!

【引用元】：[Extenject Dependency Injection IOC](https://assetstore.unity.com/packages/tools/utilities/extenject-dependency-injection-ioc-157735)


##密結合
疎結合の反対は密結合ですが、なぜダメなのでしょう。
私が陥った状況に沿って説明していきます。

---
下記のGIFのようなゲームを趣味で作ってました。

![CubeAdventure3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/48e871fb-a814-9f04-71d6-0e0242917c3d.gif)

`Virtual JoyStick`と呼ばれる、
スマホゲームでよく見かける入力インターフェースを実装してあります。

ただ、このGIFはスマホ用で、最終的にはWebGLでも遊べるようにしたかったです。
WebGLで遊ぶならキー入力の方が操作しやすいので
実装を変更するつもりでした。

もちろん、プラットフォーム判定とコピペを駆使すれば、
後から付け加えることも不可能ではありませんが、
１つのUpdate内に大量の分岐処理を書き込んだり、
他の入力系との参照関係が生まれることは予期せぬ挙動を生み出す可能性があり、
あまり好ましくありません。

オープンクローズド原則[^1]にも反するので、
もし今後、ステージのギミックで"入力が反転する"などを
実装したくなった際にスパゲッティコードになります。

##疎結合
先述の問題を解消するためにExtenjectを使います。
そのために入力機能を疎結合にするのですが、
めちゃくちゃ簡単に言うと、
**入力機能をごっそり差し替えても問題なく動くような仕組みにする**
ってことです。


図にするとこんな感じです。
本当はクラス図書くときは矢印の種類やら
いろいろとルールがあるんですが、
この図の矢印は単純に知ってるか知らないかの方向を
表しているものとします。
<img width="544" alt="Interface.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/2f0c89e3-1815-9b62-fd04-1f0fd0707816.png">

要するに中央のIInputProviderというインターフェースを使って、
**MoveCubeというクラスが、一番下のInput(入力機能)を知らない状態を作り出せば、
入力機能を差し替えても、MoveCubeに変更を与える必要はない**ということです。

##コード

まずは`IInputProvider`を作ります。

```C#
 public interface IInputProvider
{
    bool InputLeft(bool isSpaceDirection);
    bool InputRight(bool isSpaceDirection);
    bool InputUp(bool isSpaceDirection);
    bool InputDown(bool isSpaceDirection);
}
```

次に`IInputProvider`を実装した`KeyInputProvider`を作成します。

`ステージのギミックで"入力が反転する"`という実装をあらかじめ仕込んでます。

```C#
using UnityEngine;

public class KeyInputProvider : IInputProvider
{
    public bool InputUp(bool isSpaceDirection)
    {
        if (isSpaceDirection)
        {
            return Input.GetKey(KeyCode.UpArrow);
        }
        else
        {
            return Input.GetKey(KeyCode.DownArrow);
        }
    }

    public bool InputDown(bool isSpaceDirection)
    {
        if (isSpaceDirection)
        {
            return Input.GetKey(KeyCode.DownArrow);
        }
        else
        {
            return Input.GetKey(KeyCode.UpArrow);
        }
    }

    public bool InputLeft(bool isSpaceDirection)
    {
        if (isSpaceDirection)
        {
            return Input.GetKey(KeyCode.LeftArrow);
        }
        else
        {
            return Input.GetKey(KeyCode.RightArrow);
        }
    }

    public bool InputRight(bool isSpaceDirection)
    {
        if (isSpaceDirection)
        {
            return Input.GetKey(KeyCode.RightArrow);
        }
        else
        {
            return Input.GetKey(KeyCode.LeftArrow);
        }
    }
}

```

同様に差し替え機能の一つとして`JoyStickInputProvider`を用意します。

やっていることは`KeyInputProvider`と同じで、
ジョイスティックの入力具合に応じてbool値を返すだけです。

```C#
public class JoyStickInputProvider : IInputProvider
{
    float joyStickSensitivity = 0.7f;

    public bool InputUp(bool isSpaceDirection)
    {
        if (isSpaceDirection)
        {
            return SimpleTouchController.GetTouchPosition.y > joyStickSensitivity;
        }
        else
        {
            return SimpleTouchController.GetTouchPosition.y < -joyStickSensitivity;
        }
    }

    public bool InputDown(bool isSpaceDirection)
    {
        if (isSpaceDirection)
        {
            return SimpleTouchController.GetTouchPosition.y < -joyStickSensitivity;
        }
        else
        {
            return SimpleTouchController.GetTouchPosition.y > joyStickSensitivity;
        }

    }

    public bool InputLeft(bool isSpaceDirection)
    {
        if (isSpaceDirection)
        {
            return SimpleTouchController.GetTouchPosition.x < -joyStickSensitivity;
        }
        else
        {
            return SimpleTouchController.GetTouchPosition.x > joyStickSensitivity;
        }
    }

    public bool InputRight(bool isSpaceDirection)
    {
        if (isSpaceDirection)
        {
            return SimpleTouchController.GetTouchPosition.x > joyStickSensitivity;
        }
        else
        {
            return SimpleTouchController.GetTouchPosition.x < -joyStickSensitivity;
        }
    }
}
```

##Extenject

そして、ここからExtenjectの力を借ります。

導入までは下記参考リンクで完璧です。
導入以降もめちゃくちゃわかりやすいので100回ぐらい見た方がいいです。

【参考リンク】：[Zenject入門その1　疎結合とDI Container](https://qiita.com/toRisouP/items/b3d3c43db40857ca4ad4#zenject%E3%81%A8%E3%81%AF)

※AssetStoreではExtenjectと検索すれば出てきます

---
`MoveCube`が`IInputProvider`を使うわけですが、
このままだとnullになってしまいます。

`IInputProvider`ってどこ？どれ？って状態です。

```C#:MoveCube
    //MoveCubeはどこのどのIInputProviderを使えばいいかわからない
    IInputProvider inputProvider;

    [SerializeField]
    bool tmpFlag = true;

    void Update()
    {
        //Right 
        if (inputProvider.InputRight(tmpFlag))
        {
            //適当な処理
        }
        //Left 
        if (inputProvider.InputLeft(tmpFlag))
        {
           //適当な処理
        }
        //Up 
        if (inputProvider.InputUp(tmpFlag))
        {
            //適当な処理
        }
        //Down 
        if (inputProvider.InputDown(tmpFlag))
        {
            //適当な処理
        }
    }
```

そこで、**[Inject]**を使います。

インターフェースに`[Inject]`というアトリビュートを与えることで、
`MoveCube`が「`IInputProvider`を使いたい！」となった際に、
Extenjectが「お前が使いたいのは**これ**かい？」
といった感じに手助けしてくれます。

```C#
 [Inject]
 IInputProvider inputProvider;
```

###Extenjectに"これ"がなんなのかあらかじめ設定しておく

先ほど
>Extenjectが「お前が使いたいのは**これ**かい？」
といった感じに手助けしてくれます。

と書きましたが、Extenjectに助けてもらうには
まずはこちら側で"これ"が一体何のことを示しているのか
あらかじめ教えておくことが必要です。

####Installer

Extenjectに助けてもらうために`Installer`を作ります。

`Create`→`Zenject`→`Installer`　で作れます。

<img width="350" alt="ZenjectInstaller.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/bd46759a-cea9-d34f-9226-5ebca38de59f.png">

中身はこんな感じにします。
それぞれ、`IInputProvider`が呼ばれた際に、
「`To以下のジェネリクスで指定したProvider`＝**"これ"**」になるよ～
って設定してます。

```C#
using Zenject;

public class KeyInputInstaller : Installer<KeyInputInstaller>
{
    public override void InstallBindings()
    {
        Container
            .Bind<IInputProvider>()
            .To<KeyInputProvider>()
            .AsCached();
    }
}
```

```C#
using Zenject;

public class JoyStickInputInstaller : Installer<JoyStickInputInstaller>
{
    public override void InstallBindings()
    {
        Container
            .Bind<IInputProvider>()
            .To<JoyStickInputProvider>()
            .AsCached();
    }
}
```

さらに、今回はプラットフォーム判定を利用して
より簡単に差し変わるようにしてみました。

```C#
using Zenject;

public class InputInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        //スマホ用
#if UNITY_ANDROID || UNITY_IOS 
        JoyStickInputInstaller.Install(Container);
#endif

        //WebGL
#if UNITY_WEBGL 
        KeyInputInstaller.Install(Container);
#endif
    }
}
```

Installerをもう一つ用意して、
先ほど作成した2つのInstallerをプラットフォーム別判定の中で呼び出しています。

後ほどInspectorで登録するのでMonoInstallerを継承させておく必要があります。

##Context
次に、Installerの影響範囲決めとInstallerの登録を行います。

下記リンクに全部載ってますが、一応メモします。

【参考リンク】：[Zenject入門その1　疎結合とDI Container](https://qiita.com/toRisouP/items/b3d3c43db40857ca4ad4#3-context%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B)

---

`Context`というものを作成します。
今回使うのは`SceneContext`と呼ばれるものです。

<img width="279" alt="SceneContext.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/28cbc980-4585-adbd-b318-8fec7c02dc76.png">

影響範囲や設定方法については下記リンクが参考になります。

【参考リンク】：[【Unity】【Zenject】DIの影響範囲を指定するContextの使い方まとめ](http://light11.hatenadiary.com/entry/2019/02/25/233318)

あとはInstallerの登録を行えば完了です。
<img width="269" alt="SceneContextSettings.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/afd9adee-a75d-31f3-b8bf-503a75bf23c4.png">

##おわりに
インターフェースってなんのためにあるんだ？
使ったことないけど困ったことないぞ？
って感じだったので触ってみました。

まだまだ小規模なテストも書いてない個人製作の範囲なので、
その強力さにいまいちピンときてませんが、
できるだけ疎結合を意識した設計ができたらいいなと思ってます。

---
追記

記事を書くにあたって、Unityゲーム開発者ギルドでいろいろと
疑問点にお答えいただいた方、ありがとうございました。

まだ入ってない人は早急に入った方がいいと思います。
損することは何もないです。(強いて言えばみんな強すぎてちと凹む。。。)

[^1]: [オープン・クローズドの原則の重要性について](https://medium.com/eureka-engineering/go-open-closed-principle-977f1b5d3db0)