#oss #CSharp #Unity #DI > [!NOTE] > これは [Gotanda.unity #16](https://meetup.unity3d.jp/jp/events/1268) (2021/1/13) の発表内容です --- ## What's VContainer - https://vcontainer.hadashikick.jp - Unity専用 の DI コンテナ - Unity に DI ? → この話の後半で - 特徴 - 低オーバヘッド - Unity専用DI界で圧倒的な実行速度 - GCゴミが **非常に** 少ない / コードサイズがとても小さい - **必要十分な機能を厳選** - 目的を見失った滅多打ちのDIを非推奨にしていくスタイル - 透明性(=何が起きてるかわかりやすい)があるシンプルAPI - 非同期を想定 - 非同期なアセット/シーンのロードを自由に記述可能 - プロジェクト固有の選択を邪魔しない --- ## VContainer DIコンテナとしての性能 ![](https://i.gyazo.com/8b1ae955a4375bfdbb44163899801f79.png) - Zenject(有名) より 10倍〜速(当社比)。Unity Editor上では30倍くらい速い。 - 複雑なケースになるにつれてのパフォーマンス劣化が小さい - = 低オーバヘッドだから - IL生成によるメタプログラミング焼き込みモードでさらに5倍速 --- ## × DI は遅い - ○ 残念ながらZenjectが遅い - リフレクションは、せいぜいメソッド呼び出しの数十倍遅い程度。 - 普通はそこまで問題にならない - Zenject は シーン内の全MonoBehaviourに対してのリフレクション、Resolveでのアロケーションがあるので、影響が表面化しかねないレベル --- ## VContainer GC Alloc ![](https://i.gyazo.com/3e8ed08c11573ca3d8d46078e175850b.png) - VContainerのResolveは **ゼロアロケーション** を達成 --- ## GC は遅いとか速いとかより Stop the Worldが問題 - GC.Collect で Unityの全マネージドスレッドは停止 - [Unity - Manual: Understanding Automatic Memory Management](https://docs.unity3d.com/Manual/UnderstandingAutomaticMemoryManagement.html) - メインスレッドを止めることが体験の劣化につながるソフトウェアではGCがあること自体がハンデ - GCゴミを減らすこと = 実行速度よりも時に大事 - → Apple が iOS SDK に GC を載せない理由 --- ## VContainer コードサイズ ![](https://i.gyazo.com/476de69d0c786a523ebbdb24a9758024.png) - ランタイム部分の コードサイズ Zenjectのおよそ $\frac{1}{10}$ - クライアントサイドにでかいもの入れたくない教に朗報 --- ## IL → C/C++ 変換時のサイズ増加割合が少なめ - 内部の型の複雑な継承関係がない - .callvirt より .call - .call 命令 → C++ の関数呼び出しにマッピングされる - .callvirt命令 → IL2CPPが吐いた型ごとのvtable経由での関数呼び出し --- ## VContainerの「V」とは… - Unityの頭文字「U」を、夜な夜な彫刻刀で1ミリずつ削ってスリムにした形状をイメージ(観念の世界) --- ## ゲームにDIを持ち込むことのメリットってあんの? 1. Inversion of Control (IoC) - 一貫した単純な 制御フローの強制に一役 - DI ← マーティン・ファウラーが名付ける以前は IoC Container とも 2. オブジェクト同士の干渉に良い感じの制約をつける - 大きめコードベースでは、部品の意図を伝えるために制約は割と有用 3. インターフェイスとかを使った抽象化のサポート - そこまで重要ではない(かも) --- ## 教科書的な話への感想 - 実装ではなく抽象に依存させる ? - これは目的ではなく手段 - 抽象化はコストがかかる。リーズナブルな買い物をしよう - とはいえ Unityは Interfaceのサポートが弱いので有用な場面はある(かも) - テスタビリティ ? - DI しなくてもスタブは挿せる(とマーティンファウラーも言ってました - ユニットテストがマッチするような場所はそもそも変な依存を取り除く --- ## ゲーム特有の難しいとこ - 素朴なUnityプログラミングでは、命令を下す側と、命令を下される側が混ぜこぜになりやすい - Unityのオブジェクトは、実行時にめまぐるしく生成/破棄が繰り返される - DI が育まれた javaウェブアプリケーション世界ではそんなことは起こらない - → ゲームの複雑さそのものはDIでは解決できない。 - → シンプルなDI でOKな構造 = シンプルな構造 --- ## Unity = Easy to use - GameObject - シリアライズとツールの統合 - 機能を Componentで分割しつつ、内側にカプセル化 - Prefab の最強ポータビリティ - → **使いやすい。ありがとうUnity。ベストフレンドフォーエバー。** - 自身が内側に隠蔽しているオブジェクトは詳細を知っていて特に問題ない。 - Prefabの最強ポータビリティをDIで殺すのはもったいない --- ## GameObject = Component Entity pattern ![](https://i.gyazo.com/a6d6c9faf76a26f84f397e97a0641943.png) - シーン上のオブジェクトは それ単体で 完全に動作する - 直感的でわかりやすい - 水平分割のみのサポート --- ## Unityに足りないもの - GameObject 同士の メッセージのやりとりのサポートが微妙 - 素朴に考えると、GameObject同士の関連が爆発してしまう --- ## ゲームにおけるオブジェクトの参照関係を考えよう ![](https://i.gyazo.com/94638b7053c465a35422158bee6dfdc9.png) --- ## 人間の脳内での素朴な参照関係 ![](https://i.gyazo.com/ec081eb723beac17d672a4c197f8dc58.png) - N:Nの関係構築の連打 - → 茹で時間: 1分 --- ## オブジェクト指向のオブジェクトとは… - オブジェクトは、(直接的には)メソッドを叩く/叩かれる という通信方法しか持たないので - 「犬」とか「猫」みたいな現実世界で認識している「物」をまんまモデル化しようとしただけでは、参照関係がえらいことに - Unity = Easy to use --- ## 問題1 ゲームのオブジェクトの不安定な寿命 ![](https://i.gyazo.com/30c647361b55dec22a928dfaf813138f.png) - 生成/破棄がいつ起こるかわからない。実行時の条件分岐で 生成/破棄が繰り返される - N:N 関連が爆発したままこれをすると… - → 実行時エラー - → リソースのリーク - → `// なぜか1フレーム待つと直る` --- ## 問題2 複雑な制御フロー ![](https://i.gyazo.com/c146d71e28752bff2b7590eb30878dfe.png) - スパゲティコードとはつまり - 誰もが命令を下す側 - 誰もが命令を下される側 - になっている状態のこと --- ## アプリケーション設計は、クリーンかどうかとかより、メッセージングの方向が重要 - 上記のような関係性のまま、 Interfaceを切ろうが、型を挟もうが、複雑さを解決できてない --- ## 問題1 の対策. 依存関係を根本的に単純にする ![](https://i.gyazo.com/91d67e6cd01875d053e276d9820eaf0f.png) - 1:1 関係に直す - DDD / Data-Centric --- ## 「データ」は不安定な寿命をもつ - データに強く結びついたオブジェクトも当然 不安定な寿命を持つ - **GameObjectも当然不安定な寿命を持つ** - 「純粋関数」は永遠の寿命を持つ - 「データ」依存部分と「機能」を分離 - → Unity も ECS はこっちの設計に寄ってきたで - 参照解決の難易度激減 --- ## 問題2の対策. 制御フローを根本的に単純にする ![](https://i.gyazo.com/e717718360251069628bfe88c756e380.png) - 制御の反転 = IoC - → Actorをエントリポイントにしない。 --- ## 命令を下す人 = とても特別な存在 - Mediator / Event Aggregator / 中央メッセージング 的なパターンが割と有用 - 命令を下す人 - 生涯を通して誰からも直接的には指図されることはない - 命令を下される人 = その他すべて - デバッグ/テスト しやすい --- ## シンプルな構造を保つために - 複雑さを抱えたまま 強力機能やテクニックを持ち込んでも、複雑さの上に複雑さを上塗りするだけ - → DI や強力機能は、シンプルにするための制約づけや補助のためにこそむしろ使える(という提案) --- ## Unityが搭載している実行時の参照解決 - `FindObjectOfType` / `FindGameObject*` - 誰でも どこでも 実行できてしまう - 一週間に一回しか通らない分岐でのみ参照をとろうとして壊れていたら? - バグ報告/調査/修正/レビュー/デバッグ/リリース、全ての工程がもったいない - 設定内容はビルド時に保証したい --- ## OOPでの参照解決テクニック 1. どこからでもアクセスできる大域変数/メソッド - Singleton / static / またはそれの importとか - わかりやすい。楽。 - Web界隈の後発の環境は割とこれな気がする 2. Service Locator - static変数経由の変形 - どこからでもアクセスできる、オブジェクト参照のレジストリをつくる - 最近だと swift の [vapor](https://vapor.codes) 3. Dependency Injection / IoC Container - オブジェクトのコンストラクタへ、外側から参照を渡してあげる --- ## 型システム強力言語の参照解決テクニック 4. Scala発祥 Cake Pattern など - ひとつのtraitの中に、クラスの実装と、空っぽの依存オブジェクト変数宣言 をまとめるテクニック - traitの中のクラスは、「依存オブジェクト変数」がないと 使用が不可能なことが保証される - **→ コンパイル時に解決** --- ## 関数型言語での参照解決テクニック 5. Readerモナド - モナド = 純粋関数 に おまけ(副作用) をつける関数型の一般的なテクニック - 依存解決=副作用 。 「依存注入」機能をモナドでラップ - **→ コンパイル時に解決** - [frouriojs/velona](https://github.com/frouriojs/velona) - クロージャ内に依存をキャプチャ - 依存解決後の関数を返す関数を扱う --- ## DIのまあまあ良いところ - コーディングスタイルにそこまで干渉しない - コンパイル時ではないにせよ、スコープ開始時の早期にバリデーションできる - 何に依存しているか宣言的で明示的になる - 誰がいつ参照を取りにくるかルール違反しずらい - オブジェクトの寿命の管理を DI コンテナに肩代りさせることができる - → スコープという概念がある --- ## DIにおけるスコープという概念 ![](https://i.gyazo.com/a8dc786f4adb61b1bd1f18151d88c31c.png) - DIコンテナで管理されるオブジェクトは、コンテナがライフサイクルの面倒を見る - 階層構造になっている - HTTPサーバでは… - リクエスト単位でスコープができることが自明 - 通常「スコープの生成/破棄」はフレームワークが勝手にやってくれる - アプリケーションの制御フローよりも下 --- ## DIのセットアップ3ステップ 1. いつスコープができていつ破棄されるべきかの決定 - フレームワークの実装に含まれている - **アプリケーションの制御フローの関心の外** 2. アプリケーション側の設定 - 参照させたいオブジェクトの登録 (Composition Root) 3. アプリケーション側の実装 - 設定と実装の分離 --- ## ゲームにおけるスコーピング - HTTPサーバと違って、いつどこでどのようにスコープを生成するべきか自明ではない - シーン単位 - シーンより細かい単位 / etc.. - プロジェクト毎に決めたい - 非同期にリソースをロードした後にスコープを構築したい - 非同期のためのソリューションもプロジェクト毎に異なっている - UniTask / Task / IValueTaskSource / UniRx / Unity コルーチン / コールバック / etc.. --- ## VContainerの特徴 - スコープ制御 - いつでもどこでも非同期ロードした後でもスコープを宣言できる - 依存関係の設定と、スコープの生成 は分かれている - → DI のライブラリの一般的な姿 - → Zenject は 1 と 2 がくっついているがけっこう特殊 --- ## VContainerの特徴 - エントリポイント - `RegisterEntryPoint<T>(...)` で、独自PlayerLoopSystem上にスケジュールされる - MonoBehaviourに依存せずにエントリポイントになれる - → MonoBehaviourの外側から「命令を下す人」をつくれる - MonoBehaviour **に** DI をすることを積極的には推奨していない - どちらかというと MonoBehaviour **を** DI することを推奨 - MonoBehaviour は「命令を下される側」の人と見做した方が良い - そのためのDI - そのためのIoC --- ## GameObjectにたくさんのものを外から注入する必要があんまない - Viewコンポネに状態を渡したいことはしょっちゅうある? - それは「依存性の注入」じゃなくてただのメソッドの引数 - View内部にさらに複雑なパターンが隠蔽されているときは? - 外から隠蔽しているものは密結合していてあんまり問題ない - PrefabのポータビリティをDIで壊すのはもったいない --- ## まとめ - ゲームにシンプルなDI を持ち込んで嬉しいポイント - 制御フローをシンプルにすることの補助に使える - オブジェクト間の複雑すぎる関連を抑止する制約をつけることができる - 制御フローや依存関係がそもそも複雑なままDIをしても余計複雑になるだけ - VContainer は必要十分だし速い - よかったら [Github](https://github.com/hadashiA/VContainer) の ☆ も押してね m(_ _)m