【Unity】Unity で ゲームデータをセーブするのに FlatBuffers を使ってみる(導入編)
永続化したいデータ(ゲームを終了しても残しておきたデータ)をどうやって保存するのが最適なのかはいつも悩みます。今開発中のゲームはサーバを使用しないつもりなので、データはローカル(端末)に保存するつもりですが、パッと思いつくだけでも色んな実装方法が考えられますね。
- PlayerPrefs に保存
- SqlLite に保存
- オブジェクトをJsonファイルに変換して保存
- オブジェクトをバイナリに変換して保存
今回は比較的大量のデータを高速で保存したかったので「オブジェクトをバイナリに変換して保存」を採用しました。今までは MessagePack を使ってオブジェクトをバイトデータに変換してたのですが、さらに速いと噂の FlatBuffers を使ってみることにしました。FlatBuffers は cocos-2dx や Facebook でも採用されているみたいです。
FlatBuffers のインストール
導入するまでが結構面倒です(そのうち改善されるとは思いますが..)
Windows だとexeファイルが用意されているようなので簡単そうですが、Macだとソースコードからビルドして実行環境を作らないといけません(*1)
FlatBuffers のソースコードをダウンロード
下記の FlatBuffers の github に行って...
右側の 「Clone or download」からソースコード一式をダウンロード
ダウンロードしたファイルは解凍して適当な場所に置いてください。
CMake のインストール
CMake がインストールされてなければ、下記サイトへ行って...
Mac用をダウンロード
ダウンロードしたファイルをダブルクリックしてアプリケーションをインストール。
CMake の実行
インストールした Cmake.app をダブルクリックして起動すると、下記の画面が立ち上がるので...
[Where is the source code] に 先ほどダウンロードしたFlatBuffersのソースコード(flatbuffers-master)を指定
[Where to build the binaries] には任意のフォルダを指定(ビルドしたファイルが格納される場所です。flatbuffers-build とか適当なフォルダを作成して選択すればOK)
Configure ボタンを実行するとこんな画面が...
赤くてびっくりしましたが、気にせずGenerateボタンを実行
下のウィンドウに Generating doneと出てれば完了。出力先に指定したフォルダに各種ファイルが生成されてるはず。
Make を実行
ターミナルを開いて、先ほど出力先に指定したフォルダに移動して
make
を実行!終わったら、
./flattests
を実行してみて、「ALL TESTS PASSED」と出力されればインストール成功!
Unity で FlatBuffers を使用するための準備
スキーマファイルを作成
とりあえず動かすことが目標なので、UserData というテーブルの中にCoinNum変数があるだけの構造を定義しました。
namespace Rogue;
table UserData {
coinNum:int;
}root_type UserData;
これを UserData.fbs というファイル名で保存しました。公式のサンプルにならって拡張子を fbs (FlatBuffersの略だと思われる)にしましたが、中身は単なるテキストファイルです。拡張子を txt にしても問題なく動きました。
クラスを自動生成
バイナリデータをC#で扱うためのクラスファイルを、スキーマファイルから自動生成します。
./flatc -o [出力先のパス] -n [スキーマファイルのパス(ex: UserData.fbs)]
実行すると出力先のフォルダに UserData.cs が生成されるはずです。生成されたクラスは Unity のAsset フォルダ内の任意の場所に置いてください。
FlatBuffers ライブラリを Unity へインポート
github からダウンロードしたフォルダ(flatbuffers-master)のnetフォルダ配下にある FlatBuffers フォルダを Unity の Assetフォルダ以下へコピー。
実装サンプル
書き込み処理サンプル
バイナリデータを保存する処理のサンプルです。 Startを呼んで値を設定してEndを呼んで最後にFinishという流れで書かないとエラーになります。
// 事前に using FlatBuffers; が必要
FlatBufferBuilder fbb = new FlatBufferBuilder (1);
// 値を設定する前に Start を呼び出す必要があるみたい
UserData.StartUserData (fbb);
// コイン数を15枚で設定
UserData.AddCoinNum (fbb, 15);
// 値の設定が終わったら End を呼び出して..
Offset<UserData> offset = UserData.EndUserData (fbb);
// 最後に Finish すると バイナリデータが生成されるのかな
UserData.FinishUserDataBuffer (fbb, offset);
// 最終的にバイナリデータをファイルに書き出して完了
using (MemoryStream ms = new MemoryStream(fbb.DataBuffer.Data, fbb.DataBuffer.Position, fbb.Offset)) {
File.WriteAllBytes(filePath, ms.ToArray());
Debug.Log("SAVED !");}
filePath には
Path.Combine (Application.persistentDataPath, "UserData.bytes")
などを出力先のパスを設定してください。
読み込み処理サンプル
読み込みのほうが簡単ですね。
if (File.Exists (filePath)) {
ByteBuffer bb = new ByteBuffer (File.ReadAllBytes (filePath));
UserData userData = UserData.GetRootAsUserData (bb);
Debug.Log ("## user coin num = " + userData.CoinNum);
}
あとがき
導入の手軽さで言えば、MessagePack のほうが圧倒的に簡単ですね。JSONで出力してて遅いなと思った時は MessagePack に簡単に移行できます。シリアライズの実装も MessgePack だと1行で済むところが、FlatBuffers だと スキーマファイルを作成して、決められた順序でデータをセットしなきゃいけないので(子要素がある場合は子要素から順番にシリアライズしないといけないとか)、開発コストがかかりそうです。
MessagePack もそこそこ速いので、データ量が多くなければ速度差はそこまで気にならないかもしれないですね。ただ、MessagePack だとオブジェクトを丸ごとシリアライズして保存するので保存ファイルに定義データが含まれるのですが、FlatBuffersだとスキーマが別ファイルなので、ファイルサイズが小さくて済みそうです。
あと、FlatBuffers は生のバイナリデータを持ったまま、必要な時に必要な分だけパース処理をするので、大量にデータはあるんだが実際に使用するデータはごく一部の場合は、FlatBuffes のほうがかなり速くはなりそうです。
参考資料
C# での FlatBuffers を使う流れが解説してあります。
運用面での考察が興味深いです。
英語ですけど、1番わかりやすかったです。
FlatBuffers が何故速いのか、とても参考になりました。
*1:後から知ったのですが、Homebrewでもインストールできるみたいです。こっちのほうが簡単だったかも。