はじめに
VContainerとは、Unity用のDIコンテナです。DIとは、Dependency Injection=依存性の注入だそうです。
依存性の注入
何か注入すんだよ。
カレー定食クラス群(依存関係あり)
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 が依存関係を見て順番に作ってくれる。
VContainer とは
- 「依存関係を自動で解決して、必要なオブジェクトを組み立てる仕組み」。
- 呼び出しをつなげるわけではなく、オブジェクトの組み立て(new 連鎖)を裏で自動でやってくれる。
var 定食 = new カレー定食(
new ご飯(new 炊飯器(new 米(), new 水())),
new カレー(new 鍋(new 肉(), new 野菜(), new ルー()))
);
実際の解決の流れ
「カレー定食ちょうだい」と container.Resolve<カレー定食>() を呼んだとき:
カレー定食 を作りたい
↓
必要: ご飯, カレー
↓
ご飯 を作りたい
↓
必要: 炊飯器
↓
炊飯器 を作りたい
↓
必要: 米, 水
↓
米 new!
水 new!
炊飯器 new(米, 水)
ご飯 new(炊飯器)
↓
カレー を作りたい
↓
必要: 鍋
↓
鍋 を作りたい
↓
必要: 肉, 野菜, ルー
↓
肉 new!
野菜 new!
ルー new!
鍋 new(肉, 野菜, ルー)
カレー new(鍋)
↓
カレー定食 new(ご飯, カレー)依存関係のチェーンを 深掘りして順番に new していく。これを VContainer が全部自動でやってる。
つまり、VContainer は「コンストラクタに書かれた引数」を見て 依存関係グラフを作る。
そのグラフを「必要なものから順番にたどって new」していく。
だから「new の地獄」を書かなくても、**最終的な完成品(カレー定食)**を一発で手に入れられる。
なるほど、やっと依存関係の解決って言葉が、しっくりきた・・・。
Playmakerでも使いたい
Playmaker と VContainer の関係
Playmaker
→ FSM(状態遷移)で「何をするか」を管理するツール
VContainer
→ オブジェクトやサービスを「どう組み立てるか」を管理する仕組み
1. VContainer がやること(材料の準備)
VContainer の役割は「材料を組み立てて渡すこと」。
- VContainer 側の設定
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);
2. Playmaker がやること(振る舞い・状態管理)
Playmaker の FSM は「どう振る舞うか」を管理する。
例:FSM のステート
- 注文を受ける
- VContainer に「カレー定食ちょうだい」とお願いする
- カレー定食が来たらお客さんに提供する
Playmaker から VContainerを呼ぶ例
using HutongGames.PlayMaker;
using VContainer;
public class OrderCurry : FsmStateAction
{
public override void OnEnter()
{
// VContainer から依存関係解決
var currySet = ProjectContext.Instance.Container.Resolve<カレー定食>();
UnityEngine.Debug.Log("PlayMaker: カレー定食を受け取りました!");
Finish();
}
}FSM の「カレーを注文する」ステートにこのアクションを入れると、
Playmaker が注文 → VContainerが材料を揃えて作る → 提供する という流れになる。
まとめ
VContainer は材料と料理を用意する工場(依存性解決)
VContainer = 厨房のシェフ 🍳
→ 「カレー定食をちょうだい」と頼むと、自動で米・水・炊飯器…ぜんぶ揃えて作ってくれる。
Playmaker はその料理をどう出すかを管理する脳みそ(状態遷移)
Playmaker = 店長 👨🍳
→ 「お客さんが来たら → カレーを注文 → 提供 → 会計 → 次の客へ」みたいな 流れや状態管理を担当。
なるほど、いまいち実用メリットがようわからん。
例えば「ガチャを引く処理」を作るとする。
VContainerがない場合
public class GachaManager {
private NetworkClient network;
private Logger logger;
private UserData user;
public GachaManager() {
network = new NetworkClient(); // 直接 new
logger = new Logger(); // 直接 new
user = new UserData(); // 直接 new
}
public void DoGacha() {
network.Request("/gacha");
logger.Log("ガチャ実行");
}
}のようになる。直接 new する設計は 短期的には楽だけど、
- テストできない
- 環境切り替えできない
- 依存が隠れて分かりにくい
- 再利用性ゼロ
- 修正コストが爆増
VContainerありの場合
public class GachaManager {
readonly NetworkClient network;
readonly Logger logger;
readonly UserData user;
public GachaManager(NetworkClient network, Logger logger, UserData user) {
this.network = network;
this.logger = logger;
this.user = user;
}
}
- テスト用のモックを差し替えられる
- 環境ごとのクライアントを簡単に切り替えられる
- 依存関係が明示的でわかりやすい
- 修正は LifetimeScopeの設定だけで済む
LifetimeScopeで登録
using VContainer;
using VContainer.Unity;
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
// 依存関係を登録
builder.Register<NetworkClient>(Lifetime.Singleton);
builder.Register<Logger>(Lifetime.Singleton);
builder.Register<UserData>(Lifetime.Singleton);
builder.Register<GachaManager>(Lifetime.Singleton);
}
}
依存を受け取るクラス側
public class GachaManager
{
readonly NetworkClient network;
readonly Logger logger;
readonly UserData user;
// VContainer がここを見て自動で渡してくれる
public GachaManager(NetworkClient network, Logger logger, UserData user)
{
this.network = network;
this.logger = logger;
this.user = user;
}
public void DoGacha()
{
logger.Log("ガチャ実行!");
network.Request("/gacha");
}
}
MonoBehaviourで使う場合
using VContainer;
public class GachaButton : MonoBehaviour
{
GachaManager gachaManager;
[Inject]
public void Construct(GachaManager gachaManager)
{
this.gachaManager = gachaManager;
}
public void OnClick()
{
gachaManager.DoGacha();
}
}- LifetimeScope で依存を登録
- クラスは コンストラクタ or [Inject] メソッドで必要な依存を宣
- MonoBehaviour も
[Inject] Construct()を用意すれば勝手に注入される
おわりに
確かに機能ごとのマネージャークラス10個くらいから互い呼び出して、それぞれが「誰を使うか」の依存関係がごちゃごちゃになって追うの大変だったから、VContainer使うことにより、メンテしやすいんだろうなってのは頭のなかでモヤモヤしてる。
Qittaでも書いたよ。

