Avatar VCS
VRC用アバター改変のUnityパッケージ
背景
Unityプロジェクトにおいてバージョン管理はGitで行われることが多い。しかし、UnityのアセットはGitで管理するには不向きなものが多く、特にVRC用アバターの改変はGitで管理することが難しい。そのため、視覚的にアバターの改変を管理できるツールを作成することにした。
工夫したこと
できるだけ関数を単純化することでテストしやすくした。また、アバターの改変を視覚的に管理できるようにUIを工夫した。利用者からのフィードバックをもとに、UIの改善を行った。
振り返り
Unityにおけるテストの難しさを痛感した。Unityのテストは、Unityのエディタ上でしか動作しないため、CI/CDの導入が難しい。また、WEB開発と違い、クライアント側で持つ情報が多いために互換性を持ったバージョンアップを行うことが難しいと感じた。設計の段階から、どのようにバージョンアップを行うかを考える必要があると感じた。
技術詳細開く閉じる
アセンブリの分け方
Unity のテストが書きにくい原因の多くは、ロジックが Editor API とシーンに貼り付いていることにある。そこで UnityEditor に依存するコードと依存しないコードを、アセンブリの単位で分けた。
flowchart TB
E["AvatarVcs.Editor / UI・記録・適用・履歴操作"]
C["AvatarVcs.Core / モデル・履歴・差分・表示ロジック"]
R["AvatarVcs.Runtime / マーカーコンポーネント"]
E --> C
C --> R
E --> R
Core は UnityEditor API を一切呼ばないので、シーンも AssetDatabase も立てずにテストできる。差分計算やコミットIDの検証、ブランチ HEAD の更新といった、壊れると痛いところはすべてこの層に置いた。Editor 層に残るのは Unity API を実際に叩く部分だけになる。
UI と状態の分離
EditorWindow に状態を持たせるとテストできる範囲が一気に狭まるため、状態と遷移は AvatarVcsPresenter に寄せ、ウィンドウ側は描画と操作の受け渡しだけにした。Presenter が依存するのは3つのポートだけで、Editor 実装はそれぞれの裏に置いている。
| ポート | 役割 | Editor 実装 |
|---|---|---|
IHistoryStore |
コミット・インデックス・設定の読み書き | EditorHistoryStore |
IAvatarGateway |
シーン上のアバターへの操作 | EditorAvatarGateway |
IUserPrompt |
確認ダイアログの表示 | EditorUserPrompt |
ブランチ切り替えや比較モードの遷移は Presenter のテストで確認できる。ダイアログを差し替えられるので、「削除の確認で No を押したとき」のような分岐もウィンドウを開かずに書ける。
保存する場所と形式
コミットは Unity のアセットではなく ProjectSettings の下に JSON で置いている。
ProjectSettings/AvatarVcs/
├── guid-remapping.json
└── avatars/{avatarGuid}/
├── config.json ブランチ名 → HEAD コミットID
├── index.json コミット一覧
└── commits/{commitId}.json
アバターは32文字の hex GUID で識別する。この GUID はアバターに付けたコンポーネントが持っていて、アバターを複製すると GUID が重複するため、OnValidate で検知して振り直している。GUID とコミットIDはそのままファイルパスになるので、書き込み前に形式を検証してパストラバーサルを防いでいる。
記録するのは Prefab そのものではなく、Prefab の GUID 列と、その上に乗せた差分(Transform、tag/active/layer、コンポーネントのフィールド値、BlendShape、マテリアルのスロット、シェーダープロパティ)である。アバターの改変は「どの衣装を入れたか」より「入れたあとに何をどういじったか」が本体で、そこだけが Git で扱いにくかったため、そこを構造化して持つことにした。
コミット時にはアセットの内容ハッシュも一緒に記録しておき、checkout のときに現在の内容と突き合わせる。衣装側が更新されていた場合に、黙って適用せず警告を出せるようにするため。
配布とCI
配布は VPM リポジトリの形を取っていて、リポジトリ直下の index.json がパッケージの索引になっている。リリースはタグを押すと GitHub Actions が走る形にした。
タグの v を除いた文字列と、そのタグ地点の package.json の version が一致しない場合はそこで失敗させている。main の先頭を指していないタグからリリースすると、名前と中身の食い違った zip が出てしまうため。検証を通ると zip を作り、index.json に版を追記して main へ書き戻し、GitHub Release を作る。
テストは push と PR のたびに game-ci/unity-test-runner で EditMode を実行する。Core と Editor の2アセンブリに分けてあるので、シーンを必要としない方は同じ仕組みの中で軽く回る。