Unity + Azure SignalR でリアルタイム通信

# はじめに

Unity で SignalR を使いたい！と思っても意外とネット上の情報だったり書籍も少なく、情報を集めるのが困難でした。
今回は SinalR のサーバー側には説明せずに Unity 側のみを説明したいと思います。
（サーバー側は [Azure Functions + SignalR を使ってみる
](https://qiita.com/akiojin/items/ad8b61f63b129c40ed6d) で書きました。）

# 環境の用意

まず、Unity で SignalR を利用するには以下のライブラリが必要です。

- [Microsoft.AspNetCore.SignalR.Client](https://docs.microsoft.com/ja-jp/aspnet/core/signalr/dotnet-client?view=aspnetcore-5.0&tabs=visual-studio)

上記の .NET ライブラリは NuGet によって取得できますが、Unity から NuGet を利用しようとすると [NuGetForUnity](https://github.com/GlitchEnzo/NuGetForUnity) などで NuGet パッケージを取得する必要がありますが、このプラグインでは最新版パッケージのみしかインストール出来ないため、Unity で使える .NET のバージョンと合わない場合があるので NuGet.exe で直接インストールをしようと思います。
今回の SignalR のパッケージですが最新版の場合、依存関係のあるパッケージで Unity 側で対応していないエラーがありますのでできるだけ最新の『ver.3.1.19』を選択しています。

NuGet.exe はマイクロソフトのサイトからダウンロードします。
ダウンロードサイト：[NuGet.exe](https://dist.nuget.org/win-x86-commandline/latest/nuget.exe)

上記の NuGet.exe と以下の PowerShell のコードを同じディレクトリに配置します。
Unity の場合だと Assets/Plugins/NuGet/Packages などのディレクトリに配置すれば良いです。

```powershell
$targets = @("netstandard2.0", "netstandard2.1", "net462", "net46", "net45")

$outDir = ".\temp"
$pluginDir = ".\"

# すでに存在する dll は削除する
Remove-Item "$($pluginDir)\*.dll"

.\nuget install Microsoft.AspNetCore.SignalR.Client -Version 3.1.19 -OutputDirectory $outDir

$packages = Get-ChildItem -Path $outDir

foreach ($p in $packages) {
	# 優先度順にターゲットフレームワークを検索する
	foreach ($t in $targets) {
		if (Test-Path "$($p.FullName)\lib\$($t)") {
			$targetDir = "$($p.FullName)\lib\$($t)"
			break
		}
	}

	if ($targetDir -eq $null) {
		Write-Error "ファイルが見つかりませんでした。 (Package=$($p.FullName))"
		break;
	}

	if (Test-Path "$($targetDir)") {
		$dll = Get-ChildItem -Path "$($targetDir)\*.dll"
		if (!($dll -eq $null)) {
			foreach ($d in $dll) {
				Copy-Item -Path $d.FullName -Destination $pluginDir -Force
			}
		}
	}
}

# テンポラリディレクトリの削除
Remove-Item $outDir -Recurse
```

上記の PowerShell を実行することで Unity で利用可能な SignalR の NuGet パッケージがインストールされます。

# クライアント側のコード

以下に SignalR への接続用の Unity クライアントコードを記載します。
実際には非同期の処理をもう少しまともに書き換える必要がありますが…。

※それと今回は HubConnection は Azure Functions に接続する前提の作りとなっているため、デフォルトで呼び出される negotiate 関数などへのアクセスは関数キーを使用するようにしています。

```C#
public sealed class SignalR : IDisposable
{
	readonly HubConnection _connection;
	readonly CompositeDisposable _disposables = new CompositeDisposable();

	public bool IsConnected()
		=> _connection?.State == HubConnectionState.Connected

	public SignalR(string url, string key, string userId)
	{
		_connection = new HubConnectionBuilder()
			.WithUrl($"{url}/api", options => {
				// negotiate 関数を呼び出すために必要な関数キー
				// SignalR サーバー側で関数認証をしない場合は不要
				options.Headers["x-function-key"] = key;
				// SignalR サーバー側で SignalRConnectionInfo の UserId に設定するために
				// ユーザーを識別する ID をここで設定する。
				options.Headers["x-userid"] = userId;
			})
			.WithAutomaticReconnect()
			.ConfigureLoggin(logging => {
				// SignalR .NET ライブラリが出力する先を Unity のコンソールに出力するようにする。
				//logging.SetMinimumLevel(LogLevel.Debug);
				//logging.AddProvider(new UnityDebugConsoleLoggingProvider());
			})
			.Build();

		_connection.Closed += OnClosed;
		_connection.Reconnecting += OnReconnecting;
		_connection.Reconnected += OnReconnected;
	}

	Task OnClosed(Exception ex)
	{
		// SignalR サーバーとの接続が意図せず切断された場合に呼び出される
		return Task.CompletedTask;
	}
	Task OnReconnecting(Exception ex)
	{
		// SignalR サーバーとの接続を開始した場合に呼び出される
		return Task.CompletedTask;
	}
	Task OnReconnected(string connectionId)
	{
		// SignalR サーバーとの接続が完了した場合に呼び出される
		return Task.CompletedTask;
	}

	public void Connect()
	{
		// On() で登録するメソッドは StartAsync() の呼び出し前に行う必要があるため、
		// StartAsync() の呼び出しを別メソッドとして実装
		_connection.StartAsync();
	}

	public void Dispose()
	{
		if (!_disposables.IsDiposed) {
			_connection.Closed -= OnClosed;
			_connection.OnReconnecting -= OnReconnecting;
			_connection.OnReconnected -= OnReconnected;

			_connection.DisposeAsync().ConfigureAwait(false);

			_disposables.Dispose();
		}
	}

	public void On<T>(string methodName, Action<T> func)
	{
		// この On() で登録する処理は Connect() を呼び出す前に呼び出す必要がある
		_connection.On<T>(methodName result => {
			func(result);
		}).AddTo(_disposables);
	}
}
```

上記で SignalR サーバーへの接続が出来るようになっているので、実際に接続する場合は

```C#
SignalR _signalR = new SignalR("http://localhost:7071", "<key>", "0123456789");

_signalR.On("Hello_World", result => {
	// サーバーからの通知を受け取った場合に呼び出される
});

_signalR.Connect();
```

# おわりに

クライアント側のコードはさほど難しいわけではありませんが、Unity でインストールするライブラリだったりで躓く可能性が高いと思いますし、SignalR の negotiate 関数辺りもちょっと鬼門だったりしますので、参考程度になればと思います。

~~今後は、サーバー側も紹介したいと思います。~~（勢い余って書きました。[Azure Functions + SignalR を使ってみる](https://qiita.com/akiojin/items/ad8b61f63b129c40ed6d)）