はじめに
Extenject(Zenject)、設計、インターフェース等々、理解が浅い部分が多いので
誤りがあれば遠慮なくご指摘ください。
Extenject(Zenject)って?
依存性の注入のためのフレームワークです。
【参考リンク】:Zenject入門その1 疎結合とDI Container
疎結合な設計をする際に発生する問題に対して、
いろいろと手助けをしてくれる最強ライブラリです。
ExtenjectはZenjectのメンテナンス用として作られたそうですが、
なんやかんやあって今はExtenjectらしいです。
Extenject is the continuation of the Zenject Project!
【引用元】:Extenject Dependency Injection IOC
密結合
疎結合の反対は密結合ですが、なぜダメなのでしょう。
私が陥った状況に沿って説明していきます。
下記のGIFのようなゲームを趣味で作ってました。
Virtual JoyStick
と呼ばれる、
スマホゲームでよく見かける入力インターフェースを実装してあります。
ただ、このGIFはスマホ用で、最終的にはWebGLでも遊べるようにしたかったです。
WebGLで遊ぶならキー入力の方が操作しやすいので
実装を変更するつもりでした。
もちろん、プラットフォーム判定とコピペを駆使すれば、
後から付け加えることも不可能ではありませんが、
1つのUpdate内に大量の分岐処理を書き込んだり、
他の入力系との参照関係が生まれることは予期せぬ挙動を生み出す可能性があり、
あまり好ましくありません。
オープンクローズド原則1にも反するので、
もし今後、ステージのギミックで"入力が反転する"などを
実装したくなった際にスパゲッティコードになります。
疎結合
先述の問題を解消するためにExtenjectを使います。
そのために入力機能を疎結合にするのですが、
めちゃくちゃ簡単に言うと、
入力機能をごっそり差し替えても問題なく動くような仕組みにする
ってことです。
図にするとこんな感じです。
本当はクラス図書くときは矢印の種類やら
いろいろとルールがあるんですが、
この図の矢印は単純に知ってるか知らないかの方向を
表しているものとします。
要するに中央の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と検索すれば出てきます
MoveCube
がIInputProvider
を使うわけですが、
このままだとnullになってしまいます。
IInputProvider
ってどこ?どれ?って状態です。
//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
を作ります。
Create
→Zenject
→Installer
で作れます。

中身はこんな感じにします。
それぞれ、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
と呼ばれるものです。

影響範囲や設定方法については下記リンクが参考になります。
【参考リンク】:【Unity】【Zenject】DIの影響範囲を指定するContextの使い方まとめ
おわりに
インターフェースってなんのためにあるんだ?
使ったことないけど困ったことないぞ?
って感じだったので触ってみました。
まだまだ小規模なテストも書いてない個人製作の範囲なので、
その強力さにいまいちピンときてませんが、
できるだけ疎結合を意識した設計ができたらいいなと思ってます。
追記
記事を書くにあたって、Unityゲーム開発者ギルドでいろいろと
疑問点にお答えいただいた方、ありがとうございました。
まだ入ってない人は早急に入った方がいいと思います。
損することは何もないです。(強いて言えばみんな強すぎてちと凹む。。。)