step by step VContainer vol.1
当記事の概要
VContainerってなんか凄そう?VContainerを理解したいけど「意味わからん!」というUnityを交えたC#のプログラミングにある程度慣れてきた中級者向けに、VContainerに関連する用語や知識、プログラムサンプルを数回の記事に分けて整理したものになります。主に自分のために書いていますが、もしどなたかの参考になれば幸いです。
第1回:準備編
第1回の今回は、VContainerの基本的な事柄について次の内容を整理してみました。VContainerを使った具体的な実装やサンプルを確認したい方は第2回以降をご参照ください。
- VContainerとは
- VContainerのインストール
- 関連リンク
VContainerとは
ハダシA氏作のUnity用Dependency Injection(依存性の注入とも。以下DIと略称表記します)フレームワーク。分類的にはDIコンテナと呼ばれるタイプのソフトウェアで、VContainerは他のDIコンテナとの違いとして特に次に挙げる特徴を有します。
- コードサイズ小さく、実行速度が速く、ガーベージコレクションが少ない
- 必要十分な機能に厳選し、何が起きてるかわかりやすいシンプルなAPI
- 非同期に利用できる形で提供されており、プロジェクト内の他の仕組みに縛られない
筆者の主観になりますが、デザインパターンをはじめとした高度なプログラミング知識をフル活用するため、少々上級者向けのフレームワークであるような印象を受けます。
DIコンテナとは何か?
そもそもDIコンテナとはどんなタイプのソフトウェアでしょう?
次のようなWeaponクラスとPlayerクラスがあったとします。
public class Weapon
{
string name;
int power;
public Weapon(string name,int power)
{
this.name = name;
this.power = power;
}
}
using UnityEngine;
public class Player
{
int hp;
Weapon w;
public Player()
{
hp = 10;
w = new Weapon("Gun",1);
}
public CheckWeapon()
{
Debug.Log( w.name );
}
}
PlayerクラスはWeapon型のフィールドwを保有しており、Playerクラス内でnewをして具体的なWeaponインスタンスを生成しています。Playerクラスがこういった構造をしていることを「PlayerがWeaponに依存している」と呼びます。
この構造は、Playerクラス内でWeaponの詳細を変更したい時やWeapenクラスを継承した別クラスを作成したとき、Player.csファイルをコードエディタ(VisualStudioなど)で開いてわざわざ編集しなければいけません。
オブジェクト指向プログラミングに多少慣れた人であれば、次のようにコンストラクタを上手に活用することでPlayerの中から「依存を無くす」ことが出来るでしょう。以後Player内部で使われるWeapon(フィールドw)については、外部のプログラムにて、Playerをnewするときに任意のWeaponインスタンスを手渡す形を取ることでPlayerのプログラムをいちいち編集せずに済むようになります。このように外部から必要なデータを渡すことをDIコンテナでは、「注入する」とか「Injectする」と表現したり、「注入する」ことでフィールドwの中身が具体的になることを「解決する」と表現します。
using UnityEngine;
public class Player2
{
int hp;
Weapon w;
public Player2(int hp, Weapon w)
{
this.hp = hp;
this.w = w;
}
public CheckWeapon()
{
Debug.Log( w.name );
}
}
またUnityを利用するのであれば、UnityEditorを使うことでWeaponとPlayer双方のクラスに対して、Serialize化を行ったり、MonoBehaviourを継承したコンポーネントとすることなどを通して、上手に「注入する」ことが実現可能です。
using System;
[Serializable]
public class Weapon2
{
public string name;
public int power;
public Weapon2(string name, int power)
{
this.name = name;
this.power = power;
}
}
using UnityEngine;
public class Player2 : MonoBehaviour
{
[SerializeField] int hp;
[SerializeField] Weapon2 w;
void Update()
{
CheckWeapon();
}
public void CheckWeapon()
{
Debug.Log( w.name );
}
}
UnityEditor上から依存関係を解決
上記のような対応で一旦依存関係を整理することはできましたが、よりプログラムの規模が大きくなってくると、「Aのプログラムの注入はBで」、「Cのプログラムの注入はDで」、「Eのもつフィールドfの注入はGで、フィールドhの注入はIで」と様々な場所で「注入する」ことが行われるようになり、依存関係を正しく解決することがどんどん大変になります。さらにUnityでの開発になると、この依存関係の経路になりうる選択肢がさらに増え、「Jへの注入はUnityEditor上で」「Kへの注入はUnityEditorからGameObjectのLで、Lへの注入はさらにPrefabのNを使って・・・」と複雑さに磨きがかかります。
加えて困るのが、オブジェクトの生成と破棄の管理です。データは存在するはずなのに参照(つまりは依存の注入)が上手くいかず、NullReferenceのエラーに苦しんだ経験のある開発者も多いのではないでしょうか?
さぁ、ここで登場するのがDIコンテナです。
DIコンテナは、依存関係の管理や解決を事前に決めた枠組みで提供するもので、ものによってはAPIの形(つまりは決まったプログラミングの書き方)で、ものによっては設定ファイルを編集することで、ものによっては双方を使って依存関係解決の複雑さを解消するソフトウェアとなります。
当記事で紹介するVContainerは主にAPIの形で枠組みを提供しており、決まった形でプログラムを書くことで自動で様々なプログラム間の依存関係を解決します。
DIコンテナを利用するメリット
DIコンテナを使う使わないは開発者の自由です。実際、多くのUnity開発者の方はDIコンテナを使わずともデータの受け渡しに独自のルールを設定して、ゲームを制作していると思います。
- GameObjectを上手に準備し、FindGameObjectを活用する
- staticクラスを活用する
DIコンテナのメリットのひとつは、ここまでの説明を通してもなんとなく理解できるように、データの受け渡しやオブジェクトの生成や破棄の管理を高度に洗練された仕組みに統一できることです。特に複数人で開発する際に、「このDIコンテナ使うから!」とすることで、プロジェクト参入プログラマー間の情報共有や教育を共通化出来ることは大きなメリットとなりえるでしょう。
また、多くのDIコンテナでは依存関係の解決を一箇所もしくは階層化された数箇所にまとめます。これにより、
- 依存関係が一望できる
- まとめに対して一括した処理を行える(例:データ生成やデータ破棄。準備した値が正常値か確認する処理など)
- バージョン管理しやすい
などのメリットも生じます。
VContainerのインストール
公式ドキュメント上に複数のインストール方法が提示されています。今回は.unitypackageファイルを使用した方法を試します。
まず、VContainerのリリースページ内部から以下の画像のような.unitypackageのダウンロードリンクを探し出し、ダウンロードします。2022年12月22日現在での最新バージョンは1.12.0でした。
続けて、VContainerを導入したいUnityプロジェクト内部からmanifest.jsonファイルを探し出し、ファイル内部に存在する"dependencies":{という部分の次の行を改行するなどしてから、以下のような記述を加えます。VContainerの動作にMono.Cecilライブラリが必要なことに起因するそうです。
"com.unity.nuget.mono-cecil": "1.10.1",
UnityEditorからではなくて、OSのディレクトリシステムからでないと見つかりません
最後に、当該UnityプロジェクトをUnityEditorで起動後、Projectウィンドウにて、右クリック > Import Package > Custom Package... とクリックし、ダウンロードした.unitypackageファイルを選択し、開いた小窓でImportボタンクリックすれば完了です。
簡単な使い方
早速ですが、前述のWeaponクラス、PlayerクラスをベースにUnityでVContainerを使った簡単なサンプルを作ってみます。
VContainer利用の流れは次の3ステップに集約できます
- 準備
- 登録
- 解決
1.準備
準備において重要になるのが、次の3つのクラスです。
- LifetimeScopeクラス
- 注入するクラス、注入されるクラス
- 依存を解決したいクラス
LifetimeScopeクラスはVContainerにおけるDIコンテナの主軸となるクラスです。今回はそのLifetimeScopeを継承したSampleLifeScope.csと名前をつけたスクリプトを用意してみました。
using VContainer;
using VContainer.Unity;
public class SampleLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
//ここで依存関係を構築する
}
}
LifetimeScopeにはConfigureと呼ぶ抽象メソッドが用意されており、継承先であるSampleLifetimeScopeクラス内で具体的な実装を行います(現時点ではコメントのみですが、この後記述を追加します)。実はこの部分が依存関係を整理する場所となっています。
続けて、「注入するクラス、注入されるクラス」を準備します。先ほどのサンプルで言えば、Weapon2.csが良いでしょうか。これについてはそのまま使います。せっかくなので、もう一つ他愛のないクラス追加してみます。
using UnityEngine;
public class Shield
{
public void Guard()
{
Debug.Log("Guard Success!");
}
}
最後に、「依存関係を解決したいクラス」を準備します。先ほどのサンプルで言えば、Player2.csに該当しますが、VContainerに最適化させるためにVContainerのAPIを使った次のようなPlayer3.csを用意します。
using UnityEngine;
using VContainer.Unity;
public class Player3 : ITickable
{
int hp; // ITickableはMonobehaviourではないので[SerializeField]を除去
Weapon2 w; // ITickableはMonobehaviourではないので[SerializeField]を除去
Shield s; // 追加
public Player3(int hp, Weapon2 w, Shield s) // コンストラクタを追加
{
this.hp = hp;
this.w = w;
this.s = s;
s.Guard();
}
void ITickable.Tick() // void Updateを置き換え
{
CheckWeapon();
ShowCurrentHp(); // 追加
}
public void CheckWeapon()
{
Debug.Log( w.name );
}
public void ShowCurrentHp() // 追加
{
Debug.Log( hp );
}
}
詳細は次回移行の記事に回しますが、VContainerには様々なタイプの「注入する」方法が提供されており、今回は「コンストラクタインジェクション」と呼ばれる方法を採用しています。Monobehaviourの形を維持したいケースでは、依存関係を解決するために「メソッドインジェクション」と呼ぶ方法を採用することも可能です。
2.登録
スクリプトファイルが準備できたところで、これらをコンテナへ登録(Register)していきます。SampleLifetimeScopeを次のように編集します。
using UnityEngine;
using VContainer;
using VContainer.Unity;
public class SampleLifetimeScope : LifetimeScope
{
[SerializeField] int playerHp;//追記1
protected override void Configure(IContainerBuilder builder)
{
builder.Register<Shield>(Lifetime.Singleton); //追記2
builder.Register<Weapon2>(Lifetime.Singleton).WithParameter("name", "Gun").WithParameter<int>(1); //追記3
builder.RegisterEntryPoint<Player3>().WithParameter<int>(playerHp); //追記4
}
}
おおよその流れとしては、Configureメソッドの引数にあるbuilderのもつ様々な登録系メソッドを実行し、その実行時に先述の「注入するクラス、注入されるクラス」や「依存関係を解決したい」クラスを登録していきます。
追記内容に関しては、以下に簡単な説明を付けておきます。
追記箇所 |
内容 |
---|---|
追記2 | Registerメソッドのジェネリクス部分に登録対象のクラスを記述することでShieldクラスをコンテナに登録します。この方法はMonoBehaivourなどのUnity特有のクラスではなく、純粋なC#プログラムに対して利用可能です。引数のLifetime.Singletonに関して後述します。 |
追記3 | Weapon2クラスをコンテナに登録します。追記2のShieldとは異なり、Weapon2は将来的にインスタンスを生成する時にコンストラクタの引数の内容を具体的に決める必要があるため、WithParameterメソッドをつけて初期化用の値をセットしています。 |
追記1 追記4 |
追記4はVContainerのAPIからITickableインターフェースを使用しており、追記2や追記3とは異なりRegisterEntryPointメソッドを使用し、登録する必要があります。また、追記3のようにインスタンスに値を直接記入する必要はなく、追記1に見られるような、普段のMonoBehaviourが値を参照する時のような運用も可能です。 |
登録時には、追記3に見られるようにメソッドチェーンの要領でメソッド繋げて記述することで様々なパラメーターを登録クラスに与えることも可能です。
3.解決
最後に、SampleLifetimeScopeをHierarchy上の任意のGameObjectにアタッチします。LifetimeScopeクラスはMonoBehaviour継承しており、Monobehaviourを継承して記述する一般的なUnityのプログラムと同じようにコンポーネントとして取り扱うことが可能です。
後は実行して、どのような結果が得られたか確認しましょう。
本来この解決のステップはもっと複雑なものとなります。ただ現段階では、実行すると「LifetimeScopeで登録された依存関係が解決される」という認識程度で問題ありません。
付属ツール:VContainer Diagnostics Window
VContainerはフレームワークであり、APIだけでなくDIの利便性を向上させるツールも付属します。その一つが、「VContainer Diagnostics Window」です。
「VContainer Diagnostics Window」はVContainerをImportしたUnityプロジェクト上でDIが具体的にどのような状態であるかをウィンドウの形で表示してくれるツールです。
使い方に関しては、公式ドキュメントに記載されている通りなので割愛します。(注意するとしたら、RootLifetimeScopeプロパティに代入できるのはPrefabになるため、私のケースではSampleLifetimeScopがアタッチされたEntryPointのGameObjectをProjectウィンドウにドラッグ&ドロップし、Prefabを生成したことくらいでしょうか・・・)
試しに、つい先ほど作成したサンプルの実行中にはどんな表示がなされるかをみてみました。
第1回目の終わりに
これにて準備編は完了です。次回はVContainerを使い始める前に理解しておいたほうが良い各種用語の確認を行いたいと思います。
関連リンク
VContainer - GitHub
データのダウンロードなどはこちらから
VContainer
困ったら公式ドキュメントです。公式ドキュメントは全体的にWikipediaのような形でまとまっています。(JavaDocやUnityのスクリプトリファレンスのようなものを期待した方は残念ながらちょっと違います。)
Unity専用最速DIコンテナVContainer と、UnityにおけるDIの勘所
作者がVContainerの全体像や勘所をまとめたメモです。当記事もこちらの内容を踏まえて記載している項目が多いです。
Discussion
step by step VContainer vol.2