本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーは谷脇真琴さんで、テーマは「Perlでの今風のゲームサーバ開発とテスト」です。
MySQLを使ったコードのテストの構築のしかたや、普通のWebサービスには見られないマスタデータに対するテストなど、Perlによるゲームサーバの開発によって得られた知見を紹介します。
本稿のサンプルコードは、WEB+DB PRESS Vol.102のサポートサイトから入手できます。
ゲームサーバのAPI開発
ゲームと一口に言っても、家庭用据え置きゲーム機や携帯ゲーム機、スマートフォンなど、さまざまなプラットフォームにゲームがリリースされています。筆者はその中でも、スマートフォン向けのゲームアプリの開発チームにいます。いわゆる、ソーシャルゲームアプリと言われているものです。
現代のゲームアプリでは、アプリがデータを保存するためや、ほかのユーザーのデータを取得するために、専用のAPIサーバを開発することが多いです。筆者の開発しているサービスでは、PerlでJSONをレスポンスとして返すRESTful APIサーバを実装しています。
ゲームサーバと言っても基本的な構成はWebサービスと同じです。ですが、2つの大きな違いがあります。一つは、ゲームアプリとの通信頻度をできるだけ少なくするため、一度にまとめてデータを返す必要があり、そのために複雑なデータ構造の管理が求められることです。もう一つは、ユーザーデータも、アイテム定義などを含むマスタデータも、ともに膨大なサイズになることです。
以上の違いを踏まえて、ゲームアプリのサーバ開発特有の問題を、Perlを用いてどのように解決してきたかを述べていきます。
IDLを用いたAPI定義の共有と活用
APIサーバの返す複雑なレスポンスをクライアントアプリと確実に共有するために、筆者の作っているサービスではIDL(Interface Description Language、インタフェース定義言語)を用いています。
ここでは、広く使われているIDLと、筆者が用いている独自IDLのBaalについて紹介します。
IDLとは何か
IDLは、ソフトウェアの中での何らかの境界、たとえばプロセス間の通信や、ライブラリとそれを使うソフトウェアの間でのAPI定義などに使われます。Webと違い特定のクライアントからしかアクセスされないゲームサーバでは、APIの定義をIDLで記述し、クライアントと定義を共有すると、効率的に開発を進められます。
IDLにはさまざまな種類があり、それぞれが用途ごとに用いられます。Web APIに使われるIDLとして、次のようなものがあります。
- JSON Schema、JSON Hyper-Schema
JSONのデータ構造を、JSONで定義するためのIDL。レスポンスがJSON形式のRESTful APIに使われる
- Swagger
RESTful APIのサービス定義などをYAML(YAML Ain't Markup Language)で記述するIDL。エディタやモックサーバなどのツールが充実している
- Protocol Buffers
バイナリ表現のシリアライズ形式で、スキーマ定義を行うのに独自のIDLが用いられている。gRPCというプロトコルに用いられている
- GraphQL
クライアント側がレスポンス形式をコントロールできるAPI問い合わせ言語で、IDLとも言える。GitHub APIに用いられている
一般的にIDLは、スキーマ定義とサービス定義に分けられます。スキーマ定義では、データフォーマットの構造を定義します。サービス定義では、URLエンドポイントを決定し、リクエストとしてどんなデータ構造で受けて、レスポンスとしてどんなデータ構造で返すかを決めます。たとえばJSON Schemaでは、JSONSchema(core)がスキーマ定義、JSON Hyper-Schemaがサービス定義になります。スキーマ定義とサービス定義で分かれていることで、あるスキーマ定義を複数のサービス定義から用いることができます。
Baal──独自のIDL
筆者のプロジェクトでは、BaalというIDLを用いています。新規開発時にJSON Schemaなどがまだ一般的ではなかったため、独自に作ったプロプライエタリなフォーマットです。BaalをパースするモジュールであるBaal::ParserがCPANにあり、これをPerlアプリケーションのテストなどに利用しています。
現在、CPANモジュールには前述した広く使われているIDL用のモジュールが存在するため、Baalを新たに採用するメリットはもうありませんが、ここでは、プロジェクト開発の中でIDLのユースケースやメリットを説明するために、まずBaalの解説を行います。
Baalは、スキーマ定義とサービス定義の両方ができ、スキーマ定義の継承とmix-inもできます。
継承とmix-inの機能があることは重要です。たとえば、ソーシャルゲームにおける友達一覧と友達の詳細機能を考えてみます。前者のリスト表示用のデータは、表示領域の関係上、名前やアイコン程度の少ない情報しか必要としません。一方、詳細表示用のデータでは、より掘り下げた情報を要求されます。
この機能に使われるスキーマ定義に関して、2つのやり方が考えられます。
- リスト表示用のデータと詳細表示用のスキーマ定義を分けずに同じ定義を使う
- 区別して別々の定義を使う
現在は区別して別々に定義するやり方を取っています。同じ定義を使うやり方では、使われないデータを送信して、無駄な通信量や処理時間が発生するからです。しかし、完全に区別すると、データ構造が違うことから、ビューのメソッドを使いまわせないなどの問題が発生します。
そこで、Baalを用いたスキーマ定義では、簡易表示用と詳細表示用のデータの関連を、継承によって表現します。リスト表示用のデータを継承して詳細表示用のデータを定義し、追加で必要なプロパティを定義する形です。これにより、たとえばリスト表示用のデータが機能追加で増えたとしても、詳細表示用のデータも自動で追従できます。
また、APIの中にはページャ表示のように、複数のAPIで同じUIを用いて表示させるものがあります。このとき、ページャに必要な総ページ数や現在のページ番号といったデータ構造が複数のスキーマ定義に現れることがあります。このような場合にmix-inを用いると、部品化されたスキーマ定義を個々のスキーマ定義に混ぜ込めます。
Baalについての詳細は、「IDL「Baal」について」をご覧ください。
Baalの活用例
Baal形式でAPI定義を書けば、複雑なデータ構造をサーバとゲームアプリ間で正確に共有できます。しかし、定義を見て手でプログラムを書くのでは、効果が発揮されません。マシンリーダブルな特徴を活かしましょう。
筆者のプロジェクトではゲームアプリをUnityで開発しているためC#の話になりますが、ゲームアプリで用いられているAPIクライアントを自動生成しています。ゲームアプリ開発者はBaalの定義をするだけで、C#のオブジェクトにレスポンスをマッピングしたり、メソッドを介してリクエストを行ったりすることができます。C#などの静的型付き言語では、プログラムの中でサーバからのレスポンスを扱うために手でマッピングするのはとても苦労するので、マシンリーダブルなIDLの定義があることは非常に便利です。
サーバ側では、Baalを用いて次のようなテストコードを書いて、E2E(end to end)テストを行っています。
(1)のMyApp::Test::Baal
はPlack::Testのラッパとして実装されています。(2)のexecute
の内部ではBaal::Parser
を使って、Baal上に定義されたAPIに対してリクエストを送り、返却される型のチェックを行います。このように、IDLを使うことでE2Eテストを正確かつ簡単に書くことができます。
<続きの(2)はこちら。>
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
- 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
- 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT