「IDOLY PRIDE」におけるマスターデータについて

はじめに
株式会社QualiArtsでUnityエンジニアをしている吉田です。 2021年6月リリースの「IDOLY PRIDE」(以降、アイプラ)の開発に携わり、主にクライアント側のAPI,マスターデータなど共通基盤系の開発などを担当していました。
本記事では、アイプラでのマスターデータについて解説していきます。
マスターデータとは
GREEさんのEngineers' Blogにいい説明がありましたので、引用させていただきました。
マスターデ ータとは、ゲーム内で不変の共通パラメータ群のことを指します。モバイルゲームにおいては、アプリのバイナリアップデートをせずにゲームに反映できるよう、起動時に最新のマスターデータを引っ張ってくることが多いです。
出典:https://labs.gree.jp/blog/2015/12/15368/
まさに、アイプラでも同様でゲーム内での不変共通パラメータであり、アプリアップデートとは別にゲームに反映できるように、サーバーからマスターデータを取得しています。
マスター情報の取得
サーバーサイドに対してマスター情報取得APIを実行し、そのレスポンスからマスターファイルのダウンロード先URLなど必要な情報を得ることができます。
ダウンロードしたマスターファイルは端末のローカルストレージに保存されます。
アイプラのマスターファイルは現在132ファイルあります。
1つのマスターファイルに1種類のマスターが対応しており、運用でマスターデータが更新された場合は更新されたマスターファイルのみダウンロードされるようになっています。
マスターファイルについて
マスターファイルはSQLCipherのDBファイルになっています。
下記はキャラクターマスターを例にDBファイルにデータが格納されているイメージになります。
テーブルのカラムはデータを一意に識別するためのPrimaryKeyカラム部とdataカラムで構成されています。
dataカラムにはProtocol Buffers(以降proto)形式でシリアリズされたバイトデータが格納されています。
キャラクターマスターのテーブルイメージ
Id | data |
---|---|
char-kotono | byte[] |
char-sakura | byte[] |
: | : |
// テーブル定義DDL:キャラクターマスター
CREATE TABLE Character(
Id STRING, // データを一意に識別するためのID
data BLOB, // protoのシリアリズされたバイトデータが格納される
PRIMARY KEY(Id)
)
// proto定義:キャラクターマスター
message Character {
option (master.table) = true;
// id
string id = 1 [(master.pk) = true];
// アセットID
string assetId = 2;
// 所属グループID
string characterGroupId = 3 [(master.fk) = "CharacterGroup"];
// 順番
int32 order = 4;
// キャラクター名
string name = 5;
:
}
MasterManger
クライントでのマスターデータの管理はMasterMangaerクラスが責任をもっています。
マスターファイルのダウンロードや、マスターデータ取得もこのクラスを経由することになります。
マスターデータ取得方法
Key(データを一意に識別できる値)でのデータ取得メソッド(FindByKey系)と、全件取得メソッド(GetAll系)の2つのタイプのメソッドが用意されています。
GetAll系のメソッドは対象条件があるメソッドでも、全データ取得してからフィルターされるのでコストが高いメソッドになります。
下記キャラクターマスターからのデータ取得例になります。
// Keyで取得
var kotono = MasterManager.CharacterMaster.FindByKey("char-kotono");
// Keyを複数で取得
var chars = MasterManager.CharacterMaster.FindByKey(new []{"char-kotono","char-sakura"});
// 全データ取得
var allChars = MasterManager.CharacterMaster.GetAll();
// 対象条件とソート設定して取得
var filterChars = MasterManager.CharacterMaster.GetAll(x=> x.CharacterGroupId = "sun", (a,b)=> a.Order.CompareTo(b.Order));
// 全データ取得かつKeyでソート
var allCharsSortById = MasterManager.CharacterMaster.GetAllWithSortByKey(CharacterSortType.Id_Asc);
MasterManager.xxxxMaster
のxxxx
部分は各マスター毎に変化します。
マスターデータのリレーション
取得したマスターデータが別のマスターデータとリレーションしている場合があります。
リレーションされたデータの取得コードは冗長になるのでなるので、シンプルに取得できるような工夫をしています。
下記例はキャラクターマスターからキャラクターグループマスターを取得する例になります。
// Keyで取得
var kotono = MasterManager.CharacterMaster.FindByKey("char-kotono");
// リレーションデータの取得
// この方法だと面倒でコードが冗長すぎる
var charGroup = MasterManager.CharacterGroupMaster.FindByKey(kotono.CharacterGroupId);
// 上記の方法よりコードがスッキリ( プロパティ内部では上記と同じコードになっている )
var charGroup2 = kotono.CharacterGroup;
自動生成
これらのコードはproto定義に設定されているoption情報(マスター用custom option)を利用して自動生成されており、新規マスターの追加、既存マスターの更新された場合に、手動でのコードのメンテナンスは不要になっています。
protoで独自のマスター用custom optionを定義するにあたり、Qiitaのprotocプラグインとカスタムオプションを参考にさせていただきました。
// proto定義:キャラクターマスター
message Character {
option (master.table) = true; // マスター判定用option
// id
string id = 1 [(master.pk) = true]; // PrimaryKey判定用option
// アセットID
string assetId = 2;
// 所属グループID
string characterGroupId = 3 [(master.fk) = "CharacterGroup"]; // マスターリレーション判定用option
// 順番
int32 order = 4;
// キャラクター名
string name = 5;
:
}
マスターデータ取得コストを下げる
マスターデータを取得するためには、下記3つのステップがあります。
- DBファイルオープン(コネクション作成)
- SQL(SELECT)発行
- protoデシリアライズ
マスターデータを取得する頻度は非常に高いので、できる限り低コストで取得できるように対応をしています。
DBファイルオープン時の設定
DBファイルは読み取りしかしないので、OpenReadOnlyフラグ設定してオープンしています。
SQLite3Manager
はSQLCipherをC#から使用するために実装したクラスです。
Open時に設定するフラグ情報はSQLiteのドキュメントを参考にしました。
https://www.sqlite.org/c3ref/open.html
SQLCipherはSQLiteを拡張して実装されておりSQLCipher用に追加された機能以外は、SQLiteのドキュメントが非常に役立ちました。
:
// DBファイルをオープンしてコネクションを作成
// 更新しないのでOpenReadOnlyフラグを設定
var connection = SQLite3Manager.Open(dbFilePath, key, SQLite3.OpenReadOnly);
:
PRAGMAによる設定
トランザクション機能も不要なので DBオープン処理後に下記PRAGMAを実行して最適化しています。
PRAGMAでの設定情報はSQLiteのドキュメントを参考にしました。
https://www.sqlite.org/pragma.html
// DBファイルオープン後に実行しているPRAGMAを一部抜粋
:
PRAGMA journal_mode = OFF // トランザクション使わないのでOFFにする(ROLLBACKしないので)
PRAGMA synchronous = 0 // マスターは読み取りしかしないので 同期OFF設定にする
:
コネクションプーリング
DBオープンにより作成したコネクションはコネクションプールで管理されます。
指定数のコネクションは常に保持されており、使用頻度の高いコネクションは常にプーリングされています。コネクションプールにコネクションが存在する場合は、そこからコネクションが取得され使用されます。
すべてのマスターのコネクションを生成しておく方法が一番パフォーマンスがよいのですが、iOSではファイルオープン数の制限(200程度)があり、常時オープンするコネクション数は制御する必要がありコネクションプール方式を採用しています。
マスターデータのキャッシュ
DBファイルから取得したマスターデータは、メモリーにキャッシュされます。
キャッシュに存在する場合はDBファイルへアクセスがなく高速に取得することができます。
キャッシュ保持数は各マスター毎に設定できるようになっています。
最後に
マスターデータは運用でデータがどんどん肥大化していきます。
その影響でアプリの起動や動作が遅くならないように、またメモリーを使用しすぎないようにするために、アイプラでは今回解説した方法を採用しました。
マスターデータに関する実装の参考になれば幸いです。