
はじめに
株式会社WinTicketでUnity&XRエンジニアをしています。加田です。私たちスポーツ映像テック事業部では、新たなスポーツ映像の創出を目指し、日々開発をしています。現在、データの可視化にはゲームエンジンのUnityを用いて、リアルタイムCG合成をしています。
本記事では、Unity製描画アプリにおいて、複数のスポーツに対応するための通信プロトコルとしてProtocol Buffersを導入した経緯と実装について共有します。
この記事で学べること
- Protocol Buffersの基本概念と、生のbyte配列通信からProtocol Buffersへ移行するメリット
- UnityプロジェクトにProtocol Buffersを導入する具体的な手順(NuGetForUnityの利用、パッケージインストール、protoファイルのコンパイル方法)
- Unity(C#)でUDP通信経由でProtocol Buffersメッセージを受信・パースする実装方法
- 異なる言語間(C#、Go、Python)での通信プロトコルを統一する方法
- 型安全性を確保しながら、パフォーマンスとメンテナンス性を向上させる実践的なアプローチ
想定読者
- Unityでネットワーク通信(特にUDP通信)を実装しているエンジニア
- 複数の言語間で通信プロトコルを統一したいエンジニア
- 型安全性やパフォーマンス向上に興味のあるエンジニア
- バイト配列を直接扱う通信実装の課題を感じているエンジニア
- Protocol BuffersのUnityへの導入手順を知りたいエンジニア
目次
- Protocol Buffersを使った通信とは
Protocol Buffersを使った通信とは
課題:生のbyte配列を使った通信の限界
Protocol Buffersを導入する前はUDP通信でbyte配列を直接やり取りしていました(参考)。受信側となるUnityではC#で実装されていますが、送信側のアプリはGoやPythonで書かれており、関係者間でバイトアライメントを決めて実装をしていました。byte配列でのやり取りは要素の順番を間違えるとパースエラーが発生してしまうため、実装には細心の注意が必要でした。また、変数の追加・削除も容易に行うことができず、どの時点の仕様で実装されているかが分からないというバージョニングの問題もありました。
Protocol Buffersの導入
これらの課題を解決するため、Protocol Buffersを導入し、型安全で効率的な通信を実現しました。
Protocol Buffersは、Googleが開発したバイナリシリアライズ形式です。
.protoファイルでデータ構造を定義し、そこから各言語(C#、Go、Pythonなど)のコードを自動生成できます。これにより、異なる言語間での通信でも型安全性を保ちながら、JSONと比較して高速でコンパクトなデータ転送が可能になります。Unityプロジェクトへの導入
ここからはUnityプロジェクトへProtocol Buffersを導入する方法を紹介します。
パッケージのインストール
まず、NuGetForUnityをプロジェクトにインストールします。
Package Managerを開き、Add package from git URL...で下記のURLを入力してAddボタンでインストールします。https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity
NuGetForUnityがインストールできたら、ツールバーの
NuGet→Manage NuGet Packagesを開き、下記2つのパッケージをインストールします。
gRPC通信自体は行いませんが、protocコンパイラを利用するために、Grpc.Toolsパッケージを導入しています。- Google.Protobuf
- Grpc.Tools


Protoファイルの用意
今回は送信用に簡単なProtoファイルを作成しました。
syntax = "proto3"; // Pingメッセージ: UDP通信で使用するテスト用メッセージ message Ping { // メッセージ内容 string message = 1; // 送信時刻(Unixタイムスタンプ) int64 sent_unix = 2; }この.protoファイルを送信側と受信側で共有し、各言語にコンパイルすることでProtocol Buffersでの通信を実現します。
Unity(C#)でのProtoファイルコンパイル方法
今回はAssetsの直下に
Protoというフォルダを作り、その中に.protoファイルを格納しました。Assets/ └── Proto/ └── ping.proto同じフォルダで下記のコマンドを実行すると、protoファイルをコンパイルした
Ping.csが生成されます。
コンパイルに必要なツールはProjectRoot/Packages/Grpc.Tools.x.xx.x/toolsにOSごとに分かれて配置されています。
パスはバージョンごとに異なるため、2.76.0以外のバージョンがインストールされている場合は読み替えてください。Windows(x64)
..\..\Packages\Grpc.Tools.2.76.0\tools\windows_x64\protoc.exe ^ ./ping.proto ^ --csharp_out=./ ^ --grpc_out=./ ^ --plugin=protoc-gen-grpc=..\..\Packages\Grpc.Tools.2.76.0\tools\windows_x64\grpc_csharp_plugin.exeMac
../../Packages/Grpc.Tools.2.76.0/tools/macosx_x64/protoc \ ./ping.proto \ --csharp_out=./ \ --grpc_out=./ \ --plugin=protoc-gen-grpc=../../Packages/Grpc.Tools.2.76.0/tools/macosx_x64/grpc_csharp_plugin通信で受け取ったbyte配列をパースする
Ping.csが生成され、コンパイルが通ったら受信処理を実装します。
以前の記事で作成したUDP受信クラスを流用します。using System; using System.Net; using System.Net.Sockets; using R3; /// /// ネットワーク経由でデータを受け取るクラス /// public class NetworkDataReceiver : IDisposable { UdpClient udpClient = null; Subject<byte[]> subject = new(); /// /// データの受信イベント(メインスレッドではないので注意) /// public Observable<byte[]> OnReceivedBytes => this.subject; public int Port { get; private set; } /// /// コンストラクタ /// public NetworkDataReceiver(int port) { this.Port = port; this.udpClient = new UdpClient(port); this.udpClient.BeginReceive(OnReceived, this.udpClient); } private void OnReceived(System.IAsyncResult result) { UdpClient getUdp = (UdpClient)result.AsyncState; IPEndPoint ipEnd = null; var getByte = getUdp.EndReceive(result, ref ipEnd); this.subject.OnNext(getByte); getUdp.BeginReceive(OnReceived, getUdp); } public void Dispose() { this.udpClient.Close(); } }using UnityEngine; using R3; public class ProtoCheck : MonoBehaviour { NetworkDataReceiver dataReceiver; void Start() { this.dataReceiver = new NetworkDataReceiver(port:50051); this.dataReceiver .OnReceivedBytes .ObserveOnMainThread() // UIへの適用など、Unityの関数を使う場合にはメインスレッドで処理をする .Subscribe(bytes => { // Protobufメッセージをデシリアライズ Ping ping = Ping.Parser.ParseFrom(bytes); Debug.Log($"メッセージ: {ping.Message}, 送信時刻(Unix): {ping.SentUnix}"); }).AddTo(this); } void OnDestroy() { this.dataReceiver.Dispose(); } }受信したbyte配列を
ParseFromメソッドに渡すと、対象のオブジェクトに変換されます。
これにより、型安全な状態で通信が可能となります。
protoファイルにコメントを記述しておくと、C#側でもハイライト表示されるため便利です。
Protoファイルの更新により通信内容が変わったときは、C#にコンパイルした時点で変更されたことを検知できます。
Protoファイルが更新されたら、C#に自動変換する仕組みを構築すれば、さらに安全にやり取りすることが可能となります。まとめ
本記事では、Unityアプリにおいて通信プロトコルとしてProtocol Buffersを導入した経緯と実装について紹介しました。
Protocol Buffersの導入により、型安全性とパフォーマンスを向上させ、複数スポーツのデータを統一的に扱える通信プロトコルの構築ができました。今後も改善を続けていきます。
Appendix
Protocol Buffersを検証するためにPythonで簡易的な送信サーバーを作成しましたので、そのコードも載せておきます。
あらかじめprotocコマンドを使ってping.protoをping_pb2に変換しています。import socket import time import ping_pb2 HOST = "127.0.0.1" PORT = 50051 def main(): # UDP ソケット sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: while True: # Protobuf メッセージ作成 msg = ping_pb2.Ping( message="Send UDP Message", sent_unix=int(time.time()), ) # シリアライズ payload = msg.SerializeToString() # UDP は sendto 1 回 = 1 datagram sent = sock.sendto(payload, (HOST, PORT)) print(f"sent {sent} bytes") # 1秒待機 time.sleep(1) except KeyboardInterrupt: print("\n送信を停止しました") finally: sock.close() if __name__ == "__main__": main()参考リンク
