Extenject(旧Zenject)を触ってみた ~Inputの切り替えを例に~

はじめに
どうも、土鍋です。
この記事はIwaken Lab.の開発合宿ブログリレー11日目の記事です。
9月に行われた開発合宿で初めて触ったExtenjectについて書きました!
Extenjectとは?
DI(依存性の注入)フレームワークで、もともとZenjectという名前でしたが、開発者の方が会社とごたごたがあって名前が変わったそうです。 役割としては、「疎結合やテストのことを考えて設計したときに発生してしまう問題を解決してくれる」といった感じです。
Extenjectを使うには、疎結合な設計になっていないとその効力を発揮しない。
疎結合な設計
そこでまず、疎結合な設計について見ていきます。 例えば、複数デバイスの入力に対応し、切り替えを行う場合、どうしてもコードがごちゃっとしたり、デバッグが大変だったりします。
→ そこでInterface等を使って疎結合にしていく!!
例としてVRとキーボードで入力切り替えることを考えて設計していきましょう。
まずこれが密結合の状態です。

これでは操作方法が増えるたびにPlayerMoveを書き換える必要が出てきてしまいます。これでは手間ですし、チーム開発においては様々な問題に繋がりかねません。
これをinterfaceを使うことで、疎結合にしていきます。

これによりPlayerMoveは書き換えることなく、様々な操作方法に対応できます。
具体的なスクリプト
この例ではスカイダイビングゲームを想定して書きました。
PlayerMove
Playerの動き用スクリプト
public class PlayerMove : MonoBehaviour { [SerializeField] private float moveSpeed = 2; [SerializeField] private float fallSpeedMagnification = 3; private IInputProvider _inputProvider; private Rigidbody _rigidbody; private void Start() { _rigidbody = GetComponent<Rigidbody>(); } private void Update() { _rigidbody.AddRelativeForce(_inputProvider.GetMove() * moveSpeed); _rigidbody.drag = _inputProvider.FallSpeed() * fallSpeedMagnification; } }
うまーいこと疎結合にしているとInputが増えてもここを書き換える必要はない。
IInputProvider
今回作ったInput用のインターフェイス。使用する動きを定義。
public interface IInputProvider { Vector2 GetMove(); float FallSpeed(); }
VRInputProvider
IInputProviderを実装しています。
public class VRInputProvider : IInputProvider { public Vector2 GetMove() { 処理 } public float FallSpeed() { 処理 } }
KeyboardInputProviderもVRInputProvider同様に実装する。
Extenjectの利用
ただ、疎結合にするとクラスの利用者的には不便な場面が多いです。
例えば 「利用するインスタンスの決定をどうやって行うのか?」 「どのタイミングで呼び出すのか?」 といった問題が発生します。
そこでExtenjectで依存性を注入する!!
導入方法

従来のアセット通り、AssetStoreからインストールできます。
今回の実装
主な構成はこんな感じ。


1. Installerを書いていく
DI Container1に対して「IInputProviderが要求されたら、VRInputProviderを注入する」ということを設定するVRInputInstallerを書く。
VRInputInstaller
public class VRInputInstaller : Installer<VRInputInstaller> { public override void InstallBindings() { Container .Bind<IInputProvider>() .To<VRInputProvider>() .AsCached(); } }
KeyboardInputInstaller
同様にしてKeyboardInputInstallerを記述。
public class KeyboardInputInstaller : Installer<KeyboardInputInstaller> { public override void InstallBindings() { Container .Bind<IInputProvider>() .To<KeyboardInputProvider>() .AsCached(); } }
InputManager
さらにVRかキーボードかを判定するInputManagerを書く。
public class InputManager : MonoInstaller { ~~中略~~ public override void InstallBindings() { if (XRGeneralSettings.Instance && XRGeneralSettings.Instance.Manager.activeLoader != null) { Debug.Log("VR Mode"); VRInputInstaller.Install(Container); } else { Debug.Log("Keyboard Mode"); KeyboardInputInstaller.Install(Container); } } }
2. Contextを設定する
配置されたシーンにのみInstallerの影響範囲を設定するScene Contextをシーンに配置する。

Scene ContextにInputManagerを登録します。

3. DI Containerがオブジェクトを注入する先を設定
変数に[Inject]属性をつけることでContainerがオブジェクトを注入してくれます。
いやー簡単ですね...
今回であれば、PlayerMoveのIInputProviderの宣言の部分に[Inject]属性を追記。
[Inject]
private IInputProvider _inputProvider;
これで実行時にIInputProviderにVR or キーボードのInputProviderが注入されます。
まとめ
今までまともに設計したことがなかったのですが(設計しても結局うまくいかなかった)、Extenjectを学ぶにあたりインターフェイス周りの勉強にもなりました。
UniRx/UniTask等と組み合わせることで、さらにチーム開発においてかなり効力を発揮しそうですねー。今度はそこら辺取り組んで記事にしようかなと思います。
参考
以下の記事を参考にさせていただきました!ありがとうございました!
Zenject入門その1 疎結合とDI Container - Qiita
【Unity(C#)】Extenject(Zenject)使って入力機能を切り離したものを管理してみた - Qiita
-
DI Containerは簡単に言えば「環境にあらかじめ依存関係を設定しておくと、それに応じて自動的にインスタンスを設定してまわってくれる便利な存在」です。Zenject入門その1 疎結合とDI Container - Qiitaより引用↩