凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で自作ライブラリの Package Manager / Scoped Registry 対応をしてみた

Unity Advent Calender 2021

この記事は「Unity Advent Calendar 202119 日目の記事です。

qiita.com

18 日目は @Azukiidxさんによる以下の記事でした。

qiita.com

私は(残念ながら非推奨になってしまった)UNET のサーバー向けに Linux 上でヘッドレスで動かすビルドを作ったことはありましたが、Linux 上で動くエディタはまだ試したことはありませんでした。Linux の開発環境やサポートが必要になったら試してみようと思います。

はじめに

前々回の記事で、自作ライブラリの Package ManagerUPM)対応についての記事を書きました。

tips.hecomi.com

Package Manager の登場により、元々はビルトインだった数々の機能(e.g. URP/HDRP などの新レンダリングパイプライン、新しい Input System、XR 関連機能、Timeline、Cinemachine など)がオプションとして提供され、必要な機能だけ自分のプロジェクトで使えるようになりました。またそれだけに留まらず、サードパーティも同じ仕組みの上でアセットを配布できるようになり、結果としてユーザは必要な機能を Package Manager の GUI 上で簡単に追加・削除できるようになりました。更に裏側では、それらのバージョン管理が適切に行われ、依存関係が適切に解決され、またグローバルキャッシュを通じて異なるプロジェクト間でアセットが共有され、...といった具合に色々と便利になっています。サードパーティに関しては対応しているライブラリはそれほど多くないですが、それでも徐々に増えてきている印象です。こういったパッケージマネージャの仕組みは Unity 以外の環境では珍しいものではないですが、コードだけでなく GUID を通じてアセットも含めて依存関係解決できるようになっており、かなり便利だなと感じています。

前述の記事の執筆後、一月かけて .unitypackage 形式で配布しているすべての自分の Unity プロジェクトのメンテナンスと UPM 対応を行いました。また、npmjs へ登録を行い、スコープ付きレジストリ対応を行いました。本エントリではこうして UPM 対応した結果、ユーザ視点および開発者視点から、どのようにライブラリとして配布されているアセットの利用が変わるかについて、具体的な例を通じて見ていければと思います。また、UPM 対応を行う上で各ライブラリで対応した小話も後半に書こうと思います。

なお、Package Manager のついては 1 日目の @RyotaMurohoshi さんのエントリでも触れられていますので併せてお読みください。

qiita.com

パッケージ利用者側の視点

パッケージの追加(削除)

スコープ付きレジストリ(Scoped Registries)は、Unity 公式のライブラリを配布するデフォルトのレジストリに加えて、サードパーティ製のパッケージを配布するレジストリを使用することを可能にします。例えば以下のように Project Settings > Package Manager > Scoped Registries に登録してみます。

  • Name: hecomi
  • URL: https://registry.npmjs.com
  • Scope(s): com.hecomi

すると、Package Manager の Packages: My Registries から以下のように私が配布している(npmjs に登録している)パッケージが見えるようになります。同様に他の開発者のスコープ付きレジストリも登録することでリストに様々なパッケージが並ぶようになります。

f:id:hecomi:20211208004104g:plain

例として、ランタイムに REPL によるコードデバッグを可能にする uREPL を インストールして入れてみましょう。インストールすると、Packages の中に入るので、この中から必要なアセットを引っ張ってきます。uREPL では Prefab(+ EventSystem)を置くと使えるようになります。

f:id:hecomi:20211208010555g:plain

必要がなくなったら Package Manager で Remove するだけです。Assets ディレクトリを汚さずに、簡単にインストール・アンインストールが出来る感じですね。

サンプルのインポート

その他にも必要なサンプルだけインポートしやすい、というメリットもあります。例えばレイマーチングしたオブジェクト用のシェーダを簡単に生成できる uRaymarching を例に見てみます。

tips.hecomi.com

f:id:hecomi:20211208232450g:plain

インポートしたあとに Samples を見るとインポート可能なサンプルが列挙されています。例えば現在利用しているレンダリングパイプラインが Forward(Legacy)の場合、Common と Forward をインストールすると、それ用のサンプルのみがプロジェクトに追加されます(Deferred や URP など別のパイプラインのアセットはインポートしないで済む)。追加される場所は、Samples > [パッケージ名] > [パッケージバージョン] > [サンプル名] となっています。

アップデート

使用しているライブラリに更新があると、ライブラリ名の右側に↑アイコンを表示することで教えてくれます。Update [new version] ボタンを押下すると更新され、新しいライブラリが使用されるようになります。自分で逐一個別に情報を取りに行かなくてもこうして一覧化されると便利ですね。

f:id:hecomi:20211219002249p:plain

なお、古いバージョンを使用したい際はライブラリの項目を展開し、See other versions を押下して古いバージョンを表示、選択しインストールしてください。

f:id:hecomi:20211219002454g:plain

依存関係の解決

面倒くさい依存関係を自動で解決してくれます。例えば先程見た uRaymarching は uShaderTemplate というシェーダジェネレータライブラリに依存しています。

tips.hecomi.com

これまでは、.unitypackage などを個別にダウンロードしてこないとなりませんでしたが、先程見たように欲しいパッケージを入れるだけで依存ライブラリは自動的にインストールされます。例えば先ほど uRaymarching のみをインストールした場合でも、次のように uShaderTemplate もインストールされている状態になります。

f:id:hecomi:20211208234258p:plain

また、自分でインストールしたパッケージを削除すると自動的に依存パッケージも削除されるようになっています。ちなみにこの依存関係は、Show Dependencies をチェックすると、依存しているもの(Is using)と依存されているもの(Used by)が見れるようになります。

f:id:hecomi:20211219002928g:plain

グローバルキャッシュ

こうしてインストールしたパッケージは特定のディレクトリ以下に格納され、同じバージョンのものは異なるプロジェクト間であっても共有されます。

docs.unity3d.com

場所は以下になります。

OS ディレクト
Windows (システムユーザーアカウント以外) %LOCALAPPDATA%\Unity\cache
Windows (システムユーザーアカウント) %ALLUSERSPROFILE%\Unity\cache
macOS $HOME/Library/Unity/cache
Linux $HOME/.config/unity3d/cache

例えば Mac ではこんな感じです。

f:id:hecomi:20211209002137p:plain

デメリット

パッケージを導入したあとに、そのパッケージを直接色々いじるようなケースには向いていません。一応、パッケージのスクリプトはいじれますが、先に見たようにグローバルキャッシュを直接いじることになるので、他のプロジェクトでも変更が反映されてしまいます。このようなケースでは従来どおり直接 Assets 以下に追加する方式が良いでしょう。

開発者側の視点

開発者側にも色々と利点があります。ユーザが上記の様に良い体験を得られる(簡単に使ってもらえる)というのがもちろん一番大きいところですが、他にもあるのでみてみましょう。

依存ライブラリを指定できる

先程の uRaymarching の例のように、依存ライブラリを指定することでプロジェクトをシンプルに保つことが出来ます。以前は開発時に依存ライブラリを自分の Assets に含まなければならなかったのですが、依存ライブラリはパッケージとして切り分けることによって開発プロジェクトをクリーンに保つことが出来ます。

また、自分のプロジェクト間の依存関係だけではなく、Unity 公式のライブラリへの依存関係も解決できます。リップシンクを行う uLipSync というライブラリでは BurstJob を使うことでバックグラウンドスレッドで高速に音声信号処理を行う、ということをしています。

tips.hecomi.com

f:id:hecomi:20211216223854g:plain

この Burst と SIMD の効いた計算を行うことの出来る数学関数を提供する com.unity.mathematics に依存しているので、これまではユーザに README にこんな画像を貼ることでこれらをインストールするよう促す必要がありました。

f:id:hecomi:20211212145648p:plain

しかしながら package.json次のように依存関係を記述することでユーザ側でこれらを個別にインストールする必要はなくなります。また、依存ライブラリのバージョンが上がったとしてもそれらの管理責任は Unity 側にお願いできるようになります。

{
    ...
    "dependencies": {
        "com.unity.burst": "1.4.11",
        "com.unity.mathematics": "1.2.5"
    },
    ...
}

スコープ付きレジストリをまたいでの依存関係解決についてはまだ試したことがないので分かり次第追記します。

サンプルで別のライブラリを分離できる

動作として依存していなくてもサンプルとして他のライブラリに依存したいときがあります。例えば NVIDIA のハードウェアエンコーダ / デコーダである NVENC/NVDEC の実装である NvPipe *1 を使って Texture2D を H264 へとエンコード / デコードしてくれるライブラリの uNvPipe を例に取ってみてみます。

tips.hecomi.com

ライブラリの実装としてはエンコードとデコードの機能を提供するところができれば良いですが、サンプルとしてはネットワークを通じてエンコードされたデータを送ってデコードするところを含めたいなと考えました。ネットワーク部分はシンプルに UDP で送ろうと思い、それを簡単に出来る uOSC をプロジェクトに含めてサンプルを書いていました。ただこの uOSC は本プロジェクトの本質ではないのでプロジェクトに含めるのはちょっと汚い感じもします。

しかしながらパッケージのサンプル提供の機能を使うと、uOSC が必要でないサンプル、必要なサンプルと切り分けることができ、この結果ユーザは自分が欲しい物だけインストールすることができます。なのでネットワークのテストをしたい人だけ uOSC をダウンロードしたあとにサンプルを見てね、とお願いする形式にすることが出来るわけですね。依存パッケージを特定のサンプルにのみ含める、みたいな形式でも良いと思います。

f:id:hecomi:20211212205930p:plain

ここでは 02. uOSC + Network というサンプルをインポートすると uOSC なしだとエラーが出ますが入れれば動く、という感じになってます。

f:id:hecomi:20211216224235g:plain

とは言えサンプル名で追加ライブラリが必要感を出すしか出来ず...、サンプルインポート時に依存ライブラリをインストールしたりしてほしいですが...、流石にやりすぎな気もするので、せめて package.json に書いた description の表示くらいはしてもらって、ユーザにインストールしてねメッセージは表示したいところです(Unity さんお願いします...)。

デプロイは意外と簡単

GitHub で管理している人は GitHub Actions を使うと簡単にデプロイ出来ます。UPM 対応したディレクトリ構造を作るところは前回の記事に書きましたが、そこから npmjs にアップロードするところは、次のような GitHub Actions でのステップを追加し、必要な Secret を追加すれば簡単にデプロイ出来ます。

name: Create UPM branches and run NPM publish
...
jobs:
  update:
    ...
    steps:
      ...
      - name: NPM publish
        run: npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

f:id:hecomi:20211212151429p:plain

私は tag の push にフックして UPM ブランチ作成と npmjs へのデプロイを次のような GitHub Actions で行っています。

github.com

かっこいい

自分のライブラリがズラッと並ぶのを見ると満足感があります!(これが一番の理由かも)

f:id:hecomi:20211212150303p:plain

利用状況が見れる

npmjs にアップロードすると週間ダウンロード数が見れます。たくさんダウンロードされているとちょっとうれしいですね。

f:id:hecomi:20211212151814p:plain

対応パッケージでの具体的事例

対応したライブラリの中で具体例や、問題があったものに関しての事例を紹介します。

HLSLToolsForVisualStudioConfigGenerator

tips.hecomi.com

これは HLSL のシンタックスハイライトや補間をしてくれる HLSL Tools for Visual Studio 向けに Unity プロジェクト用の設定ファイルを生成してくれるツールです。こういうプロジェクト自体に影響を与えないツール、みたいなものは特に Assets 以下に入れないで単に Package として入れるのが良いですね。

uTouchInjection での XR 依存

tips.hecomi.com

uTouchInjection は Windows 向けにマルチタッチのエミュレーションを行うためのライブラリです。メインのユースケースとしては VR のデスクトップアプリを作ることで、以前は VR 向けのデモとして uDesktopDuplication と Oculus Integration の組み合わせを試すために、この 2 つは自分でダウンロードしてきてね、というスタンスで公開していました。

UPM 対応にあたっては、Unity 上で uDesktopDuplication と XR Interaction Toolkit を Package Manager から自分でインストールしてね、という形式にしました。これで Unity 上でセットアップが完結するようになり、Oculus への依存がなくなりました!ただし、XR Interaction Toolkit を使うためにちょっと手順が多くて大変ですが...。

github.com

uRaymarching のサンプルでのインクルードパス

これまでは Assets/uRaymarching/... みたいなパスを期待してシェーダーのインクルードを行ったサンプル(Assets からの絶対パス)となっており、UPM 対応するとこれが大きく変わってしまいます。ただ、uRaymarching ではシェーダ生成時にパスを解決する仕組みを入れているので、シェーダーを再生成すれば問題ありません。ただこれをユーザーにやらせるのは一旦エラーも吐いてしまいますし微妙なので、サンプル読み込み時に自動で AssetPosptocessor を使って再生成が走るようにしました。

public class GeneratorAssetImportProcessor : AssetPostprocessor 
{
    const string ext = ".asset";
    const string dir = "uRaymarching";

    static void OnPostprocessAllAssets(
        string[] importedAssets, 
        string[] deletedAssets, 
        string[] movedAssets, 
        string[] movedFromAssetPaths)
    {
        foreach (string str in importedAssets) {
            if (!str.EndsWith(ext) || !str.Contains(dir)) continue;

            var generator = AssetDatabase.LoadAssetAtPath<uShaderTemplate.Generator>(str);
            if (!generator) continue;

            var editor = Editor.CreateEditor(generator) as uShaderTemplate.GeneratorEditor;
            editor.Reconvert(); // 新しく読み込まれたジェネレータでシェーダの再生成を行う
        }
    }
}

このスクリプトをサンプルの Common に含め、Common を始めにインポートしてもらえていれば以降のサンプルは自動で変換が走るようになりうまく動くようになります。ただこれもまだ欠点があり...、Common → 別のサンプルと Package Manager で素早くポチポチと Import すると Common 内にある上記スクリプトコンパイルが終わる前に別のサンプルもインポートしてしまって再変換がされない...、というものです。。ここは良い回避策が思いつかなかったので、しばらくは質問されたら答える運用でカバーしようと思っています。。

f:id:hecomi:20211216222941p:plain

サンプル間の依存解決

上記のような問題を解決するために、サンプル間の依存解決が出来たら良いのになぁ...、などと思っています。上記のように、Common はいずれかのサンプルがインポートされたら一緒にインポートしてほしいユースケースや、まとめて 1 つのサンプルとするにはでかすぎるけれどサンプルとしては分けたい依存関係のあるものなど、こういったケースにどう対応すれば良いか悩ましい時があります。

悩みつつサンプルも多くなってしまって面倒になり、結局 Samples と名前をつけた 1 つのサンプルにしてしまったケースもあります。もしかすると小分けなど考えずにこれが大体のケースの正解なのかもしれない...と思ったりもしてきています。

f:id:hecomi:20211216223523p:plain

uDllExporter 断念

tips.hecomi.com

C# の Managed な DLL のビルドを簡単に行うツールを以前作ったのですが、2019.2 から Unity のディレクトリ構造が変わってしまいそのままだと動かずサポートも諦めました...。package.json には動作する Unity のバージョンを記述できるのですが、特定の Unity バージョンまでは動くけど以降は動きませんよ、みたいな指定は今の所出来ません。泣く泣くサポートできない場合にもこういう指定ができると良いなぁと思ったりしました。

組み合わせのサンプルが作りやすい

どれかの特定のライブラリのサンプル、というわけではなく複数のライブラリにまたがった例を作るときにも良かったです。例えば以前書いたこのデスクトップキャプチャした Texture2D を H264 にエンコードし、パケット分割して UDP で送信、リモート側でそれで結合、デコードして Texture2D として表示、というサンプルを見てみます。

tips.hecomi.com github.com

以前は、ゴチャッと Assets ディレクトリ以下に複数のプラグインが配置されていました。

f:id:hecomi:20211216225215p:plain

それがこれらの依存関係を UPM に追いやることで、Assets 以下にはこのプロジェクト固有のスクリプトのみが置かれるようになりました。

f:id:hecomi:20211216225432p:plain

プロジェクト的にとてもクリーンですね。ただデメリットも有り、以前の方式のほうが各ライブラリ含めて色々改変しながら結合動作を試したいときに便利で、各ライブラリの完成度が結構高くないと、あっちのパッケージを更新して、こっちのプロジェクトで Package Manager でアップデートして...みたいな行ったり来たり状態になり効率が落ちてしまいました。まぁこれは Unity に限った話ではないと思いますので、どんな場合でも用法用量を良く守って適材適所が大事ですね。。

おわりに

改めて 2020.3 LTS をベースに十数個のライブラリの UPM + スコープ付きレジストリ対応を行ったのですが、機械的に対応出来ないものもあり、結構時間がかかってしまいました。ただ、対応するとより多くの人が便利に使えるだろうと思いますし、今後の自分の開発も効率化出来ることが分かりました。

ちょっとこれまで制作物の宣伝記事みたいな感じになり長くなってしまいましたが、以上で終わりになります。ここまで読んでいただきありがとうございました。

明日 20 日目は @coposuke さんによる「Jump Flooding Algorithmについて」になります!

coposuke.hateblo.jp

*1:現在は Deprecated になってしまいました... https://github.com/NVIDIA/NvPipe