見出し画像

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

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

はじめに

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

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

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

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

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

過去資料

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

● MagicOnionから始めるリアルタイム通信 (前編)
● MagicOnionから始めるリアルタイム通信 (後編)
● 運用中タイトルでも怖くない! 『メルクストーリア』におけるハイパフォーマンス・ローコストなリアルタイム通信技術の導入事例

前提

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

採用技術

クライアント

● Unity 2018.4 (C# 7.3)
MagicOnion (gRPC)
MessagePack for C#
UniTask

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

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

ネットワーク構成

画像1

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

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

コード共有

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

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

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

画像2

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

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

csharp
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);
}
csharp
[MessagePackObject(true)]
public sealed class ReceiveMessageData
{
   public string Name { get; set; }
   public string Message { get; set; }
}

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

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

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

csharp
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 を作成して、ヘッダーなどを設定
  して、リアルタイムサーバーに接続する。

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

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

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

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

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

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

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

画像3

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

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

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

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

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

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

画像4

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

おわりに

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

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

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

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

--

Happy Elements カカリアスタジオでは
いっしょに「熱狂的に愛されるコンテンツ」をつくっていただけるメンバーを大募集中です!

もし弊社にご興味持っていただけましたら、是非一度
下記採用サイトをご覧ください!