こんにちは!
プログラム2課の今井です。
今日は2月14日バレンタインデーですね!店先に並んだおいしそうなチョコレートを見るとついつい買ってしまいます。
さて、本日ご紹介するのはチョコレート…ではなくUnityのフレームワーク、Extenjectです!
環境
・Unity:2022.3.2f1
・Extenject:9.2.0
Extenjectとは
Extenjectとは、Unityゲーム開発用の依存性注入(Dependency Injection, DI)フレームワークで現在アセットストアにて無料でダウンロードできます。(旧名称はZenjectになります)
依存性注入(Dependency Injection, DI)とは
依存性注入(DI)とは、ソフトウェア設計のパターンの一つです。オブジェクトの依存関係を外部から注入することによって、コードの柔軟性を高め、再利用性を向上させ、テストの容易性を確保することを目的としています。
依存性注入のメリットとは
モジュール性: クラスは特定の実装に強く結びつかないため、コードの再利用や変更が容易になります。
テスタビリティ: 依存関係をモック(模擬)オブジェクトに置き換えることで、単体テストが簡単に行えます。
コードの可読性: 依存関係が明示的になるため、コードの構造や動作が理解しやすくなります。
レースゲーム開発を例に考えてみる
さて、上記でExtenjectや依存性注入の説明をしましたが、これだけの説明だけでは想像しにくい部分が多いと思います。
そこでレースゲーム開発を具体的な例に挙げて考えていきましょう。
レースゲームと言えば、当然車を操作しコースを走行する様子を想像しますね。ゲーム制作中、車の操作のテストする際にゲームメインで扱うコースとは別に車の性能をテスト専用のコースを作成することもあると思います。
上記から今回例に↓のような条件を決め、考えてみたいと思います
・ゲームメイン用のコースとテスト用のコース2つのコースが存在する
・開発中、テスターはテスト用のコースのみに触れ、車の走行操作テストを行う
さっそく、下のようなCourseManagerを作成してSceneに配置し内部でゲーム用のコースとテスト用のコースを選ぶように記述したとします。
public class CourseManager : MonoBehaviour
{
// コースの種類
private enum COURSE_TYPE {
Game = 0,
Test
}
[SerializeField]
private COURSE_TYPE _type = COURSE_TYPE.Game;
private CourseBase _course = null;
private void Start()
{
switch (_type)
{
case COURSE_TYPE.Game:
_course = new GameCourse();
break;
case COURSE_TYPE.Test:
_course = new TestCourse();
break;
}
_course.Setup();
}
}
上のコードで特に問題なさそうに感じますが、”依存関係”に着目してみると今後作業を進めていくうちに突き当たる問題が想像できるようになります。
依存の問題
では”依存関係”を意識してコードをもう一度見てみましょう。すると_courseはSwitch文の中でGameCourseクラスとTestCourseクラスでnewされるように決められていますね。この状態はセットアップを行っているStart関数がGameCourseクラスとTestCourseクラスに依存していることになります。
では上記の依存関係はどのような時に困るでしょうか
開発中、車の走行操作テストを行うテスターが、テストコースでテストを行う際もCourseManagerのStart関数内でGameCourseに依存しているため、GameCourseに関わるコードのスコープ範囲やアセットリソースを共有する必要になる可能性があります。開発初期はそこまで多くなかったアセットリソースでも開発後期になると量も多くなり、テスト箇所に関係のない部分のビルドやエラー、インポートに多くの時間がかかるかもしれません。
つまり、開発に関わる方それぞれに合った適切な範囲共有ができていない状態になっています。
図にすると下のようになります。
(実際にはStart関数が依存していますが、わかりやすいようにCourseManagerと記述しています)
Extenjectを利用して依存問題を解決する
ここから実際にExtenjectを利用して依存の問題を解決していきたいと思います。アセットストアからExtenjectをインストールして変更を加えていきましょう。
尚、パッケージ名ではExtenjectと表示されていましたが機能名称にまだZenjectという旧名称が使用されていますので注意していただけますと幸いです。
1.Interfaceを作成し、Injectでの宣言に変更する
まず初めに依存関係にあったクラスGameCourse、TestCourseの依存関係を解消するために、CourseManager内で宣言していたコース情報を外から注入できるように変更します。
まさに、最初の言葉の通り依存性の注入(Dependency Injection)です。
public interface ICourse
{
public void Setup();
}
public class CourseBase : ICourse
{
public virtual void Setup(){}
}
public class GameCourse : CourseBase
{
public override void Setup()
{
/// set up code...
UnityEngine.Debug.Log("Game Course Setup");
}
}
public class TestCourse : CourseBase
{
public override void Setup()
{
/// set up code...
UnityEngine.Debug.Log("Test Course Setup");
}
}
public class CourseManager : MonoBehaviour
{
[Inject]
public ICourse _course;
private void Start()
{
_course.Setup();
}
}
これでコースの情報を注入するためのInterfaceを作成し、newの部分の依存も解消できました。
また、併せてこの後どちらのSetupが呼ばれたかわかりやすいようにSetup関数内にログ表示を追加しました。
Injectについて
そして、気になるワード「Inject」が出てきましたね。
これはZenjectが依存性を注入するための属性です。
完成したCourseManagerはシーンに配置しておきます。
2.Installerを作成する
注入される側の作成はできたので、次は注入する側を作成します。
Create>ZenjectからInstallerを選び「Mono Installer」を作成します。
ゲームメイン用のコースとテスト用のコースそれぞれのMonoInstallerを作成します。
public class GameCourseInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<ICourse>().To<GameCourse>().AsSingle();
}
}
public class TestCourseInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<ICourse>().To<TestCourse>().AsSingle();
}
}
ここでBindしたものが先ほどのInjectに注入されます。
シーンのHierarchyでCreate Emptyから空のオブジェクトを2つ作成し、それぞれ「GameCourseInstaller」「TestCourseInstaller」と命名します。命名したオブジェクトの名前に対応したScriptファイルをAdd Componentします。
そして、この2つのゲームオブジェクトですがprefabにします。prefabにする意図としては後々に別のシーンなどで再利用する際に容易にするためです。
3.Contextを作成する
最後に「Context」を作成していきます。
シーンのHierarchyを右クリックするとこちらにもZenjectという項目が追加されています。3つ種類がありますが今回は「Scene Context」を利用して作成します。
先ほど作成したprefab(GameCourseInstaller)をScene ContextのMono Installersに代入します。Installerのオブジェクトは設定したContextで親子関係にしておくとわかりやすくなります。
4.実行確認
これで一旦ゲームメインコースでのセットアップの実行準備が整いましたので実行してみましょう!
画像の通りゲームメインコースのセットアップが呼ばれました!
では、テストコースの場合も実行できるか確認してみましょう。
テスト用のシーンを作成して同じようにContextを追加してTestCourseInstallerを設定したり、ゲームメインのコースのGameCourseInstallerをTestCourseInstallerに変更しても確認できると思います。
テストコース用のセットアップも呼ばれました!
まとめ
今回はExtenjectを利用して依存関係を解消し、Installerで分けてセットアップを行うまでの一連の修正を行いました。
今回とりあげた例では最初にメリットとして記述した ”依存関係をモック(模擬)オブジェクトに置き換えることで、単体テストが簡単に行えます” という部分を強く感じることができたのではないしょうか。今回はセットアップ部分を疎結合にし、再利用できるようにしましたが機能部分などにも扱えそうですね。個人的にExtenjectはゲームの開発だとアウトゲーム部分と相性が良い印象があります。また、新しい要素を付け加えたい場合は新しいInstallerをContextに追加することによって容易に結合ができます。
最後に
今回紹介できたのは依存性の注入のためのフレームワーク「Extenject」の機能の一部分だと思います。Extenject以外にも依存性注入のフレームワークは「VContainer」など公開されていますので是非扱いやすさや用途に合ったものを選んで利用していただけますと幸いです!
参考サイト様
・【Unity】【Zenject】Zenjectをサクッと使って理解する
・[Unity] Zenject入門
・Dependency Injection: 依存性の注入 のお役立ち例
・【Unity(C#)】Extenject(Zenject)使って入力機能を切り離したものを管理してみた
・Extenject(旧Zenject)を触ってみた ~Inputの切り替えを例に~
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。