62
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-26

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

##Extenject(Zenject)って?

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

【参考リンク】:Zenject入門その1 疎結合とDI Container

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

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

Extenject is the continuation of the Zenject Project!

【引用元】:Extenject Dependency Injection IOC

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


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

CubeAdventure3.gif

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

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

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

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

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

図にするとこんな感じです。
本当はクラス図書くときは矢印の種類やら
いろいろとルールがあるんですが、
この図の矢印は単純に知ってるか知らないかの方向を
表しているものとします。
Interface.PNG

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

##コード

まずはIInputProviderを作ります。

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

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

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

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値を返すだけです。

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

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


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

IInputProviderってどこ?どれ?って状態です。

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が「お前が使いたいのはこれかい?」
といった感じに手助けしてくれます。

 [Inject]
 IInputProvider inputProvider;

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

先ほど

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

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

####Installer

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

CreateZenjectInstaller で作れます。

ZenjectInstaller.png

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

using Zenject;

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

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

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

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


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

SceneContext.png

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

【参考リンク】:【Unity】【Zenject】DIの影響範囲を指定するContextの使い方まとめ

あとはInstallerの登録を行えば完了です。
SceneContextSettings.png

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

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


追記

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

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

  1. オープン・クローズドの原則の重要性について

62
49
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?