UnityC#/VRChatU#用のDIライブラリを制作しリリースしました🎉
Sardinjectという Unity C#、VRChat Udon(U#) 用のシンプルなDI(依存性注入)ライブラリを制作しリリースしました。
既存のDIライブラリではカバーできなかったUdonに対してネイティブで対応していることが大きな特徴です。
また通常のUnityプロジェクトではプロジェクト=1つのアプリとなることが多いですがVRChatにおける開発においては1つのプロジェクト内に複数のアプリ(ワールド)データを置き開発することが多いためそのようなケースでも対応しやすいような設計にしています。
DIライブラリとしての大枠の設計はVContainerを参考にさせて頂きました。今までDIライブラリは使用したことはあれど自作したことがなかったので簡潔な実装になっておりとても参考になりました。
Sardinjectの特徴
Udonに標準対応(既存ライブラリではU#とUdonの対応をうまく解決できないことが多い)
Udonの存在しないUnityC#環境でも動作する
スコープをプロジェクト・シーン(ワールド)・ヒエラルキで分離できる
実行時処理負荷を減らす設計になっていてワールド入場時の負荷が減る
現状ではU#環境を中心とした設計になっているため複雑な最適化は入れていませんが今後のユースケースを見つつ機能拡張や最適化をしていく予定です。
現状ではU#環境を中心としていますがU#の存在しないUnityC#環境でも動作するように実装していますのでUnity製アプリとVRChatワールドのマルチプラットフォーム体制で制作するような用途でもご利用いただけます。
既存のDIライブラリを参考にしつつよりVRChatワールド制作事情にマッチした仕組みになるように作りました。
Sardinjectの使いどころ
DIライブラリの使いどころは使ってみないとわからない、という面が多くあります。
いきなり複雑な使い方をせず、まずは以下のような使い方から入ってみるのが良いかもしれません。
以下のようなコインを管理するクラスが存在するとします。
このクラスはワールドに1つ必ず存在してほしいもので、コインを増やしたり減らしたりしたいクラスからは参照を持つ必要があるものとします。
// コインを管理するクラス
public class CoinManager : UdonSharpBehaviour {
public int Coins;
}
利用したいクラスは以下のようになっているものとします。
コイン管理クラスに対して参照を持っています。
// ミニゲームを管理しているクラス
public class TheGame : UdonSharpBehaviour {
// コイン管理クラスへの参照
[SerializeField]
CoinManager coinManager;
// ゲーム開始のときに100コイン消費する
public void GameStart() {
coinManager.Coins -= 100;
}
}

この実装の問題点として、
コイン管理クラスが必ずワールドに1つ設置されていないといけない
コイン管理クラスへの参照をインスペクタから設定しないといけない
どちらも利用者が熟練の制作者であったりそもそもの製作者であれば仕組みを理解できるのでうまく設置と設定ができます。
しかし現実として利用者はあまり実装に詳しくないケースも多く存在し、初期設定を一つでも飛ばしてしまうとうまく動作しなかったりおかしな挙動をしてしまうことになります。
対処として初期化時にうまく処理するようにしたり、エディタ拡張などの部分でできるだけ利用者の負荷を軽減してあげたり、様々な対応を講じることになりますが後者は製作者の負担が高くなってしまいますし前者は処理負荷に繋がるため環境によってはワールドに入ったとき重すぎて動けない状況に繋がったりします。
そこでSardinjectのDIを使って以下のようにしてみます。
以下のようなクラスを作って空のゲームオブジェクトに「ProjectScope」コンポーネントと一緒に追加します。
ゲームオブジェクトをプレハブ化しResourcesフォルダを作成しその中に収めておきます。
public class CoinManagerInstaller : MonoBehaviour, IInstaller {
public void Install(ContainerBuilder builder) {
builder.RegisterComponentOnNewGameObject<CoinManager>(Lifetime.Cached);
}
}

コインを利用したいクラスを少し変えて以下のようにします。
public class TheGame : UdonSharpBehaviour {
// DIで注入するのでInject属性をつけておく
// インスペクタから触る必要がないのでHideInInspector属性もつけておく
[Inject, SerializeField, HideInInspector]
CoinManager coinManager;
public void GameStart() {
coinManager.Coins -= 100;
}
}
以上のTheGameクラスをつけているオブジェクトに「HierarchyScope」を付けておきます。
併せて以下のクラスも作成し付けておきましょう。
public class TheGameInstaller : MonoBehaviour, IInstaller {
public void Install(ContainerBuilder builder) {
builder.RegisterComponentInHierarchy<TheGame>();
}
}

少しスクリプト数は増えますが、以上の設定を行っておけばコイン管理クラスはシーンに設置しておく必要がありません。
自動的にシーン上にプレハブが生成され、利用したいクラスのcoinManagerフィールドに参照がセットされます。

副次的な効果でコイン管理クラスは使いたいクラスがいなければシーンに生成されることはありませんのでいらないものがワールドに設置されて処理負荷を食ってしまう、といったことが起こりません。

最後に
実はSardinjectは前進にあたるものを3年ほど前から制作して使っていました。
ただこれは自分が制作効率をよくするためだけに使っていたためとても使い勝手が悪く配布アセットなどに利用できる形ではありませんでした。
Sardinjectはその時得た経験などをもとに1年ほど前に作り直し1年間のブラッシュアップを経てリリースと相成りました。
アセットを作る人たちにより作りたいものの本質に集中してもらえるように、そしてアセットを使う人たちもUnityの複雑な設定やよくわからないエラーに悩まされずにいろんなアセットを使って楽しいワールドをたくさん作ってもらえたら嬉しいな、と思いOSSとして公開しました。
まだまだ使い込みが足らずバグなどあると思いますがその時は報告頂けたら嬉しいです。
本アセットは有償アセットなどへの組み込みも大歓迎です。
もし使ってみて楽できた!や新しいことができるようになった!などありましたら以下で寄付など歓迎していますのでよろしくお願い致します!