土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#unity (C#)でFlatBuffersを使い倒す(1) ゲーム開発時に数値系データを外部バイナリファイルで管理する方法

 前回の記事でJSONデータを構造体配列に読み込むことができたので、次はこれをバイナリデータとしてシリアライズ/デシアライズする方法を検証します。どちらかと言えばここからが本番です。
 しばらくはFlatBuffersというライブラリの挙動を検証します。実際にコードを書く前に、そもそもの話である「データを外部ファイルにシリアライズ(永続化)するとはどういう事なのか」について、軽くまとめておきたいと思います。

ゲームで使うデータ/Excelの2次元テーブル

 ゲームでは様々なフォーマットの、かつ、大量の数値データを扱います。わかりやすい例としてよく出てくるのはRPGのモンスターのデータです。
 1体のモンスターは多くの数値データから構成されています。例えば、名前、HP、MP、攻撃力、防御力……などです(ゲームによって変わります)。また、攻撃方法や使える魔法、特殊能力、ドロップアイテム発生時に参照するテーブルなどをそれぞれ識別番号(IDと呼ぶことが多いです)やビットフラグで設定します。
 異なるモンスターも、同じデータ構造(つまり構造体)で管理します(とういか、管理できるように設計します)。モンスターは何十種~何百種といるでしょうから、これは巨大な2次元テーブルを構成します。
 ゲーム開発では、通常この2次元テーブルをExcelで管理します。一つのxlsxファイルに、上記のようなテーブルが書かれたシートが1~n枚入っていて、また、そのようなxlsxファイルが大量を作成します。ゲームプランナーの仕事に含まれるサブ作業(というか、むしろメイン作業かも)に、これらの膨大なxlsxファイルの更新があります。
 Excelによるテーブル管理が最も効率的なのかは、正直よくわかりません。ログが取れたり安全性の高いRDBを使うべきという考えもあるし、実際そうしているメーカーもあるかもしれません(土屋は知りませんが)。
 しかし、ゲーム開発では、異なるデータ構造(RDBで言う所のテーブルスキーマ)のテーブルを何十~何百種類も作る必要があり、また個々のテーブルのデータ構造自体も、開発の最中にコロコロ変わるので、Excelで管理した方が急な変更に対応しやすいというのはあると思います(将来的には変わっていくのかもしれませんが)。

Excelデータを実行中に読み込む

 さて、Excelで作った大量のテーブルを、ゲームの実行時にどうにかして取り込まなければなりません。Excelファイルをそのままパッケージに添付するわけにもいかないので、大抵はExcelのテーブルをバイナリデータにコンバートします。
 この作業は、大体以下のような2段階で行います。

  1. 事前に、テーブルの数値データをツールでバイナリデータにコンバートしてバイナリファイルを作る(ツールはプログラマが独自に作る。言語は様々)
  2. ゲーム実行時にバイナリデータを読み込み、そのデータの中身を構造体配列のインスタンスに格納する。

 格納が終わった時点で、プログラムからは単に構造体配列が見えているだけなので、添え字番号を指定して、任意のモンスターのデータを取得できるわけです。

メモリマッピング

 バイナリデータを構造体配列にマッピングするには様々な方法があるのですが、ゲーム開発では、メモリマッピングというプリミティブなマッピング手法が広く使われています。これは、バイナリデータをメモリ上にベタに展開し、そのメモリ領域の先頭アドレスと、構造体配列のポインタ(≒参照先)とを強制的に一致させ、その構造体配列をそのまま使うという物です。
 これは恐ろしくプリミティブな処理でして、開発中の事故が怖い(例えば、急にフィールドが一個増えたりすると破綻します)のですが、その分恐ろしく高速に動作します。というか、ロジックと言えるような物がそもそもないので、あらゆるマッピング処理の中で恐らく最速ではないかと思います。
 32bitコンソール機が主流だった頃はまだCPU速度も、CD-ROM等のメディアアクセス速度も低かったため、レスポンスがプレイアビリティに直結するゲーム開発においては速度が重視され、メモリマッピングが標準的に使われてきたのだろうと想像できます。現在でもC++をメインで使う開発では使われているかと思います(土屋は知りませんが)。

FlatBuffers

 メモリマッピングは強力ですが、前述の通りスキーマ変更時の事故に弱く、また、生のポインタ使用によりメモリリークの危険もあります。そして、C#などのポインタを直接扱えないプログラミング言語では、簡単にはメモリマッピングを実現できません(unsafeな手法を使えば可能かなとも思いますが、アライメントやエンディアンの問題まで考えると現実的では無いのかなとも思います。詳しい方いたら教えてください)
 さて、ではUnityでの、つまりC#によるゲーム開発ではどのようなマッピング手法が有効でしょうか。選択肢は幾つかあるのですが、今回はFlatBuffersというライブラリを使ってみることにします(他のライブラリについてもおいおい検証していきたいと思います)。

続く

 シリアライズの解説をしただけでこんなになってしまったので今回はここまで。なお、「なぜJsonUtilityだけ使うのではだめなのか」についてもいずれ書くつもりです。

余談:ORマッピングについて

 ビジネスのWebアプリでは、下位層がRDBに対してクエリを送り、戻ってきた結果(=2次元テーブル)をオブジェクトにマッピングして上位層に返す事が一般的です。上位層ではオブジェクトを操作して情報を処理し、HTMLを生成してクライアントに送信します。このような「クエリ結果をオブジェクトにマッピングする」という事またはその処理を「ORマッピング(Object Relational Mapping)」と言います。
 オブジェクトは基本的にテーブルと対応し、そのテーブルが持つフィールドと同じ名前のプロパティを持ちます。このテーブルに対応するオブジェクトのコードは定型なので、一般的にはWebアプリ用のフレームワークによって自動的に生成すれます。
 今後解説するFlatBuffersには、flatc.exeという実行ファイルがあり、データ構造に応じて必要なコードを自動的に生成します。

Unityの教科書 Unity2019完全対応版  2D&3Dスマートフォンゲーム入門講座 (Entertainment&IDEA)

Unityの教科書 Unity2019完全対応版 2D&3Dスマートフォンゲーム入門講座 (Entertainment&IDEA)