メインコンテンツまでスキップ

VContainer

VContainerは Unity(ゲームエンジン)上で動作する高速なDIフレームワークです。(DI = Dependency Injection のこと。)

"V" は、Unity の頭文字 "U" を 鉛筆削りで鋭く削ってスリムにした形状をイメージしています。

  • 高速なResolve: Zenjectとの比較でおよそ 5-10倍の性能
  • GCにやさしい: オブジェクト解決時 ゼロアロケーション 。(生成したインスタンス自身を除く)
  • コンパクトなサイズ: internal 型や .callvirt 命令とかも少ない。ビルドサイズにやさしい。
  • 過不足のない機能: シンプルで透明性のあるAPI。過度に複雑で理解し難い設定を避けます。
  • イミュータブルなコンテナ: 一度コンテナをつくったら遅い処理がほとんどない。スレッドセーフ。

主な機能#

DI + Inversion of Control for Unity#

image

通常、Unityは MonoBehaviour を継承したクラスのみを処理の起点にできますが、VContainerを用いることでオブジェクト同士の参照関係/所有関係を自由に構築したり、純粋なC# クラスのエントリポイントをつくることができます。これはUnityのコンポーネントに依存する部分・見た目の部分と、その他純粋なロジックを分離する設計の手助けになります。

このような制御の反転はIoC(Inversion of Control) などと呼ばれていて、DIコンテナの設計上の利点のひとつです。

パフォーマンス#

Benchmark result for 10,000 iterations for each test case (Unity 2019.x / IL2CPP Standalone macOS)#

  • デフォルトでは、VContainerやZenjectはDIのために実行時のリフレクションを使います。
  • "VContainer (CodeGen)" は コンパイル時 IL コード生成による高速化モード時の性能です。

GC Alloc result in the Resolve Complex test case (Unity Editor profiled)#

基本の使い方#

VContainerを使うには、最初にスコープを表現するコンポーネントを作成します。設定をC#コードで記述することで、自動的にオブジェクト同士の参照が組み立てられます。

public class GameLifetimeScope : LifetimeScope{    public override void Configure(IContainerBuilder builder)    {        builder.RegisterEntryPoint<ActorPresenter>();
        builder.Register<CharacterService>(Lifetime.Scoped);        builder.Register<IRouteSearch, AStarRouteSearch>(Lifetime.Singleton);
        builder.RegisterComponentInHierarchy<ActorsView>();    }}

クラス定義の例)

public interface IRouteSearch{    /* ... */}
public class AStarRouteSearch : IRouteSearch{    /* ... */}
public class CharacterService{    readonly IRouteSearch routeSearch;
    public CharacterService(IRouteSearch routeSearch)    {        this.routeSearch = routeSearch;    }}
public class ActorsView : MonoBehaviour{    // ...}
public class ActorPresenter : IStartable{    readonly CharacterService service;    readonly ActorsView actorsView;
    public ActorPresenter(        CharacterService service,        ActorsView actorsView)    {        this.service = service;        this.actorsView = actorsView;    }
    void IStartable.Start()    {        // VContainerのPlayerLoopSystemが`Start`を呼び出します。    }}
  • この例では、 CharacterServicerouteSearch が自動的に AStarRouteSearch のインスタンスとして設定されます。
  • エントリポイントとして登録した ActorPresenter は,Unityのライフサイクル上にスケジュールされます。
  • くわしくは:

動的なスコープ生成と非同期#

LifetimeScopeは、動的に子スコープを作ることができます。ゲームでよく発生する非同期のリソース読み込みに対応できます。

public void LoadLevel(){    // ... 追加のアセットを実行時に読み込んだりする ...!
    // 子スコープ作成の例    instantScope = currentScope.CreateChild();
    // 特定のプレハブを使用した子スコープ作成の例    instantScope = currentScope.CreateChildFromPrefab(lifetimeScopePrefab);
    // 追加でインスタンスを登録した子スコープ作成の例    instantScope = currentScope.CreateChildFromPrefab(        lifetimeScopePrefab,        builder =>        {            // Extra Registrations ...        });
    instantScope = currentScope.CreateChild(builder =>    {        // ExtraRegistrations ...    });
    instantScope = currentScope.CreateChild(extraInstaller);}
public void UnloadLevel(){    // スコープの破棄    // コンテナが管理しているオブジェクトもDisposeされる    instantScope.Dispose();}

また、追加のUnityシーンをAdditiveで読み込む際に、LifetimeScopeの親子関係を作ることもできます。

class SceneLoader{    readonly LifetimeScope currentScope;
    public SceneLoader(LifetimeScope currentScope)    {        currentScope = currentScope; // Inject the LifetimeScope to which this class belongs    }
    IEnumerator LoadSceneAsync()    {        // LifetimeScope generated in this block will be parented by `this.lifetimeScope`        using (LifetimeScope.EnqueueParent(currentScope))        {            // If this scene has a LifetimeScope, its parent will be `parent`.            var loading = SceneManager.LoadSceneAsync("...", LoadSceneMode.Additive);            while (!loading.isDone)            {                yield return null;            }        }    }
    // UniTask example    async UniTask LoadSceneAsync()    {        using (LifetimeScope.EnqueueParent(parent))        {            await SceneManager.LoadSceneAsync("...", LoadSceneMode.Additive);        }    }}
// LifetimeScopes generated during this block will be additionally Registered.using (LifetimeScope.Enqueue(builder =>{    // Register for the next scene not yet loaded    builder.RegisterInstance(extraInstance);})){    // Loading the scene..}

UniTask#

async UniTask なメソッドを処理の起点にすることもできるようになっています。

public class FooController : IAsyncStartable{    public async UniTask StartAsync(CancellationToken cancellation)    {        await LoadSomethingAsync(cancellation);        await ...        ...    }}
builder.RegisterEntryPoint<FooController>();

Getting Started#