はじめに
Unity用のDIコンテナであるVContainerの依存性注入とは何かをおさらいしました。
DI = Dependency Injection = 依存性注入、とは言われても「何をどう注入するの?」
という疑問が湧きますよね。
この記事では「カレー定食クラス群」を例に、依存関係の仕組みを考えたいと思います。
依存性の注入(Dependency Injection)とは?
「依存性を注入する」という言葉ではピンと来ないことも多いです。
例えば「クラスAがクラスBを使うにはどうやって“B”を渡すか?」という構造を抜きにしてしまうと、実装が見えづらくなります。
そこで、以下の “カレー定食クラス群(依存関係あり)” を見てみましょう。
// ご飯を作るのに炊飯器が必要
public class ご飯 {
public ご飯(炊飯器 riceCooker) {
UnityEngine.Debug.Log("ご飯が炊けました!");
}
}
// 炊飯器を作るのに米と水が必要
public class 炊飯器 {
public 炊飯器(米 rice, 水 water) {
UnityEngine.Debug.Log("炊飯器に米と水を入れました!");
}
}
public class 米 {
public 米() {
UnityEngine.Debug.Log("米を準備しました!");
}
}
public class 水 {
public 水() {
UnityEngine.Debug.Log("水を準備しました!");
}
}
// カレーを作るのに鍋が必要
public class カレー {
public カレー(鍋 pot) {
UnityEngine.Debug.Log("カレーができました!");
}
}
// 鍋を作るのに肉・野菜・ルーが必要
public class 鍋 {
public 鍋(肉 meat, 野菜 veg, ルー roux) {
UnityEngine.Debug.Log("鍋に材料を入れました!");
}
}
public class 肉 {
public 肉() {
UnityEngine.Debug.Log("肉を切りました!");
}
}
public class 野菜 {
public 野菜() {
UnityEngine.Debug.Log("野菜を切りました!");
}
}
public class ルー {
public ルー() {
UnityEngine.Debug.Log("ルーを準備しました!");
}
}
// 最後にカレー定食を作るのに ご飯 と カレー が必要
public class カレー定食 {
public カレー定食(ご飯 gohan, カレー curry) {
UnityEngine.Debug.Log("カレー定食が完成しました!");
}
}
このように、クラスがそれぞれ“何かを必要として”構築されています。
この「必要としている」部分を丁寧に管理するのが DI です。
VContainerで組み立てる
続いて、VContainerを使った登録・解決の仕組みを見ていきましょう。
using VContainer;
using VContainer.Unity;
public class CurryLifetimeScope : LifetimeScope {
protected override void Configure(IContainerBuilder builder) {
// 材料と調理器具を登録
builder.Register<米>(Lifetime.Singleton);
builder.Register<水>(Lifetime.Singleton);
builder.Register<炊飯器>(Lifetime.Singleton);
builder.Register<ご飯>(Lifetime.Transient);
builder.Register<肉>(Lifetime.Transient);
builder.Register<野菜>(Lifetime.Transient);
builder.Register<ルー>(Lifetime.Singleton);
builder.Register<鍋>(Lifetime.Transient);
builder.Register<カレー>(Lifetime.Transient);
// 最後にカレー定食を登録
builder.Register<カレー定食>(Lifetime.Transient);
}
}
public class CurryTester : MonoBehaviour {
[Inject]
public void Construct(カレー定食 set) {
UnityEngine.Debug.Log("お客さんにカレー定食を提供しました!");
}
}
実行ログ(想定)
米を準備しました!
水を準備しました!
炊飯器に米と水を入れました!
ご飯が炊けました!
肉を切りました!
野菜を切りました!
ルーを準備しました!
鍋に材料を入れました!
カレーができました!
カレー定食が完成しました!
お客さんにカレー定食を提供しました!
何が便利か?
-
newを手書きでたくさん書かず、VContainerに任せられる - 依存関係を「どのクラスが何を必要としているか」という形で明示できる
- テストや環境切り替え、ライフタイム管理などがしやすくなる
var 定食 = new カレー定食(
new ご飯(new 炊飯器(new 米(), new 水())),
new カレー(new 鍋(new 肉(), new 野菜(), new ルー()))
);
上記のような構築の手間を、VContainerが代行してくれます。
Playmakerで使う
Playmakerと組み合わせて使うケースを検証します。
VContainerがやること(材料の準備)
builder.Register<米>(Lifetime.Singleton);
builder.Register<カレー定食>(Lifetime.Transient);
Playmakerがやること(振る舞い・状態管理)
FSM(状態マシン)で「注文 -> 提供 -> 会計」などの流れを管理します。
using HutongGames.Playmaker;
using VContainer;
public class OrderCurry : FsmStateAction {
public override void OnEnter() {
var currySet = ProjectContext.Instance.Container.Resolve<カレー定食>();
UnityEngine.Debug.Log("Playmaker: カレー定食を受け取りました!");
Finish();
}
}
このように、
- Playmaker = 店長/状態管理
- VContainer = 厨房/依存性解決
という役割分担をイメージできます。
まとめ
- VContainer =「材料と料理を用意する工場(依存性解決)」
- “カレー定食をちょうだい”と言うと、自動で米・水・炊飯器…全部組み立てて提供してくれる
- Playmaker =「お客さん来たら→注文→提供→次」みたいな流れを管理する店長
- DI を使うと、直接
newをたくさん書く設計と比べて、テスト性・再利用性・複雑系管理のしやすさが向上する
実際のプロジェクトで依存関係が増えてくると、より自然に理解が深まります。
おわりに
ゲームの設計が複数のクラスやモジュールにまたがり始めると、
- 誰が誰を使っているのか
- 新しい処理をどこに追加するか
- いつ生成していつ破棄するか(ライフタイム管理)
といった課題が一気に増えていきます。
カレーの例:
「カレー定食ちょうだい」
-> 米・水・炊飯器・鍋・肉・野菜・ルーが全部そろった状態で出てくる。
ゲームだと:
「敵ちょうだい」
-> HP、AI、アニメ、ステータス、ドロップロジックがセットになった敵が出てくる。
「バトル画面ちょうだい」
-> UI、BGM、キャラ情報、計算ロジック、敵データが全部まとまって生成される。
「セーブデータちょうだい」
-> SaveSystem、JSON処理、暗号化、保存場所などが一式そろった状態で渡ってくる。
そんなときに VContainerが依存関係を整理し、ゲーム全体の構造をスッキリ整えてくれるので、規模が大きくなるほど “導入してよかった” と実感できるはずです。