Unity + MagicOnion + ASP.NET Core Web API でリアルタイム通信とAPI通信を両立する方法
はじめに
GoogleCloudを使ってネット対戦ができるゲームを作ろうと思っているので、下準備としてUnity + MagicOnion + ASP.NET Core Web API(以下、Web API)の構成のプロジェクトを作りました。
そんなわけで上記構成の構築手順を解説します。
今回紹介する構成にすることで、ネット対戦中の処理はMagicOnionを利用したリアルタイム通信、それ以外のログイン等の処理はWeb APIによるAPI通信、といった使い分けができるようになります。
なぜMagicOnionオンリーではなく、WebAPIも併用する構成にしたのか
MagicOnionはAPIっぽい使い方はできるので、実はMagicOnion単体でも機能自体は問題なく実装可能です。
ですが以下の理由からWeb APIを組み合わせる構成を選びました。
- MagicOnionはUnityとサーバー間のコネクションを維持するステートフルである都合上、スケールしにくい
- 将来的にAPIの処理をCloud Runに移行することも考えているので、リアルタイム通信とAPI通信が分かれていた方が都合がよい
MagicOnionの導入
基本的には公式ドキュメントに記載されている手順通りです。
テンプレートを使うのが一番手っ取り早いです。
とはいえ既存プロジェクトにMagicOnionを導入したいという方もいるでしょう。
その場合もドキュメント通りに進めていけば大体問題ないのですが、いくつかつまづきポイントがあるので補足します。
GrpcChannelではなくGrpcChannelx
ドキュメントにはチャンネル作成時に以下のような処理を行うよう書いてあります。
var channel = GrpcChannelx.ForAddress("http://localhost:5210");
クラス名に注目してください。
GrpcChannel
ではなくGrpcChannelx
です。後ろにx
がついているのが正しいです。
前者はGrpc.Net.Client.GrpcChannel
で後者はMagicOnion.GrpcChannelx
です。
エディタのコード入力補完任せだとついうっかりGrpcChannel
にしてしまいがちなので注意しましょう。
Kestrelの設定を行う
MagicOnionはHttp2で通信を行います。
必ず通信プロトコルはHttp2を使うように設定をしましょう。
具体的にはappsettings.json
に以下の設定を追加します。
ここの設定は公式のテンプレートの設定を参考にしています。
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
ちなみにKestrel
とはASP.NET Coreが標準で用意してあるWebサーバーです。
WebAPIの追加と統合
サーバー側の修正
ここでは以下のようなフォルダ構成になっているものとします。(修正対象のもののみ記載)
Server/
├── Controllers/
│ └── SampleApiController.cs
├── appsettings.json
├── Program.cs
Shared/
└── Models/
└── Api/
├── SampleApiRequest.cs
└── SampleApiResponse.cs
新規ファイル
新規追加するファイルは以下のようにしてください。
[Serializable]
public class SampleApiRequest
{
public string Message;
}
[Serializable]
public class SampleApiResponse
{
public string ReceiveMessage;
public string ServerTimeStamp;
}
[ApiController]
[Route("api/[controller]")]
public class SampleApiController : ControllerBase
{
[HttpPost("echo")]
public ActionResult<SampleApiResponse> Echo([FromBody] SampleApiRequest request)
{
var response = new SampleApiResponse
{
ReceiveMessage = request.Message + " received!!",
ServerTimeStamp = DateTime.UtcNow.ToString("o"),
};
return response;
}
}
既存ファイル
appsettings.json
はKestrel
の設定を以下のように修正してください。
ちなみにEndpoints
にあるMagicOnion
やWebApi
は任意の名前でかまいません。
"Kestrel": {
"Endpoints": {
"MagicOnion": {
"Url": "http://0.0.0.0:5000",
"Protocols": "Http2"
},
"WebApi": {
"Url": "http://0.0.0.0:5001",
"Protocols": "Http1"
}
}
}
Program.cs
は以下の処理を入れてください。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.IncludeFields = true;
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
builder.Services.AddMagicOnion();
var app = builder.Build();
app.UseRouting();
app.MapControllers();
app.MapMagicOnionService();
app.Run();
Unity側の修正
ここでは以下のようなフォルダ構成になっているものとします。(修正対象のもののみ記載)
Assets/
├── Scripts/
│ ├── Shared/
│ │ └── Models/
│ │ └── Api/
│ │ ├── SampleApiRequest.cs
│ │ └── SampleApiResponse.cs
│ └── Network/
│ └── Api/
│ └── SampleApiService.cs
新規ファイル
新規追加するファイルは以下のようにしてください。
public sealed class SampleApiService
{
private static SampleApiService _instance;
public static SampleApiService Instance => _instance ??= new SampleApiService();
public async Task<SampleApiResponse> EchoAsync(string message)
{
var request = new SampleApiRequest { Message = message };
var url = "http://localhost:5001/api/SampleApi/echo";
var json = JsonUtility.ToJson(request);
var webRequest = new UnityWebRequest(url, "POST");
webRequest.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
webRequest.downloadHandler = new DownloadHandlerBuffer();
webRequest.SetRequestHeader("Content-Type", "application/json");
await webRequest.SendWebRequest();
if (webRequest.result == UnityWebRequest.Result.Success)
{
var responseJson = webRequest.downloadHandler.text;
return JsonUtility.FromJson<SampleApiResponse>(responseJson);
}
else
{
throw new Exception("API呼び出し失敗: " + webRequest.error);
}
}
}
動作確認
Unity側のMagicOnionの接続先をappsettings.json
の変更に合わせて以下のように修正します。
GrpcChannelx.ForAddress("http://localhost:5000");
続いてUnity側で新規追加したSampleApiService
を呼び出す処理を適当な場所に追加します。
適当なシーンのStart
であったり、ボタンを押した時の挙動とかで大丈夫です。
var response = await SampleApiService.Instance.EchoAsync("テストメッセージ");
Debug.Log($"受信メッセージ: {response.ReceiveMessage}, サーバー時刻: {response.ServerTimeStamp}");
最後に上記処理を実行してみましょう。
MagicOnionによるリアルタイム通信、Web ApiによるAPI通信のいずれも正常に処理が行われていれば成功です!
MagicOnionとWebAPI共存におけるつまづきポイント
HTTP2とHTTP1の共存問題
MagicOnionとAPIを統合する際、Kestrel
の設定がHttp1AndHttp2
だとMagicOnion側が接続時にHTTP1を使ってしまい、接続エラーが発生することがあります。
この場合、この記事で紹介しているappsettings.json
のように、MagicOnionはHttp2専用エンドポイント、WebAPIはHttp1専用と分けることで解決します。
"Kestrel": {
"Endpoints": {
"MagicOnion": {
"Url": "http://0.0.0.0:5000",
"Protocols": "Http2"
},
"WebApi": {
"Url": "http://0.0.0.0:5001",
"Protocols": "Http1"
}
}
}
Unityとサーバーの共通モデルの落とし穴
この記事ではSampleApiRequest
とSampleApiResponse
のシリアライズ・デシリアライズをJsonUtility
を使っています。
JsonUtility
はpublic
なフィールドしか対象ではないのですが、なんとサーバー側はデフォルトではpublic
なフィールドをJsonのシリアライズ・デシリアライズの対象外にしていることがあるようなのです。
さらにサーバー側から返ってくるJsonは、フィールド名の最初が小文字になってしまいます。
そのためSampleApiResponse.cs
でいくら大文字始まりのフィールド名を定義していても、サーバー側からは小文字始まりのフィールド名で返ってくるのでうまく変換してくれません。
「JsonUtility
を使わない」「Jsonにするやつは命名規則を変える」等の対応でももちろん解決できます。
しかし、Program.cs
に以下の処理を追加することでも解決できます。
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.IncludeFields = true;
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
おわりに
「サクッと4~5時間くらいでできるやろ」と思ってたのですが、想像以上に苦戦して2~3日かかってしまいました。
せっかくなので苦労した箇所の説明をはさみつつ、今回紹介した構成の実現方法を解説する記事にしました。
基本的に環境構築系はChatGPTに相談しつつやっているのですが、全く見当違いの修正方針を出してくることも多々あったので苦戦しました。
やはりちょっと複雑な環境構築になってくると、AIを使う側の知識もまだまだ必要だなと実感しました。おかげさまで今回の対応でより知識が身に付きました。
Discussion