本ページにはプロモーションが含まれます
PlaymakerUnity

【Unity】VContainerとは

この記事は約17分で読めます。

はじめに

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 のステート

  1. 注文を受ける
  2. VContainer に「カレー定食ちょうだい」とお願いする
  3. カレー定食が来たらお客さんに提供する

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でも書いたよ。