💻

メルクストーリアのリアルタイム通信開発におけるコード共有について

2021/12/14に公開

この記事は「Cacalia Studio アドベントカレンダー 2021 エンジニア版」の14日目の記事です。

はじめに

メルクストーリア」 エンジニアの 岸本 と申します。

メルクストーリアでは、2020~2021年の取り組みのひとつとして、リアルタイム通信技術を採用した新規コンテンツの開発に取り組んできました。

今年の3月にギルド内コミュニケーション機能のリアルタイム化を行い、今月はじめにギルドメンバーとリアルタイムで協力プレイのできるレイドクエストをリリースしました。

去年のアドベントカレンダーで基本的なアーキテクチャーのご紹介をさせていただき、今年はCEDEC 2021 で導入事例のひとつとしてご紹介をさせていただきました。

今回は、クライアントもサーバーも C# だからできる コード共有 について、メルクストーリアでの活用事例をご紹介させていただきます。

過去資料

採用技術の基本的な部分は、過去資料も併せてご参考にしていただければ幸いです!

前提

メルクストーリアでは、クライアントを Unity (C#)、API サーバーを Ruby on Rails (Ruby) で開発しています。
加えて本記事で扱うリアルタイム通信部分では、Unity (C#) と非常に相性のいい MagicOnion を採用し、サーバーサイドを .NET (C#) で開発しており、gRPC を活用してリアルタイム通信を実現しています。

採用技術

クライアント

リアルタイム通信環境 (サービスディスカバリー / リアルタイムサーバー)

  • .NET 5 (C# 9.0)
  • MagicOnion (gRPC)
  • MessagePack for C#
  • UniTask

ネットワーク構成

今回のリアルタイム通信環境では、普段使用している Ruby on Rails の API サーバー (メインサーバー) とは別で、Kubernetesを用いた専用の環境を構築しています。

要となるリアルタイムサーバーは、Kubernetes 内のAgonesで管理されており、ロードバランサーなどを経由することなく、直接インターネットに公開しているサーバーとなるため、クライアントは、リアルタイムサーバーの接続先を知るために、一旦サービスディスカバリーという API サーバーを経由してからリアルタイムサーバーに接続する仕組みになっています。

コード共有

さて、ここから本題となる コード共有 についてご紹介させていただきます。

クライアントもサーバーも同じ C# で開発するひとつのメリットとして、クライアントとサーバーで コード共有 できる点が挙げられます。

もちろん C# のバージョンに加えて、Unity や .NET のフレームワークに依存した部分まではコード共有できませんが、そこさえ気を付ければ、実用的な範囲でコード共有が可能となっていて、以下のように Unity のプロジェクトからも .NET のプロジェクトからも同じコードを参照することができます。

【事例1】定義のコード共有

今回は MagicOnion を用いた開発になるので、必然的に以下のように gRPC の通信に使用するメソッド定義やデータ型定義をクライアントとサーバーでコード共有することになります。

public interface IChatService : IService<IChatService>
{
    UnaryResult<GetChatRoomResponse> GetChatRoomAsync(GetChatRoomRequest request);
}

public interface IChatStreamingHub : IStreamingHub<IChatStreamingHub, IChatStreamingHubReceiver>
{
    Task<SendMessageResponse> SendMessageAsync(SendMessageRequest request);
}

public interface IChatStreamingHubReceiver
{
    void OnReceiveMessage(ReceiveMessageData data);
}
[MessagePackObject(true)]
public sealed class ReceiveMessageData
{
    public string Name { get; set; }
    public string Message { get; set; }
}

クライアントとサーバーで別々の言語を扱う場合と比べ、通信で使うインターフェイスの部分になるため、一番わかりやすく恩恵を感じられるかと思います。

【事例2】通信クライアントのコード共有

MagicOnion でサーバーサイドと通信するためには、基本的には以下のようなコードだけで通信ができるようになっています。

var channel = new Channel("localhost", 12345, new SslCredentials());
var client = MagicOnionClient.Create<IChatService>(channel);

var request = new GetChatRoomRequest();
var response = await client.GetChatRoomAsync(request);

ただし、実際の運用を意識した場合、本番環境や開発環境などで異なる接続先をいい感じに切り替えられるようにしたいですし、gRPC の Channel に ChannelOption を設定したかったり、ヘッダーや CallOptions を設定したりもしたいですし、エラーハンドリングなどなども考えると、もう少し複雑な実装になるかと思います。

また今回は、要となるリアルタイムサーバーと接続する前に、サービスディスカバリーを1度経由する必要もあるため、

  1. gRPC の Channel を作成して、ヘッダーなどを設定して、サービスディスカバリーに接続する。
  2. サービスディスカバリーと通信してリアルタイムサーバーのアドレスを取得する。
  3. 取得したアドレスで gRPC の Channel を作成して、ヘッダーなどを設定して、リアルタイムサーバーに接続する。

という具合に、実際にリアルタイムサーバーと通信をはじめるまでに少し手間のかかる接続手順なっています。

メルクストーリアでは、このあたりの手間のかかる処理は通信クライアント内部で吸収してしまい、アプリケーションのレイヤーでは、以下のようになるべく簡単に扱えるようにしています。

// new しただけで通信クライアントを使えるようにしたい。
var client = new ChatClient();

// リアルタイムサーバーからのブロードキャストイベントを購読したい。
client.OnReceiveMessageAsAsyncEnumerable().ForEachAsync(x => Debug.Log(x));

// メソッド叩くだけでリアルタイムサーバーと通信したい。
await client.SendMessageAsync("Hello!!");

// 使い終わったら破棄したい。
client.Dispose();

ここまでの内容であれば、コード共有せずともクライアント側で通信クライアントを作り込めば良さそうですが、負荷試験のことも考えるとコード共有のメリットが出てきます。

メルクストーリアのリアルタイム通信環境の負荷試験では、負荷試験クライアントも .NET (C#) のコンソールアプリケーションで実装しています。

はじめは負荷試験クライアント側でもゲームクライアント側と同じく通信クライアント部分を個別に実装していましたが、開発中に通信まわりを修正した際に、ゲームクライアント側も負荷試験クライアント側もそれぞれで同じような修正を行う必要があったため、通信クライアント自体をゲームクライアントからも負荷試験クライアントからも同じものが使えるように、コード共有できるようにしています。

Unity では Grpc.Core の Channel を使いたい、.NET では Grpc.Net.Client の GrpcChannel を使いたい、などのそれぞれのクライアントに依存するような部分は、クライアント側から注入できるようにしておく必要はありますが、大部分をコード共有することができています。

【事例3】ゲームロジックのコード共有

計算などの一部のゲームロジックについて、クライアントでもサーバーでも実装したいシーンがあるかと思います。

今月リリースしたリアルタイムレイドクエストでは、レイドクエストに必要なコアロジックに加えて、モンスターの状態などをクライアントでもサーバーでも同じように管理をする必要があったため、コード共有を活用しました。

状態管理をしているロジック自体をコード共有して、サーバーでデータをエクスポートしてクライアントでインポートすれば、サーバーでもクライアントでも同じ状態になうよう設計しています。

おわりに

Unity (C#) + .NET (C#) による開発では、クライアント・サーバーの言語統一のメリットとして、コード共有 を活かすことができます。

特に今回使用させていただいた MagicOnion や UniTask では、コード共有との親和性も高く、メルクストーリアのリアルタイム通信環境のような部分的な利用でも、コード共有の恩恵を十分に受けて、効率的に開発を進めることができたと思います。

特にリアルタイムレイドクエストの開発では、いかに上手くクライアントとサーバーでデータを同期するかが課題のひとつでもあったため、コード共有の旨味を活かすことで、限られら開発リソースの中で無事に開発することができたのではないかと思います。

明日の後編では、リアルタイムレイドクエストについて、全体的なところをザックリお話したいと考えていますので、引き続きお楽しみいただければ幸いです!

GitHubで編集を提案
Happy Elements

Discussion