😁

UnityとC#で簡単にgRPCする in 2023

2023/10/19に公開

Turing株式会社のUXチームでインターンをしている東大4年の三輪です。

自動運転のUXを向上させるため、UXチームでは自動運転の「可視化」ソフトウェアの開発に取り組んでいます。

このソフトウェアはTuringが販売する車のディスプレイに搭載される予定です。AIが認識している世界をユーザにも見える形で示すことで、ユーザは自動運転AIの振る舞いをよりよく把握することができるようになります。

Turingの可視化ソフトウェアのイメージ画像
画面は開発中のものです

可視化ソフトウェアは、自動運転AIの出力を受け取り、解釈し、Unity上で描画します。この際、自動運転AIと可視化ソフトウェアの間の通信はgRPCで行うことになっています。しかし、Unity上でのgRPCはさまざまな事情でやや複雑な構成を取る必要があります。

この記事では、UnityでgRPCサーバを実装するステップを解説します。

gRPC

gRPCはRPC(Remote Procedure Call)のためのフレームワークの1つです。データのシリアライズやインターフェースには通常Protocol Buffersが利用され、通信には主にHTTP/2が利用されます。

HTTP/2とProtocol Buffersのようなモダンな技術を利用できることにより、開発面では高速な処理や型安全なAPI設計が利点として挙げられます。

gRPCの詳細な情報は公式ドキュメント等を参照してください。

https://grpc.io/

gRPCはさまざまな言語向けの実装が公式に用意されており、Unityでの開発で一般的に利用されるC#もサポートされています。

UnityでのgRPC

ということは余裕なのでは……?と思ってしまうのですが、実はUnityでやるとなると難しい部分があります。

現状、UnityでのgRPCに推奨されている実装はgrpc-dotnetと呼ばれているものです。

https://github.com/grpc/grpc-dotnet

grpc-netは図の緑色の部分です。

grpc-dotnetに関連するモジュールの依存関係を示した画像
https://grpc.io/blog/grpc-on-dotnetcore/

かつては画像の白四角で示されたGrpc.Coreと呼ばれる実装が主に用いられてきましたが、さまざまな事情から、こちらはすでにメンテナンスモードに入っており、2024年の10月でサポートが終了されることが告知されています。(ただし、サポートの終了はこれまですでに2回延期されています)

また、Windowsのみで開発していれば問題ありませんが、M1, M2などのチップで動いているmacOSの場合Grpc.Coreはアーキテクチャの都合で動作しなくなってしまいます。

このため、今からUnityでgRPCする上ではgrpc-dotnetが明らかに推奨される選択です。公式ドキュメントでもC#でのgRPCにはgrpc-dotnetが強く推奨されています。

https://grpc.io/blog/grpc-csharp-future/

一方で、grpc-dotnetには大きな課題があります。grpc-dotnetの大きな特徴は、ネイティブライブラリに依存したGrpc.CoreでのHTTP/2を見直し、.NETでサポートされたピュアなC#実装のHTTP/2を使っていることです。しかし、Unityではこのサポートがありません。つまり、grpc-dotnetはそもそも使えなかったのです。

こうした一連の経緯は株式会社Synamonのテックブログがとても詳しいので、ぜひご覧ください。

https://synamon.hatenablog.com/entry/grpc-dotnet-unity

状況はつい最近まで厳しかったのですが、2023年の7月になってhttps://github.com/Cysharp/YetAnotherHttpHandlerというHTTP/2実装がCysharp社からリリースされました。これを用いることでgrpc-dotnetに欠けていたHTTP/2を補うことが出来そうです。

neue cc - Unity用のHTTP/2(gRPC) Client、YetAnotherHttpHandlerを公開しました

そこで、grpc-dotnetにYetAnotherHttpHandlerを組み合わせる形で、UnityでのgRPCを実現します。

依存パッケージの準備

NuGetForUnityの導入

YetAnotherHttpHandlerを使うためにいくつかのunitypackageを追加する必要がありますが、この方法ではバイナリを直接管理する形になってしまい使い勝手が悪いため、NuGetForUnityを利用します。

https://github.com/GlitchEnzo/NuGetForUnity

NuGetForUnityはNuGetで管理されたパッケージをUnity上で追加するために利用できるマネージャです。UnityPMで以下の依存を追加します。

"dependencies": {
    "com.github-glitchenzo.nugetforunity": "https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity",
    "com.cysharp.yetanotherhttphandler": "https://github.com/Cysharp/YetAnotherHttpHandler.git?path=src/YetAnotherHttpHandler#v0.1.0",
}

NuGetForUnityの導入に成功したら、NuGetのmanifests.configを以下の通りに変更します。

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Google.Protobuf" version="3.24.3" manuallyInstalled="true" />
  <package id="Grpc.Core.Api" version="2.57.0" />
  <package id="Grpc.Net.Client" version="2.57.0" manuallyInstalled="true" />
  <package id="Grpc.Net.Common" version="2.57.0" />
  <package id="Grpc.Tools" version="2.58.0" manuallyInstalled="true" />
  <package id="Microsoft.Extensions.Logging.Abstractions" version="7.0.1" />
  <package id="System.Diagnostics.DiagnosticSource" version="7.0.2" />
  <package id="System.IO.Pipelines" version="7.0.0" manuallyInstalled="true" />
  <package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" />
</packages>

YetAnotherHttpHandlerの導入

続いてYetAnotherHttpHandlerを導入します。必要な依存関係はNuGetForUnityで導入済みなので、これだけで問題なく動きます。

"dependencies": {
    "com.cysharp.yetanotherhttphandler": "https://github.com/Cysharp/YetAnotherHttpHandler.git?path=src/YetAnotherHttpHandler#v0.1.0",
}

NuGetForUnityとCI

CIでGitHubビルドを実施している場合、NuGetForUnityのCI対応が必要になる場合があります。NuGetForUnityはかなりCI対応されており、dotnet環境を用意した上でNuGetForUnity.Cliをインストールすれば概ね問題なく動きます。以下はGitHub Actionsでdotnetの環境構築とNuGetForUnity.Cliのインストールを行い、依存パッケージの復元を実行する例です。

# dotnetを指定
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
  dotnet-version: 7.0.x

# NuGetForUnityをインストール
- name: Setup NuGetForUnity
run: |
  dotnet tool install --global NuGetForUnity.Cli
  mkdir -p ${{ env.UNITY_PATH }}/Assets/Packages
  // 依存パッケージを復元する
  nugetforunity restore ${{ env.UNITY_PATH }}

ただし、NuGetForUnityはCIでは2023年10月現在v2のNuGet APIしか使えないため、v3のAPIを呼び出そうとすると失敗します。現在β版ではv3に対応しているので、次以降のリリースで対応されると考えられますが、一旦は<add key="NuGet" value="http://www.nuget.org/api/v2/" />を指定することでこの問題を解決するのが良いでしょう。

NuGet.configのpackageSourcesを以下のように修正します。

<packageSources>
   <add key="nuget.org" value="http://www.nuget.org/api/v2/" />
  <!-- TODO: NuGetForUnity.Cli does not support v3 API now. When it got supported, switch package source.
	<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3"/> 
  -->
</packageSources>

CI対応は基本的に以上で問題なく動きます。

gRPCのコード生成

Protocol BuffersのファイルからC#のコード生成を行う必要があります。非常にシンプルですが、以下のようなprotoファイルを用意します。

syntax = "proto3";
option csharp_namespace = "Sample.Grpc";

package sample_service;

service SampleService {
  rpc Sample(SampleRequest)
      returns (stream SampleResponse);
}

message SampleRequest {
  // no fields now
}

message SampleResponse {
  string message = 1;
}

生成にはGrpc.Toolsを利用します。これは上でNuGetForUnityで管理しているので、直接exeを呼ぶことでスムーズに生成が可能です。

# Grpc.Toolsのパスを指定
$grpcToolsPath = "UnityProject\Packages\Grpc.Tools.2.58.0\tools\windows_x86"
# 出力先を指定
$outPath = "UnityProject\Assets\Project\GrpcGenerated"
& "$grpcToolsPath\protoc.exe" --csharp_out $outPath --grpc_out $outPath `
  --plugin=protoc-gen-grpc="$grpcToolsPath\grpc_csharp_plugin.exe" `
  --proto_path=proto sample.proto

gRPCの実装

では、最後にgRPC通信を行うクライアントを用意しましょう。以下のクライアントに必要なコールバックを指定することで、gRPCがUnityで実行できるようになります。

using System;
using System.Net.Http;
using System.Threading;
using UnityEngine;

using Cysharp.Net.Http;
using Cysharp.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;

using Sample.Grpc;

public class SampleStreamClient
{
    static readonly string Address = "https://localhost:8080";

    public delegate void ResponseReceivedHandler(SampleResponse response);
    public event ResponseReceivedHandler OnResponseReceived;
    public event Action<Exception> OnError;

    CancellationTokenSource cancellationTokenSource = null;

    public void StartStream()
    {
        if (cancellationTokenSource != null)
        {
            Debug.LogWarning("Stream is already running.");
            return;
        }
        cancellationTokenSource = new CancellationTokenSource();
        ReceiveStreamAsync(cancellationTokenSource.Token).Forget();
    }

    async UniTask ReceiveStreamAsync(CancellationToken cancellationToken)
    {
        try
        {
            using var httpHandler = new YetAnotherHttpHandler() { SkipCertificateVerification = true };
            using var httpClient = new HttpClient(httpHandler);

            using GrpcChannel channel = GrpcChannel.ForAddress(Address, new GrpcChannelOptions() { HttpHandler = httpHandler });
            var gRPCClient = new SampleService.SampleServiceClient(channel);

            var sampleRequest = new SampleRequest();
            AsyncServerStreamingCall<SampleResponse> request = gRPCClient.Sample(sampleRequest);

            while (await request.ResponseStream.MoveNext(cancellationToken))
            {
                var response = request.ResponseStream.Current;

                await UniTask.SwitchToMainThread();
                OnResponseReceived?.Invoke(response);
            }
        }
        catch (RpcException rpcEx)
        {
            Debug.LogError($"gRPC error: {rpcEx.Status.Detail}");
            OnError?.Invoke(rpcEx);
        }
        catch (Exception ex)
        {
            Debug.LogError($"Stream error: {ex.Message}");
            OnError?.Invoke(ex);
        }
        finally
        {
            cancellationTokenSource.Dispose();
            cancellationTokenSource = null;        }
    }

    public void StopStream()
    {
        if (cancellationTokenSource != null)
        {
            cancellationTokenSource.Cancel();
        }
    }
}

grpc-dotnetとYetAnotherHttpHandlerを利用することにより、gRPCのクライアントを非常にシンプルに実装することができました。

おわりに

この記事ではUnityでgRPCを行う方法を示しました。ここしばらくのあいだ状況がカオスになっていたUnityでのgRPCですが、先人たちの知恵によって最近はずいぶん簡単にできるようになっています。今回の構成であれば、今後Unityが公式にHTTP/2をサポートした際に乗り換えるのも簡単にできそうです。

TuringのUXチームではUnityを利用した可視化ソフトウェアの開発を進めています。美しく明快な可視化により、自動運転AIの思考を描き出すため、UnityやAndroidのエンジニアを積極的に募集しています。ぜひ採用情報もご覧ください。

https://herp.careers/v1/turing/RjGiUFSJJCLk

Tech Blog - Turing

Discussion