UPM経由でNuGetパッケージを扱えるUnityNuGetについて
はじめに
TL;DR
Unity で NuGet パッケージを扱うのに、UnityNuGet が良さそうに感じた。
発端
klakNDI という Unity で NDI 通信を扱うライブラリをインポートするとき、
Scoped Registory に見慣れない設定を発見しました。
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.json
とPackages/packages-lock.json
が生成され、ここに必要パッケージが記録される仕組みです。
これによって Unity プロジェクト環境に再現性が担保されますしパッケージ同士の依存解決も自動的に行われます。
エディタに GUI も実装されており、ボタンをポチポチするだけでパッケージの Install/Remove が可能です。
また Git の URL 経由でカスタムパッケージをインポートできるのでライブラリ開発者は.unitypackage
ファイルを作成せずともプロジェクトを Git で公開することで配布でき、バージョン指定にも対応しています。
パッケージは必ず Assembly Definition が切られている決まりがあるので、大量にパッケージをインポートする場合、UPM を使ったときには 2 回目以降のビルド時間を短くできる可能性があります。
こう聞くと従来の.unitypackage
と比べてモダンに感じるでしょう。
Git パッケージの依存解決にはまだ対応していませんが、有志によって開発されたパッケージを使用することで解決できるみたいです。
Open UPM
Open UPM は UPM の仕組みを使ってパッケージをインポートできるサービスです。
Open UPM は npm パッケージとして CLI を提供しており、Node.js で扱うのと同じようにコマンドラインツールからパッケージをインポートできます。パッケージは Open UPM 独自のパッケージレジストリに存在しています。
例えば Open UPM から UniTask をインポートするには以下のコマンドを実行します。
openupm add com.cysharp.unitask
このコマンドを使うためには、事前に npm パッケージをグローバルインストールしておく必要があります。
自分はあまり Open UPM について詳しくないので詳しい方がいらっしゃったら
OpenUPM のメリットなどをコメントで教えていただけると嬉しいです。
NuGet パッケージの取り扱いのめんどくささ
パッケージ管理の仕組みについて復習が終わったことで、今度は NuGet パッケージとの連携の話をしましょう。
NuGet(読みはヌゲット?)とは.NET アプリ開発において利用するパッケージ管理システムです。
.NET 環境で動作するパッケージはマネージド DLL として NuGet のレジストリにアップロードされ、dotnet
コマンドやnuget
コマンドによりローカルに展開されます。
Unity は基本的に.NET Standard 2.0 の規格にあった NuGet パッケージを使用できますので、もし .NET 系のライブラリが欲しいと思ったら NuGet の仕組みを使ってインポートします。
さてここで、みなさんはどのように NuGet パッケージを Unity にインポートしていますでしょうか。
ネットで検索すると、どうやら NuGetForUnity という有志で開発されたパッケージを経由してインポートすることが多いみたいです。
自分も以前はこちらを利用していましたが、あまり気に入っていませんでした。
記憶があまり定かではありませんが、たしかパッケージを丸ごと Assets 以下に展開することでインポートしていた気がします。パッケージによっては動作しないものもあったような......。
そんなこともあり、自分は NuGet CLI を使って別ディレクトリにコマンドライン経由で DL した DLL を、バッチファイルで Assets 以下にコピーすることでインポートしていました。
この方法であれば必要な DLL のみをバッチファイルを実行するだけでインポートできるので
気に入っていましたし、nuget.config
、app.config
を作成すれば Visual Studio からインポートできます。
例えば次のリポジトリでは、NuGet CLI を使って Azure Kinect Sensor SDK をインポートしています。
この方法だと、まず NuGet CLI がローカルにないと動作しないし環境構築のためのコストがちょっと大きいと感じていました。
同じ C#を使うプラットフォームなのだから、もっとこう、イイ感じになって欲しかったのです。
そう思っていたさなか、UnityNuGet に出会いました。
UnityNuGet を使って開発してみよう
概要:UnityNuGet とは
UnityNuGet は NuGet のパッケージレジストリです。
ただのレジストリではなく、UPM 経由でインポートできるのが特徴です。
使い方は公式リポジトリを見れば一目瞭然ですが、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 に列挙してある状況です。
見てみると、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 で以下のエンドポイントを作成しています。
module.exports = async function (context, req, connectionInfo) {
context.res.body = connectionInfo;
};
module.exports = async function (context, req) {
context.bindings.signalRMessages = [
{
target: "event",
arguments: ["Hellooooo"],
},
];
};
これらのエンドポイントによって次のことを実装しています。
-
/negotiate
から接続確立に必要な情報を取得 -
/send
に GET か POST をすると SignalR クライアントにHellooooo
とメッセージ送信
これらの詳細については下記をご参照ください。
それでは実際に Unity で通信してみましょう。
まずはMicrosoft.AspNetCore.SignalR.Client
という NuGet パッケージを
UnityNuGet からインポートします。
Packages/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 のスクリプト例
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 で公開しているので併せてご確認ください。
使用例2:Azure Kinect Sensor SDK
使用例2として Azure Kinect Sensor SDK を使って Azure Kinect からカメラ画像を取得してみましょう。
使用例1のときと同じようにmanifest.json
に追記して NuGet パッケージをインポートします。
{
"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.dll
とdepthengine_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 に上がっており、将来的に解決される可能性があります。
それでは例のごとく適当なオブジェクトに以下のスクリプトをアタッチして実行してみます。
インスペクタで MeshRenderer を指定できるので、適当な Plane オブジェクトをアタッチしてみてください。
AzureKinect のスクリプト
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();
}
}
}
こちらもサンプルプロジェクトを用意しているので合わせてごらんください。
おわりに
まとめ
今回は UnityNuGet の紹介をしました。
自分自身、たまに Unity で NuGet を扱うことがあるので、このような仕組みがあるのは嬉しく、今後使っていきたいと感じました。
そんな UnityNuGet ですが、執筆時点ではあまり文献がなさそうに感じました。
とても良さそうなプロジェクトなので、いろんな人に知ってほしいです。
最後までご覧いただきありがとうございました。本記事が少しでもみなさまのお役に立てれば幸いです。
参考文献
Discussion