Vcontainer v0.2.0 に ECSサポートが入りました

先々々週 あたり、Unity(ゲームエンジン)で使える 自作DIライブラリを公開しました。

github.com

高速でコードサイズが小さく、GCゴミが少ないところが特徴です。

Zenjectと比較すると、ZenjectがDIの宣言によってあらゆることをコントロールしようとしているのとは対照的に、VContainer は DI宣言内ではできることを限定しつつ、DIのスコープ自体をコントロールするAPIを提供していたり、アセットやシーンのロードのしかたをアプリ側の自由にさせるといった、 Unityや基盤部分にDIを従属させるような考えかたをしており、 Unityのどのプロジェクトにもフィットしやすい 大根おろしに醤油をかけたもの的な存在を目指しています。

そして先日、v0.2.0 (という控え目なバージョン)に ECS (Entity Component System) のサポートを入れてみたので紹介です。

ちなみに、この機能は プロジェクトにcom.unity.entities パッケージがインストールされている場合のみコンパイラフラグで有効になります。

ECS (Entity Component System)

ECS は、 Unity が 自身の コア機能の 再構築を 目標として育てている 、ゲームエンジンとしてのかなりベース部分に対してのC# APIと機能群です。

ECSはUnityのいつもの GameObjectMonoBehaviour (Component) といった コンポーネント指向の概念を継承しつつも、それらを新しい設計と魔法で 書き下ろしていて、元々ある一般的な「ECS」のパターンに忠実なパーツ分けがなされています。
( 将来的には Unityの既存の機能を置き換えていく構想なそうな

ECS は C# の通常のランタイムの機能を 一部置き換えるような Unity独自のテクノロジを含んでいて、コンパイル時のコード解析による依存コンポーネント解決、ジョブ内でのヒープ使用の制限、など、独自路線のブラックボックス感がややとっつきずらいものの、ユーザレベルで書くC#APIは 以前よりもstrict な感じだし、パフォーマンスと並列実行の課題をデータと関数の分離によって対処しようとしてるところなんかは割と今風です。

現時点では、Unityのオーサリングツール部分は既存のものを併用しつつも、GameObject / MonoBehavior 的なものを部分的にECSで置き換えるといった利用は既にできるようになってきてるぽいです。

ECS と DI って併用できるんですか

ECS は DOTS (Data Oriented Technology Stack) と呼ばれている、Unity の独自テクノロジで成り立っています。

DOTS は 単純な C# ライブラリだけを意味せず、C#のランタイムが担っていた機能の置き換えも含んでいます。CLRのマネージドメモリに頼らない独自のゲーム特化の効率的なメモリ管理システムや、C#のサブセットを独自コンパイラで高速化するなどのUnity独自の発明があります。

そのため、たとえば ECS の Entity (GameObjectの概念の再定義) に紐づけられるデータはCLRではなくUnityが管理下に置くし、スケジュールされたJob内ではCLRのヒープが使えないなど制限があり、そのような場所では我々は通常の OOPとは別のノリとリズムとグルーブ感で踊らなければいけません。

この辺だけ見ると、一見、通常のOOPのパターンである DI は 出番がなさそうにも遠くからは見えていたのですが、ECSユーザの方のアドバイスを頼りに調べてみたところ、ECS のパーツのうち、処理を記述する System については、データやJobのコア機能とは分離されており(それらを繋ぐラッパのような構成)、外側から見えるインターフェイスは通常のクラスとしてデザインされています。System 周辺の初期化やセットアップには通常のC#が利用できる & DI がフィットする場所がありそうでした。

これまでのUnityでは、プレゼンテーションレイヤ以下は GameObject を使って表現しつつも、より抽象的なドメインロジックはUnityに依存しないC# で書く、といったレイヤ分けができましたが、ECSになっても、GameObjectいかんのレイヤが丸ごと入れ替わるだけで、通常のC#世界とUnity世界を連携させる、といった形は変わらずあり続けそう、という所感です。

VContainer は、まずは 通常のC# 世界と SystemのつなぎめにDIパターンを持ち込んでみています。

VContaienr の ECS Integration

v0.2.0 では、System を Injection する (される) 機能を追加されました。

現状、System を初期化する方法は二種類あります。 - デフォルトの設定では、プロジェクト内に定義した System は全て自動的に 初期化されて 走り出す
- 一方、このデフォルトの初期化を無効にして、自前で System を new して 初期化することも設定次第で可能

VContainer ではこれのどちらのケースにも対応することにしました。
DI で 注入するのであれば、明示的に初期化するほうが素直ですが、デフォルト挙動の方がGameObjectをEntityに自動変換するお助け機能を持っていたり、デフォルトだけに何かと手厚いサポートがあるため、状況によってどちらもニーズがありそう、と何人かのECSユーザからアドバイスもらいました。

使い方

以下のようなECSのための専用メソッドを使うと、System に対してInjectしたりされたりが可能です。 VContainer は、汎用的なDIの宣言だけではなく、ハイコンテキストな型に対しては専用の構文を用意していくスタイルです。

// デフォルトのWorldの中の自動的に初期化された Systemを取り出して DIコンテナに登録する (Injectする)
builder.RegisterSystemFromDefaultWorld<SystemA>();

// World を生成してVContainerに管理される (PlayerLoopへの登録やSystemGroupの初期化もやってくれる)
builder.RegisterNewWorld("My World 1", Lifetime.Scoped);

// DIコンテナ内のWorldにSystemを追加して 登録
buidler.RegisterSystemIntoWorld("My World 1");
// 上記のエイリアス
builder.UseDefautWorld(systems => 
{
    systems.Add<SystemA>();
    systems.Add<SystemB>();
    // ...
});

builder.UseNewWorld("My World 1", Lifetime.Scoped, systems => 
{
    systems.Add<SystemA>();
    systems.Add<SystemB>();
    // ...
});

上のような登録をしておくと、System を Injectしたりされたりすることができます。

class SystemA : SystemBase
{
    [Inject]
    public void Construct(FooService foo) { /* ... */ }

    protected override void OnUpdate() { /* ... */ }
}

この World というオブジェクトは、System のサービスロケータ的な存在でもあり、Worldから型を指定してSystemの参照を取るAPIなどをUnityが用意しています。

内部的には、VContainer は ほぼ World のラッパですが、System に IDisposable を実装した場合は、World破棄時にSystemも Disposeされるという追加機能が用意されてます。

そのほかの詳細は READMEを。 github.com

そんなこんなで

上記の機能は、IL2CPPビルドでユニットテストをパスすることは確認してますが、
実は 自分のプロジェクトでは まだ ECS を使えていないため、なんらか対応できていないケースもあるかもしれません。 フィードバックお待ちしております。