📦

UPM経由でNuGetパッケージを扱えるUnityNuGetについて

2022/02/18に公開

はじめに

TL;DR

Unity で NuGet パッケージを扱うのに、UnityNuGet が良さそうに感じた。

発端

klakNDI という Unity で NDI 通信を扱うライブラリをインポートするとき、
Scoped Registory に見慣れない設定を発見しました。

https://github.com/keijiro/KlakNDI#how-to-install

klakNDI 自体も npm のレジストリからインポートするという珍しい構成ではありますが、
なんとSystem.Memoryを謎のレジストリからインポートしていたのです。
気になったので調べてみると、UnityNuGet というプロジェクトを発見し、その内容が興味深かったため備忘録を兼ねて筆を執りました。

内容と対象読者

本記事では UnityNuGet の概要紹介および簡単な使い方を解説します。
具体的なプロジェクト開発の内容を含みますが、UnityNuGet を中心に解説するので技術スタックについて十分な説明を含まないことをご了承ください。具体的には「SignalR」と「Azure Kinect」を扱います。
これらの技術スタックの知識があると尚良いですが、そうでなくても読める内容にしたつもりです。

本記事で想定する読者像は次の通りです。

  • Unity の基礎知識がある方
  • Unity で NuGet パッケージを扱うことに興味がある方
  • C#や.NET にまつわる基礎知識がある方

想定環境

本記事の内容は次のような環境で検証したものです。
ご参考になさってください。

  • Windows 10 Home
  • Unity 2020.3.20

Unity と Package Manager と NuGet と

UnityNuGet の具体的な説明の前に、
事前知識として Unity のパッケージ管理について復習をしましょう。

Unity パッケージインポートのしくみ

unitypackage

Unity のパッケージと聞いて、どのようなシステムを思い浮かべるでしょうか。
古くからあるのは.unitypackageファイルを D&D するなどしてインポートする仕組みでしょう。
この仕組みはインポート・エクスポートが容易であるため Unity 開発者であれば一度は使ったことがあるのではないでしょうか。

実際筆者も.unitypackageは今でも使用しているのですが、次の理由であまりスマートだとは思っていません。

  • 必要なパッケージをいちいちローカルに DL してインポートしなくてはいけない
  • パッケージ管理について意識することが多い
  • パッケージの依存関係を解決する仕組みがない
  • Assets/以下がどんどん増えていくし Git 管理に統一的な思想がない

ところで Unity 以外のパッケージ管理システムではどうでしょうか。
例えば Node.js の npm、Rust の cargo、Python の pip などを思い浮かべてみてください。
CLI によってインポートされたパッケージは、名前やバージョン情報がテキストとして記録され依存パッケージは lock されます。

ここで大事なのは次に示すようなことです。

  • パッケージの情報が文字ベースで保存され、ローカル環境に再現性がある
  • 依存パッケージは自動的に解決される

このような仕組みがあれば、開発者が神経を使って依存パッケージを揃える手間がなく、かつ安全なのです。
unitypackageではこのような仕組みがないため、あまりスマートではないと筆者は考えています。
unitypackage利用を否定するつもりはありませんが、もしチームで開発したり、OSS として公開を予定しているのであれば、もっとよい仕組みがあるのではないだろうか、と考えています。

Unity Package Manager

Unity Package Manager(以下 UPM)は前述したパッケージ管理問題を解決すべく導入されたパッケージマネージャです。
npm をベースにしているので Web 開発者の方であれば既視感のある見た目をしていたりします。

Unity のプロジェクトルートを基準に
Packages/manifest.jsonPackages/packages-lock.jsonが生成され、ここに必要パッケージが記録される仕組みです。
これによって Unity プロジェクト環境に再現性が担保されますしパッケージ同士の依存解決も自動的に行われます。
エディタに GUI も実装されており、ボタンをポチポチするだけでパッケージの Install/Remove が可能です。

img

また Git の URL 経由でカスタムパッケージをインポートできるのでライブラリ開発者は.unitypackageファイルを作成せずともプロジェクトを Git で公開することで配布でき、バージョン指定にも対応しています。

パッケージは必ず Assembly Definition が切られている決まりがあるので、大量にパッケージをインポートする場合、UPM を使ったときには 2 回目以降のビルド時間を短くできる可能性があります。

こう聞くと従来の.unitypackageと比べてモダンに感じるでしょう。
Git パッケージの依存解決にはまだ対応していませんが、有志によって開発されたパッケージを使用することで解決できるみたいです。

https://github.com/mob-sakai/GitDependencyResolverForUnity

Open UPM

Open UPM は UPM の仕組みを使ってパッケージをインポートできるサービスです。
Open UPM は npm パッケージとして CLI を提供しており、Node.js で扱うのと同じようにコマンドラインツールからパッケージをインポートできます。パッケージは Open UPM 独自のパッケージレジストリに存在しています。

例えば Open UPM から UniTask をインポートするには以下のコマンドを実行します。

openupm add com.cysharp.unitask

このコマンドを使うためには、事前に npm パッケージをグローバルインストールしておく必要があります。

https://www.npmjs.com/package/openupm-cli

自分はあまり Open UPM について詳しくないので詳しい方がいらっしゃったら
OpenUPM のメリットなどをコメントで教えていただけると嬉しいです。

NuGet パッケージの取り扱いのめんどくささ

パッケージ管理の仕組みについて復習が終わったことで、今度は NuGet パッケージとの連携の話をしましょう。

NuGet(読みはヌゲット?)とは.NET アプリ開発において利用するパッケージ管理システムです。

https://www.nuget.org/

.NET 環境で動作するパッケージはマネージド DLL として NuGet のレジストリにアップロードされ、dotnetコマンドやnugetコマンドによりローカルに展開されます。

Unity は基本的に.NET Standard 2.0 の規格にあった NuGet パッケージを使用できますので、もし .NET 系のライブラリが欲しいと思ったら NuGet の仕組みを使ってインポートします。

さてここで、みなさんはどのように NuGet パッケージを Unity にインポートしていますでしょうか。
ネットで検索すると、どうやら NuGetForUnity という有志で開発されたパッケージを経由してインポートすることが多いみたいです。

https://github.com/GlitchEnzo/NuGetForUnity

自分も以前はこちらを利用していましたが、あまり気に入っていませんでした。
記憶があまり定かではありませんが、たしかパッケージを丸ごと Assets 以下に展開することでインポートしていた気がします。パッケージによっては動作しないものもあったような......。

そんなこともあり、自分は NuGet CLI を使って別ディレクトリにコマンドライン経由で DL した DLL を、バッチファイルで Assets 以下にコピーすることでインポートしていました。
この方法であれば必要な DLL のみをバッチファイルを実行するだけでインポートできるので
気に入っていましたし、nuget.configapp.configを作成すれば Visual Studio からインポートできます。

例えば次のリポジトリでは、NuGet CLI を使って Azure Kinect Sensor SDK をインポートしています。

https://github.com/drumath2237/k4a-vfx

この方法だと、まず NuGet CLI がローカルにないと動作しないし環境構築のためのコストがちょっと大きいと感じていました。
同じ C#を使うプラットフォームなのだから、もっとこう、イイ感じになって欲しかったのです。

そう思っていたさなか、UnityNuGet に出会いました。

UnityNuGet を使って開発してみよう

概要:UnityNuGet とは

UnityNuGet は NuGet のパッケージレジストリです。
ただのレジストリではなく、UPM 経由でインポートできるのが特徴です。

https://github.com/xoofx/UnityNuGet

使い方は公式リポジトリを見れば一目瞭然ですが、https://unitynuget-registry.azurewebsites.netという URL を Scoped Registory として追加することで、一部の NuGet パッケージを UPM で扱えます。
manifest.jsonの一部の例を公式から引用します。

{
  "scopedRegistries": [
    {
      "name": "Unity NuGet",
      "url": "https://unitynuget-registry.azurewebsites.net",
      "scopes": ["org.nuget"]
    }
  ],
  "dependencies": {
    "org.nuget.scriban": "2.1.0"
  }
}

Dependency として追加された NuGet パッケージは.NET Standard 2.0 対応の DLL のみがインポートされます。
当然 Assets 以下ではなく通常の UPM パッケージと同じLibrary/PackageCache以下にインポートされます。

UnityNuGet でインポートできるパッケージは、こちらの JSON に列挙してある状況です。

https://github.com/xoofx/UnityNuGet/blob/master/registry.json

見てみると、gRPC 関係や FastEnum、Azure Kinect Sensor SDK や SignalR Service といったパッケージが見受けられます。
前述のとおり、Unity は.NET Standard 2.0 対応のパッケージのみ対応しているので、レジストリには現在手動で NuGet パッケージが加えられている状況みたいです。
PR を見ると続々と追加されているみたいですが、issue の中には開発者からの「忙しくて対応できていません」的な文言も見受けらますね。

使用例1:SiganlR Service を使ったリアルタイム通信

UnityNuGet の使用例1として Azure 上に作成した SignalR リソースと連携してリアルタイム通信のテストをしてみましょう。

なお Azure については本題ではないため詳細は割愛します。

Unity サイドの開発をする前に Azure 上で SignalR と Azure Functions のリソースを作成し、Azure Functions で以下のエンドポイントを作成しています。

negotiate
module.exports = async function (context, req, connectionInfo) {
  context.res.body = connectionInfo;
};
send
module.exports = async function (context, req) {
  context.bindings.signalRMessages = [
    {
      target: "event",
      arguments: ["Hellooooo"],
    },
  ];
};

これらのエンドポイントによって次のことを実装しています。

  • /negotiateから接続確立に必要な情報を取得
  • /sendに GET か POST をすると SignalR クライアントにHelloooooとメッセージ送信

これらの詳細については下記をご参照ください。

https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-signalr-service

それでは実際に Unity で通信してみましょう。
まずはMicrosoft.AspNetCore.SignalR.Clientという NuGet パッケージを
UnityNuGet からインポートします。
Packages/manifest.jsonを開き、次のように編集します。

manifest.json
{
  "scopedRegistries": [
    {
      "name": "Unity NuGet",
      "url": "https://unitynuget-registry.azurewebsites.net",
      "scopes": ["org.nuget"]
    }
  ],
  "dependencies": {
    "org.nuget.microsoft.aspnetcore.signalr.client": "1.0.0"
    // other packages ...
  }
}

これを見るとわかる通り、org.nuget.に続けて NuGet パッケージの文字をすべて小文字にした名前を記述します。
バージョンは、UnityNuGet のリポジトリにあるregistory.jsonを参照して記述します。今回は 1.0.0 でしたね。
こちらのバージョンが本家 NuGet パッケージとどのような関係があるかは、筆者は把握しておりません......。

インポートが完了したら適当な MonoBehaviour スクリプトを作成しましょう。
内容は好みですが、筆者は次のようにしました。

SignalR のスクリプト例
SignalRClientBehaviour.cs
using System;
using Microsoft.AspNetCore.SignalR.Client;
using UnityEngine;
using UnityEngine.Networking;

namespace UnityNuGetSignalRTest
{
    public class SignalRClientBehaviour : MonoBehaviour
    {
        private HubConnection _connection;

        private async void Awake()
        {
            // コネクションの確立
            _connection = new HubConnectionBuilder().WithUrl("<negotiateで使用するurlをここに>").Build();

            // 接続
            await _connection.StartAsync();

            // イベントを受信したらログに出力
            _connection.On<string>("event", Debug.Log);

            Debug.Log("connected!");
        }

        // GETリクエストを送って全てのクライアントにブロードキャスト
        public void SendSignalREventMessage()
        {
            if (_connection == null)
            {
                return;
            }

            UnityWebRequest.Get($"{signalRInfo.apiBaseUrl}/send").SendWebRequest();
        }

        private async void OnApplicationQuit()
        {
            if (_connection == null) return;

            await _connection.StopAsync();
            await _connection.DisposeAsync();
        }
    }
}

このスクリプトをシーンに配置し、
ボタンなどを使ってSendSignalREventMessageメソッドを実行すると、
デバッグログにHelloooooと出力されます。

サンプルを GitHub で公開しているので併せてご確認ください。

https://github.com/drumath2237/unity-nuget-signalr-sandbox

使用例2:Azure Kinect Sensor SDK

使用例2として Azure Kinect Sensor SDK を使って Azure Kinect からカメラ画像を取得してみましょう。
使用例1のときと同じようにmanifest.jsonに追記して NuGet パッケージをインポートします。

manifest.json
{
  "scopedRegistries": [
    {
      "name": "Unity NuGet",
      "url": "https://unitynuget-registry.azurewebsites.net",
      "scopes": ["org.nuget"]
    }
  ],
  "dependencies": {
    "org.nuget.microsoft.azure.kinect.sensor": "1.2.0"
    // other packages ...
  }
}

ここで注意なのですが、Azure Kinect Sensor SDK の場合、必要な DLL が揃っていない状態になります。
なぜかというと、現状だとネイティブプラグインがインポートできないからです。
Azure Kinect を動かすためにはk4a.dlldepthengine_2_0.dllという DLL が必要ですが、これらは.NET Standard 2.0 用のディレクトリに含まれておらずインポートできないのです。

この問題を解決するには手動で DLL を追加する必要があります。
例えば Azure Kinect SDK をインストールしたディレクトリにある 2 つの DLL を Unity のディレクトリにコピーするなどでしょうか。
(デフォルトだとC:\Program Files\Azure Kinect SDK v1.4.1\toolsにあります)。

ネイティブライブラリに対応していない問題は現在 issue に上がっており、将来的に解決される可能性があります。
https://github.com/xoofx/UnityNuGet/issues/90

それでは例のごとく適当なオブジェクトに以下のスクリプトをアタッチして実行してみます。
インスペクタで MeshRenderer を指定できるので、適当な Plane オブジェクトをアタッチしてみてください。

AzureKinect のスクリプト
AzureKinectDevice.cs
using System;
using System.Threading.Tasks;
using Microsoft.Azure.Kinect.Sensor;
using UnityEngine;

namespace AKDKUnityNuGet
{
    public class AzureKinectDevice : MonoBehaviour
    {
        private Device _kinect;

        [SerializeField] private MeshRenderer meshRenderer;

        private Texture2D _colorTexture;

        private Memory<byte> _rawColorData;

        private bool _isRunning = false;
        private bool _needsUpdate = false;


        private void Start()
        {
            if (meshRenderer == null)
            {
                return;
            }

            try
            {
                _kinect = Device.Open();

                _kinect.StartCameras(new DeviceConfiguration
                {
                    ColorFormat = ImageFormat.ColorBGRA32,
                    ColorResolution = ColorResolution.R1080p,
                    DepthMode = DepthMode.NFOV_2x2Binned,
                    SynchronizedImagesOnly = true,
                    CameraFPS = FPS.FPS30
                });
            }
            catch (Exception e)
            {
                Debug.LogError(e);
                throw;
            }

            _isRunning = true;

            var colorCalibration = _kinect.GetCalibration().ColorCameraCalibration;
            _colorTexture = new Texture2D(colorCalibration.ResolutionWidth, colorCalibration.ResolutionHeight,
                TextureFormat.BGRA32, false);

            meshRenderer.material.mainTexture = _colorTexture;

            _ = Capture();
        }

        private Task Capture()
        {
            return Task.Run(() =>
            {
                while (_isRunning)
                {
                    if (_needsUpdate)
                    {
                        continue;
                    }

                    using var capture = _kinect.GetCapture();

                    _rawColorData = capture.Color.Memory;

                    _needsUpdate = true;
                }
            });
        }

        private void Update()
        {
            if (_rawColorData.IsEmpty || !_needsUpdate)
            {
                return;
            }

            _colorTexture.LoadRawTextureData(_rawColorData.ToArray());
            _colorTexture.Apply();

            _needsUpdate = false;
        }

        private void OnApplicationQuit()
        {
            _isRunning = false;
            _kinect?.StopCameras();
            _kinect?.Dispose();
        }
    }
}

こちらもサンプルプロジェクトを用意しているので合わせてごらんください。

https://github.com/drumath2237/AKDK-Unity-NuGet

おわりに

まとめ

今回は UnityNuGet の紹介をしました。
自分自身、たまに Unity で NuGet を扱うことがあるので、このような仕組みがあるのは嬉しく、今後使っていきたいと感じました。
そんな UnityNuGet ですが、執筆時点ではあまり文献がなさそうに感じました。
とても良さそうなプロジェクトなので、いろんな人に知ってほしいです。

最後までご覧いただきありがとうございました。本記事が少しでもみなさまのお役に立てれば幸いです。

参考文献

https://github.com/xoofx/UnityNuGet

https://docs.unity3d.com/ja/2019.4/Manual/upm-ui.html

https://docs.microsoft.com/ja-jp/nuget/what-is-nuget

GitHubで編集を提案

Discussion