Unity Packageを自動インポートするPackageを作ってみた話
Unityの新規プロジェクトを作るたびに、毎回同じPackageを入れていました。
VContainer、R3、UniTask、MessagePipe、LitMotion、ZLinq……みたいな、だいたい自分の中で決まってる最初に入れるやつです。
毎回Package Managerを開いて、Git URLを貼って、待って、また次のURLを貼って……という作業をしていたのですが、さすがに面倒になってきました。
というわけで、自分用のUnity開発スタックを自動でインポートするPackageを作りました。
作ったもの
Unity Package ManagerからこのPackageを追加すると、私が普段使っているUnity向けPackage群を自動でインストールしてくれます。
この人たちをインストールしてくれます。
小規模では使わないヤツもあったりしますが……そんなことを考えても仕方がない。
便利なものを作ってくれた偉大な開発者に感謝。
NuGetForUnity
VContainer
UniTask
MessagePipe
MessagePipe.VContainer
SerializeReference Extensions
Reactive Input System
LitMotion
LitMotion.Animation
R3
R3.Unity
ObservableCollections
ObservableCollections.R3
ZLinq
ZLinq.Unity
R3 Extensions
Git URLのPackageをまとめて追加するだけなら簡単だったのですが、そうもいきませんでした。
問題は、R3やZLinqが少し特殊だったことです。
R3とZLinqの特殊さ
R3やZLinqをUnityで使うとき、Unity側のPackageだけを入れればはい終わりで~す、という構成ではありません。
まずNuGetForUnityを入れて、その上でNuGetパッケージとして R3 や ZLinq を入れて、そのあとにUnity向けのPackageである R3.Unity や ZLinq.Unity を入れる必要があります。
というわけで、このPackageではインストールする順番も明示的に管理しています。
全体の流れ
UnityStackInstaller では、処理をいくつかのステージに分けています。
foundation packages
↓
NuGet core packages
↓
Unity extension packages
↓
dependent git packages
↓
done
最初にNuGetForUnity、VContainer、UniTask、MessagePipe、LitMotionなどの土台になるUPM Packageを追加します。
次に、NuGetForUnity経由で R3、ObservableCollections、ObservableCollections.R3、ZLinq をインストールします。
そのあと、NuGetで入ったバージョンに合わせて R3.Unity と ZLinq.Unity を追加します。
最後に、自作(フォークしたものもあるが…)Packageである Reactive Input System と R3 Extensions を追加して完了です。
InitializeOnLoadで自動実行する
Packageを追加したら自動でインストーラーが走ってほしいので、Editor拡張側では InitializeOnLoad を使っています。
[InitializeOnLoad]
internal static class UnityStackInstaller
{
static UnityStackInstaller()
{
EditorApplication.delayCall += AutoInstallOnce;
}
}
Unity Editorが読み込まれたあと、delayCall で少し待ってから処理を開始します。
ただし、毎回Unityを開くたびにPackageを再インストールされると困るので、完了済みかどうかを ProjectSettings/UnityStackInstaller.installed に記録しています。
private const string DoneFilePath = "ProjectSettings/UnityStackInstaller.installed";
private const string StateFilePath = "ProjectSettings/UnityStackInstaller.state";
installed が存在していれば、自動実行はスキップします。
再実行したい場合はメニューから実行できます。
Tools > Unity Stack Installer > Install Packages
Unityの再コンパイルを前提にする
Unity Editor拡張でこういう処理を書くときに厄介なのが、Packageを追加すると途中で再コンパイルが走ることです。
たとえばNuGetForUnityを追加した直後は、まだNuGetForUnityの型がEditor上で使えるとは限りません。読み込みが終わっていない状態で呼び出そうとすると残念ながら失敗します。
そこで、NuGetForUnityの型が見つからない場合は、少し待ってから再試行するようにしています。
Type identifierType = FindType("NugetForUnity.Models.NugetPackageIdentifier");
Type installerType = FindType("NugetForUnity.NugetPackageInstaller");
if (identifierType == null || installerType == null)
{
Debug.Log("[UnityStackInstaller] Waiting for NuGetForUnity to finish compiling...");
nextRetryTime = EditorApplication.timeSinceStartup + 3.0;
EditorApplication.delayCall += RunInstaller;
return;
}
ここで直接NuGetForUnityの型を参照していないのもポイントです。
このインストーラー自身は、NuGetForUnityがまだ存在しない状態でもコンパイルできる必要があるので、Assembly をなめて型を探し、ReflectionでNuGetForUnityのAPIを呼び出しています。
なんか回りくどいですね。
ステージ管理はファイルに保存する
インストール途中でUnityの再コンパイルが入ると、static変数だけでは状態を保持できません。
そこで、現在どこまで進んだかを ProjectSettings/UnityStackInstaller.state に保存しています。
たとえば、最初のUPM Packageを入れ終わったら次はNuGetのステージへ進みます。
if (string.IsNullOrEmpty(state))
{
InstallUpmPackages(FoundationPackages, StageNuGet);
return;
}
if (state == StageNuGet)
{
InstallNuGetCorePackages();
return;
}
if (state == StageUnityExtensions)
{
InstallUpmPackages(BuildUnityExtensionPackages(), StageDependentGitPackages);
return;
}
この形にしておくと、Unityの再読み込みが入ってもどこから再開すればいいか分かります。
最初は正直「ファイルで状態管理するのは雑かな」とも思ったのですが、Unity Editorのライフサイクルを考えるとかなり堅実な方法でした。
R3.UnityとZLinq.Unityのバージョンを合わせる
もうひとつ気をつけたのが、NuGet側のR3/ZLinqと、UPM側のR3.Unity/ZLinq.Unityのバージョンを合わせることです。
NuGetパッケージの最新版を取得するときは、NuGetのFlat Container APIを見に行っています。
string url = "https://api.nuget.org/v3-flatcontainer/"
+ packageId.ToLowerInvariant()
+ "/index.json";
取得したバージョン一覧の後ろから見て、プレリリースではないものを選んでいます。
for (int i = index.versions.Length - 1; i >= 0; i--)
{
string version = index.versions[i];
if (!string.IsNullOrEmpty(version) && version.IndexOf('-') < 0)
{
return version;
}
}
そして、Unity向けPackageを追加するときにGit URLへ #バージョン を付けることで、NuGet側で入れたR3やZLinqと、Unity側の拡張Packageのバージョンを合わせています。
packages[i] = new PackageSpec(package.Name, package.Url + "#" + version);
UPM Packageの追加はClient.AddAndRemoveでまとめて行う
UPM Packageの追加には UnityEditor.PackageManager.Client.AddAndRemove を使っています。
request = Client.AddAndRemove(packagesToAdd, new string[0]);
EditorApplication.update += TickPackageManager;
AddAndRemoveRequest は非同期なので、EditorApplication.update で状態を監視しています。
成功したら次のステージを書き込み、AssetDatabase.Refresh() してから次の処理へ進みます。
if (request.Status == StatusCode.Success)
{
WriteState(nextStageAfterRequest);
request = null;
nextStageAfterRequest = null;
EditorApplication.update -= TickPackageManager;
AssetDatabase.Refresh();
EditorApplication.delayCall += RunInstaller;
return;
}
NuGetForUnityのPackagesは.gitignoreへ
NuGetForUnityは Assets/Packages 以下にPackageを展開します。
これをそのままGitに入れると差分が爆破されるので、インストーラー側で .gitignore に以下を追加するようにしました。
/[Aa]ssets/[Pp]ackages/*
既に .gitignore がある場合は追記し、なければ作成します。
使い方
使い方は普通のGit Packageと同じです。
UnityのPackage Managerを開いて、
Window > Package Manager
左上の + から、
Add package from git URL
を選んで、以下のURLを入力します。
https://github.com/seikasan/UnityStackInstaller.git?path=UnityStackInstaller
追加すると、インストーラーが一度だけ自動で実行されます。
手動で再実行したい場合は、次のメニューを使います。
Tools > Unity Stack Installer > Install Packages
最新版へ更新したい場合は、
Tools > Unity Stack Installer > Update To Latest
状態をリセットしたい場合は、
Tools > Unity Stack Installer > Reset Installer State
NuGetのコアPackageだけ入れたい場合は、
Tools > Unity Stack Installer > Install NuGet Core Only
を使えます。
以前の状態で壊れた場合
以前のバージョンなどで R3.Unity や ZLinq.Unity の依存関係が壊れた状態になっている場合は、Packages/manifest.json から以下を削除します。
"com.cysharp.r3": "...",
"com.cysharp.zlinq": "..."
そのあとUnityに依存関係を再解決させてから、インストーラーを再実行してください。
Tools > Unity Stack Installer > Install Packages
まとめ
新規プロジェクトを作るたびに同じGit URLを何度も貼っている人は、自分用のStack Installerを作ってみるといいんじゃないでしょうか。(知らんけど)