UniRx,UniTask,DOTweenなどUnityには便利ライブラリが多く存在します.その中の1つであるZenjectについての初歩のまとめ.
Zenjectとは.
GitHubの公式ページによると
Zenject is a lightweight highly performant dependency injection framework
Zenjectは軽量で高性能な Dependency Injection のフレームワークとのことです.
Dependency Injection
説明で出てきた Dependency Injection (DI) は「依存性の注入」と訳されます.具体的に言うと,未定義な箇所(依存する箇所)に適する物を外部から定義する(注入)します.
言葉では分かりにくいですね.簡単なコードに書き起こしてみます.
interface IInputHandler{
public Vector3 GetInputDirection();
}
// 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]
にしてインスペクター上で設定するなどがあります.
// 代入例
void Start(){
inputHandler = new HogeInputHandler();
}
しかし,これはPlayer内部でどのクラスを使うかを定義してしまい,テストのために別の入力方法を取りたい時に変更が大変になります.
注入(Injection)
そこで Dependency Injection の考え方は,Playerクラス内部でinputHandler
に代入するクラスを決めるのではなく, 外部から代入(注入)しようと言うものになります.
この外部から代入の操作をUnity上でいい感じにしてくれるのが, 「Zenject」になります.
Zenjectのセットアップ
1.ZenjectをAssetStoreからMyAssetに追加する.
今はExtenject Dependency Injection IOCとしてAssetStoreに公開されているので,ダウンロードしてきます.
2.新規プロジェクトを作成し, Window => PackageManager
のMy Assetから Extenject をインポートする.
これでZenject(Etenject)が使えるようになったため,実際に使用してみましょう.
今回作成するもの
PlayerのIInputHandlerに対して,
- WASDで動かす : InputFromWASD
- 矢印キーで動かす : InputFromArrow
- 自動で動かす : InputTest
3つのクラスを外部から注入してみます.設計は図にすると分かりやすい.
Playerに対する入力方法を変えていこうと思います.
Zenjectを試しに使ってみる
さて本題として実際に使ってみましょう.初めに上のクラス図で定義したファイルを全て作ります(Sampleプロジェクトなのでフォルダ構成は考えていません).
IInputHandlerのInterfaceを定義して3つのクラスで実装します.
InputTestでは120回関数がよばれたら逆方向を返の入力に変わるようにしました. その時々のテストを書くといいでしょう.
// IInputHandler.cs
using UnityEngine;
namespace ZenjectSample
{
public interface IInputHandler
{
Vector3 GetInputDirection();
}
}
InputFromArrow.cs
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;
}
}
}
InputFromWASD.cs
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;
}
}
}
InputTest.cs
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;
}
}
}
最後にPlayerのクラスです.
// 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フィールドに対してアノテーションがついています.
[Zenject.Inject] IInputHandler inputHandler;
このように書くこともできます.
// usingを最初に書いておく.
using Zenject;
[Inject] IInputHandler inputHandler;
[Zenect.Inject] アノテーションがこのフィールドは依存性箇所で,別の場所から注入(代入)します.と表しています.Playerクラス内では,inputHandler = **
のように代入する式が1つも出てきていません.
どのように注入(代入)するか
どのInputHandlerを使うか定義するファイルを生成します.
ProjectView => + => Zenject => MonoInstaller
を選択します.
InputInstallerという名前で保存しました.
保存するとMonoInstaller
を継承したInputInstallerのクラスが出来上がります.出来上がった際にすでにInstallBindings()
関数があるので中身を書いていきます.始めはWASDで入力出来るようにしてみます.
using Zenject;
namespace ZenjectSample
{
public class InputInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container
.Bind<IInputHandler>()
.To<InputFromWASD>()
.AsSingle();
}
}
}
ContainerにBind<>()
でInterfaceを指定し,To<>()
でどのクラスを注入するか指定します.
AsSingleの部分にはインスタンスをどのように扱うかを記載します.他にもパターンがあり,こちらで紹介されていました.
Contextの作成
Installerで,どこに何をバインドするかを決めたら最後はContext
で,適用範囲を決めます.
プログラムで言うスコープのようなもので,開発プロジェクト全体に及ぼすのか,シーン内だけに及ぼすのか設定できます.
Hierarchy -> +(or 右クリック) => Zenject => Scene Context
を押すとSceneContextのスクリプトを持つGameObjectが出来上がります.
次にInstallerをContextに設定します.今作ったSceneContextのGameObjectに,InputInstaller.cs
を取り付けます.インスペクタ上で,SceneContextコンポーネントのMonoInstallerに,InputInstallerをアタッチします.
このようにすることで,このSceneContextを持ったゲームオブジェクトが存在するシーン内では, InputInstallerがBindしたものが適用されます.クラス部分だけ再掲します. シーン内のIinputHandler
インターフェース部分にInputFromWASD
クラスが適用され, 入力がWASDでの入力となります.
public class InputInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container
.Bind<IInputHandler>()
.To<InputFromWASD>()
.AsSingle();
}
}
今回はInnputInstallerに一つのインターフェースしか記載しませんでしたが,複数Containerに定義して,デバッグ時には一括して使用するクラスを変更するなど幅広い使い方が出来るようになります.今UnityでDIを使おうとしたらZenject一択になりそうなので是非つかってみましょう.
参考