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

Cysharpから(主に)Unity用のHTTP/2, gRPC, MagicOnion用の通信ネットワーククライアントを公開しました。実装者は週刊.NET情報配信WeekRef.NETを運営している@mayukiさんです。

何故これが必要なのかの背景情報としては、Synamon’s Engineer blog - Unityでもgrpc-dotnetを使ったgRPCがしたい が詳しいのですが、まず、.NETには2つのgRPC実装があります。googleが提供してきたgRPCのネイティブバインディングのGrpc.Core(C-Core)と、Microsoftが提供しているPure C#実装のgrpc-dotnet。現在.NETのgRPCはサーバーもクライアントも完全にPure C#実装のほうに寄っていて、MagicOnionもサーバーはPure C#実装のものを使っています。

しかしクライアントに関しては、諸事情によりUnityでは動かない(TLS関連の問題など)ため、ずっとC-Coreを推奨してきました。更に、Unity用のビルドは元々experimentalだったうえに、とっくにメンテナンスモードに入り、そしてついに今年5月にサポート期限も切れて完全に宜しくない気配が漂っていました。また、古いx64ビルドなので最近のMac(M1, M2チップ)では動かないためUnity Editorで使うのにも難儀するといった問題も出てきていました。

と、いうわけで、CysharpではUnityで使うgRPCを推奨してきたということもあり、Unityで問題なく使えるgRPC実装としてYetAnotherHttpHandlerを開発・リリースしました。HttpClientの通信レイヤーであるHttpHandlerを差し替えるという形で実装してあるので、ほとんど通常の .NET でのgRPCと同様に扱えます。

内部実装としてはPure Rust実装のHTTP/2ライブラリhyperとPure RustのTLSライブラリrustlsを基盤として作ったネイティブライブラリに対して、Cysharp/csbindgenで生成したC#バインディングを通して通信する形になっています。

余談

YetAnotherHttpHandlerはgRPCやMagicOnionに限らず、Unityで自由に使える HTTP/2 Clientなので、アセットダウンロードの高速化にHTTP/2を用いる、といったような使い道も考えられます。既にモバイルゲームでも幾つかのタイトルでHTTP/2でアセットダウンロードしているタイトルは確認できていまして、例えばセガさんはCEDEC2021 ダウンロード時間を大幅減!~大量のアセットをさばく高速な実装と運用事例の共有~のような発表もされています。ネイティブプラグインを自前でビルドして持ち込むというのはだいぶ敷居が高い話でしたが、YetAnotherHttpHandlerを入れるだけでいいなら、だいぶやれるんじゃないか感も出てくるんじゃないでしょうか……?

AlterNats は 公式の NATS .NET Client v2 に引き継がれました

NATSのサードパーティー(alternative)クライアントであったAlterNatsは、公式に引き取られてNATS.NET V2となりました。v2の詳細に関してはNATS公式からのブログNATS .NET Client v2 Alpha Released with Core NATS Supportを参照ください。

NATS community members started to take note, and develop client libraries for NATS based on modern .NET APIs. One notable client library that emerged was the AlterNats library by Cysharp, which includes a fully asynchronous API, leverages Span , and supports client-side WebSockets from browsers in Blazor . NATS maintainers and AlterNats maintainers agreed that AlterNats would make a great starting point for NATS.Client v2!

NATSに関してはAlterNatsリリース時の記事 AlterNats - ハイパフォーマンスな.NET PubSubクライアントと、その実装に見る.NET 6時代のSocketプログラミング最適化のTips、或いはMagicOnionを絡めたメタバース構築のアーキテクチャについてに色々書きましたが、Cloud Native Computing Foundation配下のPubSubミドルウェアで、RedisなどでのPubSubに比べるとパフォーマンスを始めとして多くのメリットがあります。

ただしこういうものはサーバー実装も重要ですがクライアント実装も重要であり、そして当時のNATSの公式クライアント(v1)は正直酷かった!せっかくの素晴らしいミドルウェアが.NETでは活かされない、また、RedisでのPubSubには不満があり、そもそも.NETでのベストなPubSubのソリューションがないことに危機意識を感じていたので、独自に実装を進めたのがAlterNatsでした。

ただし、枯れたプロトコルならまだしも、進化が早いミドルウェアのクライアントが乱立しているのは決して良いことでもないでしょう。新機能への追随速度やメンテナンスの保証という点でも、サードパーティクライアントとして進んでいくよりも、公式に統合されることのほうが絶対に良いはずです。

というわけで今回の流れは大変ポジティブなことだし、野良実装にとって最高の道を辿れたんじゃないかと思っています。私自身は実装から一歩引きますが、使っていく上で気になるところがあれば積極的にPR上げていくつもりではあります。

なお、NATSに関しては来月CEDEC 2023でのセッションメタバースプラットフォーム「INSPIX WORLD」はPHPもC++もまとめてC#に統一!~MagicOnionが支えるバックエンド最適化手法~で触れる、かもしれません、多分。というわけでぜひ聞きに来てください……!

メタバース関連では、今年の5月にTGS VRなどを手掛けているambrさんのテックブログにてVRメタバースのリアルタイム通信サーバーの技術にMagicOnionとNATSを選んだ話という紹介もしていただいていました。

OSSとメンテナンスの引き継ぎ

権限の移管は何度か経験があって

は完全に手放しています。ほか、MagicOnionはCysharp名義に移ったうえで、現在の開発リードは私ではありません。また、最近ではMessagePack for C#はMessagePack-CSharp Organizationに移していて共同のOwner権限になっています。

どうしても常に100%の力を一つのOSSに注ぐことはできないので、本来はうまく移管していけるのが良いわけですが、いつもうまくできるわけじゃなくて、Utf8Jsonなんかはうまく移管できないままarchivedにしてしまっています。

やっぱ出した当時は自分が手綱を握っていたいという気持ちがとても強いわけですが、関心が徐々に薄れていくタイミングと他の人に渡せるタイミングがうまく噛み合わないと、死蔵になってしまうというところがあり、まぁ、難しいです。これだけやっていても上手くできないなあ、と……。

今回のは大変良い経験だったので、作ってメンテナンスを続ける、そしてその先についても考えてやっていきたいところですね。

ともあれ、良い事例を一つ作れた&素晴らしいライブラリをC#に一つ持ち込むことができたということで、とても気分がよいですです。

Microsoft MVP for Developer Technologies(C#)を再々々々々々々々々々々々受賞しました

13回目です!一年ごとに再審査での更新で、変わらずC#の最前線に立てています。

活動の中心は引き続きOSSですが、github/Cysharpでのスター数は変わらず他を圧倒していると思いますし、毎年の新規の公開数の勢いも変わらずで新しいアイディアを出し続けています。

今年はcsbindgenを起点にしてRustを活用してC#の活用幅をより広げていくことを狙っています。先日公開したMagicPhysXの他にも色々計画がって、かなり面白いインパクトが出せるんじゃないかと思っています。

MessagePack for C#もMessagePack-CSharp/MessagePack-CSharpと、organization名義に移したことで(変わらず私はOwnerなので権限を手放したわけではありません)より中立的に発展させていきます。直近ではSource Generator対応が予定されています(preview版を公開中)。

というわけで、これはもう満場一致でC#に貢献しているということでいいんじゃないでしょうかね……?

ここ最近は登壇していなかったのですが、去年はC#11 による世界最速バイナリシリアライザー「MemoryPack」の作り方というセッションをしました。その流れということで、今年の8月にCEDEC 2023にてモダンハイパフォーマンスC# 2023 Edition、それと共同講演でメタバースプラットフォーム「INSPIX WORLD」はPHPもC++もまとめてC#に統一!~MagicOnionが支えるバックエンド最適化手法~という2つの登壇予定があるので、ぜひ見に来てください。

MagicPhysX - .NET用のクロスプラットフォーム物理エンジン

MagicPhysXというライブラリを新しく公開しました!.NETで物理エンジンを動かすというもので、その名の通り、NVIDIA PhysX のC#バインディングとなっています。

使い道としては

  • GUIアプリケーションの3D部分
  • 自作ゲームエンジンへの物理エンジン組み込み
  • ディープラーニングのためのシミュレーション
  • リアルタイム通信におけるサーバーサイド物理

といったことが考えられます。

.NET用のPhysXバインディングは他にも存在しますが、C++/CLIでバインディングを生成している都合上Windowsでしか動かせなかったり、バージョンが最新ではない4.xベースだったりしますが、MagicPhysXは最新のPhysX 5ベースで、かつ、Windows, MacOS, Linuxの全てで動きます!(win-x64, osx-x64, osx-arm64, linux-x64, linux-arm64)。これはバインディングの作り方としてクロスプラットフォームコンパイルに強いRustと、Cysharp/csbindgenによってC#のバインディングの自動生成をしているからです。

先にアーキテクチャの話をしましょう。MagicPhysXはEmbarkStudiosによるphysx-rsをビルド元に使っています。

EmbarkStudiosはEA DICEでFrostbiteゲームエンジン(Battlefield)を作っていた人たちが独立して立ち上げたスタジオで、Rustによるゲームエンジンを作成中です。また、その過程で生まれたRustのライブラリをOSSとして積極的に公開しています。一覧はEmbark Studios Open Sourceにあります。必見!

PhysXのライブラリはC++で出来ていて、他の言語で使うことは考慮されていません。そのために他の言語に持ち込むためには、C++上で別言語で使うためのブリッジ部分を作った上で、バインディングを用意するという二度手間が必要になってきます。それはRustであっても例外ではありません。また、二度手間というだけではなく、PhysXのソースコードはかなり大きいため、その作業量も膨大です。

以前にcsbindgen - C#のためのネイティブコード呼び出し自動生成、或いはC#からのネイティブコード呼び出しの現代的手法についてで紹介しましたが、SWIGなどのC++からの自動生成、Rustであればcxxautocxxのような自動化プロジェクトも存在しますが、C++そのものの複雑さからいっても、求めるものを全自動で出力するのは難しかったりします。

physx-rsではAn unholy fusion of Rust and C++ in physx-rs (Stockholm Rust Meetup, October 2019)というセッションでPhysXをRustに持ち込むための手段の候補、実際に採用した手段についての解説があります。最終的に採用された手段について端的に言うと、PhysXに特化してコード解析してC APIを生成する独自ジェネレーターを用意した、といったところでしょうか。そしてつまり、physx-rsには他言語でもバインディング手段として使えるPhysXのC APIを作ってくれたということにもなります!

更にcsbindgenには、rsファイル内のextern "C"の関数からC#を自動生成する機能が備わっているので、Rustを経由することでC++のPhysXをC#に持ち込めるというビルドパイプラインとなりました。

そういう成り立ちであるため、MagicPhysXのAPIはPhysXのAPIそのものになっています。

using MagicPhysX; // for enable Extension Methods.
using static MagicPhysX.NativeMethods; // recommend to use C API.

// create foundation(allocator, logging, etc...)
var foundation = physx_create_foundation();

// create physics system
var physics = physx_create_physics(foundation);

// create physics scene settings
var sceneDesc = PxSceneDesc_new(PxPhysics_getTolerancesScale(physics));

// you can create PhysX primitive(PxVec3, etc...) by C# struct
sceneDesc.gravity = new PxVec3 { x = 0.0f, y = -9.81f, z = 0.0f };

var dispatcher = phys_PxDefaultCpuDispatcherCreate(1, null, PxDefaultCpuDispatcherWaitForWorkMode.WaitForWork, 0);
sceneDesc.cpuDispatcher = (PxCpuDispatcher*)dispatcher;
sceneDesc.filterShader = get_default_simulation_filter_shader();

// create physics scene
var scene = physics->CreateSceneMut(&sceneDesc);

var material = physics->CreateMaterialMut(0.5f, 0.5f, 0.6f);

// create plane and add to scene
var plane = PxPlane_new_1(0.0f, 1.0f, 0.0f, 0.0f);
var groundPlane = physics->PhysPxCreatePlane(&plane, material);
scene->AddActorMut((PxActor*)groundPlane, null);

// create sphere and add to scene
var sphereGeo = PxSphereGeometry_new(10.0f);
var vec3 = new PxVec3 { x = 0.0f, y = 40.0f, z = 100.0f };
var transform = PxTransform_new_1(&vec3);
var identity = PxTransform_new_2(PxIDENTITY.PxIdentity);
var sphere = physics->PhysPxCreateDynamic(&transform, (PxGeometry*)&sphereGeo, material, 10.0f, &identity);
PxRigidBody_setAngularDamping_mut((PxRigidBody*)sphere, 0.5f);
scene->AddActorMut((PxActor*)sphere, null);

// simulate scene
for (int i = 0; i < 200; i++)
{
    // 30fps update
    scene->SimulateMut(1.0f / 30.0f, null, null, 0, true);
    uint error = 0;
    scene->FetchResultsMut(true, &error);

    // output to console(frame-count: position-y)
    var pose = PxRigidActor_getGlobalPose((PxRigidActor*)sphere);
    Console.WriteLine($"{i:000}: {pose.p.y}");
}

// release resources
PxScene_release_mut(scene);
PxDefaultCpuDispatcher_release_mut(dispatcher);
PxPhysics_release_mut(physics);

つまり、そのままでは決して扱いやすくはないです。部分的に動かすだけではなく、本格的にアプリケーションを作るなら、ある程度C#に沿った高レベルなフレームワークを用意する必要があるでしょう。MagicPhysX内ではそうしたサンプルを用意しています。それによって上のコードはこのぐらいシンプルになります。

using MagicPhysX.Toolkit;
using System.Numerics;

unsafe
{
    using var physics = new PhysicsSystem(enablePvd: false);
    using var scene = physics.CreateScene();

    var material = physics.CreateMaterial(0.5f, 0.5f, 0.6f);

    var plane = scene.AddStaticPlane(0.0f, 1.0f, 0.0f, 0.0f, new Vector3(0, 0, 0), Quaternion.Identity, material);
    var sphere = scene.AddDynamicSphere(1.0f, new Vector3(0.0f, 10.0f, 0.0f), Quaternion.Identity, 10.0f, material);

    for (var i = 0; i < 200; i++)
    {
        scene.Update(1.0f / 30.0f);

        var position = sphere.transform.position;
        Console.WriteLine($"{i:D2} : x={position.X:F6}, y={position.Y:F6}, z={position.Z:F6}");
    }
}

ただしあくまでサンプルなので、参考にしてもらいつつも、必要な部分は自分で作ってもらう必要があります。

Unityのようなエディターがないと可視化されてなくて物理エンジンが正しい挙動になっているのか確認できない、ということがありますが、PhysXにはPhysX Visual Debuggerというツールが用意されていて、MagicPhysXでも設定することでこれと連動させることが可能です。

Dedicated Server

CysharpではMagicOnionLogicLooperといったサーバーサイドでゲームのロジックを動かすためのライブラリを開発しています。その路線から行って物理エンジンが必要なゲームでさえも通常の .NET サーバーで動かしたいという欲求が出てくるのは至極当然でしょう……(?)

UEやUnityのDedicated Serverの構成だとヘッドレスなUE/Unityアプリケーションをサーバー用ビルドしてホスティングすることになりますが、サーバー用のフレームワークではないので、あまり作りやすいとは言えないんですよね。通常用サーバー向けのライブラリとの互換性、ライフサイクルの違い、ランタイムとしてのパフォーマンスの低さ、などなど。

というわけで、MagicOnionのようなサーバー向けフレームワークを使ったほうがいいのですが、物理エンジンだけはどうにもならない。今までは……?

と、言いたいのですが、まずちゃんとしっかり言っておきたいのですが、現実的には少々(かなり)難しいでしょう!コライダーどう持ってくるの?とかAPIが違う(Unityの物理エンジンはPhysXですが、API的に1:1の写しではないので細かいところに差異がある)のでそもそも挙動を合わせられないし、でもこういう構成ならサーバーだけじゃなくクライアントでも動かしたい、そもそもそうじゃないとデバッガビリティが違いすぎる。

と、ようするに、もしゲーム自体にある程度、物理エンジンに寄せた挙動が必要なら、「物理エンジン大統一」が必須だと。MagicPhysXは残念ながらそうではありません。実のところ当初はそれを目指していました、Unityとほぼ同一挙動でほぼ同一APIになるのでシームレスに持ち込むことができるライブラリなのだ、と。しかし現状はそうではないということは留意してください。また、その当初予定である互換APIを作り込む予定もありません。

まとめ

このライブラリ、かなり迷走したプロジェクトでもあって、そもそも最初はBullet Physicsを採用する予定でした。ライブラリ名が先に決めてあってMagicBulletってカッコイイじゃん、みたいな。その後にJolt Physicsを使おうとして、これもバインディングをある程度作って動く状態にしたのですが、「物理エンジン大統一」のためにPhysXにすべきだろうな、という流れで最終的にPhysXを使って作ることにしました。

形になって良かったというのはありますが(そしてcsbindgenの実用性!)、「物理エンジン大統一」を果たせなかったのは少々残念ではあります。最初の完成予想図ではもっともっと革命的なもののはずだったのですが……!

とはいえ、PhysX 5をクロスプラットフォームで.NETに持ち込んだということだけでも十分に難易度が高く新しいことだと思っているので、試す機会があれば、是非触って見ください。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(.NET)
April 2011
|
July 2025

X:@neuecc GitHub:neuecc

Archive