コンテンツへスキップ

QualiArtsengineer blog

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

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

6 min read

はじめに

株式会社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)形式でシリアリズされたバイトデータが格納されています。

キャラクターマスターのテーブルイメージ

Iddata
char-kotonobyte[]
char-sakurabyte[]
::
// テーブル定義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.xxxxMasterxxxx部分は各マスター毎に変化します。

マスターデータのリレーション

取得したマスターデータが別のマスターデータとリレーションしている場合があります。

リレーションされたデータの取得コードは冗長になるのでなるので、シンプルに取得できるような工夫をしています。

下記例はキャラクターマスターからキャラクターグループマスターを取得する例になります。

// 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つのステップがあります。

  1. DBファイルオープン(コネクション作成)
  2. SQL(SELECT)発行
  3. 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ファイルへアクセスがなく高速に取得することができます。

キャッシュ保持数は各マスター毎に設定できるようになっています。

最後に

マスターデータは運用でデータがどんどん肥大化していきます。

その影響でアプリの起動や動作が遅くならないように、またメモリーを使用しすぎないようにするために、アイプラでは今回解説した方法を採用しました。

マスターデータに関する実装の参考になれば幸いです。