[Unity] 永続化シリアライザー比較 Easy Save vs Flatbuffers

#内容
UnityのAssetStoreで売られているEasy SaveとGoogle謹製Flatbuffersの両シリアライザーを比較検討する。
比較内容はゲーム内のデータを保存して次回以降でも正しく読めるかというもの。

EasySave:
https://www.assetstore.unity3d.com/jp/#!/content/768

FlatBuffers:
https://github.com/google/flatbuffers
https://github.com/google/flatbuffers/releases
https://google.github.io/flatbuffers/index.html


#環境
Unity 5.4.1p4
Android 4.0.4
ビルドはIL2CPPを使用。

Flatbuffers v1.4.0 (ランタイムは2016/11/1 最新のもの)
Easy Save2 v2.7.1f2

#結果
Android 4.0.4の低スペック実機での確認結果。
FlatBuffersの方が1/2くらいの速度で実行できることが分かった。

値はmilliseconds.
11回目～20回目はコルーチンを呼ぶ順番を変えて実行。

|  | EasySave2 | FlatBuffers |
|--:|--:|--:|
| 1 | 2,749 | 1,246 |
| 2 | 2,800 | 1,329 |
| 3 | 2,752 | 1,266 |
| 4 | 2,741 | 1,226 |
| 5 | 2,746 | 1,242 |
| 6 | 2,723 | 1,217 |
| 7 | 2,726 | 1,236 |
| 8 | 2,716 | 1,231 |
| 9 | 2,683 | 1,242 |
| 10 | 2,670 | 1,218 |
| 11 | 3,131 | 1,249 |
| 12 | 2,571 | 1,223 |
| 13 | 2,573 | 1,220 |
| 14 | 2,512 | 1,215 |
| 15 | 2,598 | 1,246 |
| 16 | 2,528 | 1,220 |
| 17 | 2,591 | 1,235 |
| 18 | 2,627 | 1,253 |
| 19 | 2,558 | 1,243 |
| 20 | 2,513 | 1,220 |
| Average | 2,675.4 | 1,238.9 |
| Max | 3,131 | 1,329 |
| Min | 2,512 | 1,215 |
|  |  |  |
| EasySaveとの比較 |  |  |
| Average | 1.00 | 0.46 |
| Max | 1.00 | 0.42 |
| Min | 1.00 | 0.48 |
| データサイズ | 103[bytes] | 60[bytes] |


#実験内容
実験の内容は自分が使うにあたっての調査なので
公平性があるものではありません。あしからず。

設定データを保存して読み込むことを想定する。
保存データは

- Version (int)
- Music Volume (float)
- Effects Volume (float)
- Language (int)

とする。

読み込みを最初に実行して、その後書き込み、これを1セットとする。

1. 読み込み
2. 書き込み

(IN -> OUT -> IN -> OUT -> ...という繰り返し)

ファイルに書き込まれたバージョンによって
読み込み処理を変更することを想定して
バージョンで分岐処理を入れる。

Easy Save2は有料アセットなのでソースコートは公開できません。


##Easy Save2の実装

Easy Save2に構造化データを保存しようとしたとき下記の実装が考えられる。
[Tags: Saving Multiple Variables to One File](http://docs.moodkie.com/easy-save-2/guides/tags-saving-multiple-variables-to-one-file/)

ただ上記実装では値を文字列への変換、文字列をパース、パースした結果を保存と
考えるだけ遅いので、以下のReader, Wirterを使用する。
(Faster Saving and Loading using ES2Writer and ES2Reader)[http://docs.moodkie.com/easy-save-2/guides/faster-saving-and-loading-using-es2writer-and-es2reader/]

EaseSaveが保存したファイルをみたが独自バイナリフォーマットっぽかった。
そういう意味ではFlatBuffersと変わらない。


##FlatBuffersの実装
[前回](http://qiita.com/akerusoft/items/8c10f8a40fee722e6d1b#%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E5%87%BA%E5%8A%9B)の実験内容を踏まえての実装をする。

IDLは以下のとおり。

```Test.fbs
namespace Data;

file_identifier "MYFI";

union Data
{
    SettingsDataV1,
}

table Root
{
    data:Data;
}

table SettingsDataV1
{
    version:int;
    music_volume:float;
    effects_volume:float;
    language:int;
}

root_type Root;
```

これをC#のスクリプトに出力して使用した。


##テストコード


```csharp:
using FlatBuffers;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Assertions;
using System.Collections;
using System.Diagnostics;
using System.IO;

public class Serializer : MonoBehaviour
{
    const int COUNT = 1000;
    const string EASY_SAVE_FILE_NAME = "EasySave.txt";
    const string EASY_SAVE_TAG_VERSION = "Version";
    const string EASY_SAVE_TAG_MUSIC_VOLUME = "MusicVolume";
    const string EASY_SAVE_TAG_EFFECTS_VOLUME = "EffectsVolume";
    const string EASY_SAVE_TAG_LANGUAGE = "Language";

    [SerializeField]
    Text _textEasySave;

    [SerializeField]
    Text _textFlatBuffers;

    long _easySaveElapsedTime;
    long _flatbuffresElapsedTime;

    IEnumerator Start()
    {
        System.GC.Collect();
        yield return StartCoroutine(FlatBuffers());
        System.GC.Collect();
        yield return StartCoroutine(EasySave());

        _textEasySave.text = string.Format("EasySave : {0}[ms]", _easySaveElapsedTime);
        _textFlatBuffers.text = string.Format("FlatBuffers : {0}[ms]", _flatbuffresElapsedTime);
    }

    IEnumerator EasySave()
    {
        Stopwatch stopWatch = new Stopwatch();

        int version = 0;
        float musicVolume = 0f;
        float effectsVolume = 0f;
        int language = 0;

        if (ES2.Exists(EASY_SAVE_FILE_NAME))
            ES2.Delete(EASY_SAVE_FILE_NAME);

        ES2Settings settings = new ES2Settings();
        settings.optimizeMode = ES2Settings.OptimizeMode.Fast;

        stopWatch.Start();

        for (int i = 0; i < COUNT; ++i)
        {
            if (ES2.Exists(EASY_SAVE_FILE_NAME))
            {
                using (ES2Reader reader = ES2Reader.Create(EASY_SAVE_FILE_NAME, settings))
                {
                    version = reader.Read<int>(EASY_SAVE_TAG_VERSION);

                    switch(version)
                    {
                        default:
                            language = reader.Read<int>(EASY_SAVE_TAG_LANGUAGE);
                            break;
                    }
                }
            }

            version++;
            musicVolume += 1f;
            effectsVolume += 1f;
            language++;

            using (ES2Writer writer = ES2Writer.Create(EASY_SAVE_FILE_NAME, settings))
            {
                writer.Write(version, EASY_SAVE_TAG_VERSION);
                writer.Write(musicVolume, EASY_SAVE_TAG_MUSIC_VOLUME);
                writer.Write(effectsVolume, EASY_SAVE_TAG_EFFECTS_VOLUME);
                writer.Write(language, EASY_SAVE_TAG_LANGUAGE);
                writer.Save();
            }

            version = 0;
        }

        stopWatch.Stop();
        _easySaveElapsedTime = stopWatch.ElapsedMilliseconds;

        yield return null;
    }

    IEnumerator FlatBuffers()
    {
        Stopwatch stopWatch = new Stopwatch();

        int version = 0;
        float musicVolume = 0f;
        float effectsVolume = 0f;
        int language = 0;

        string path = Path.Combine(Application.persistentDataPath, EASY_SAVE_FILE_NAME);

        if (File.Exists(path))
            File.Delete(path);

        stopWatch.Start();

        for (int i=0; i<COUNT; ++i)
        {
            // 読み込み
            if(File.Exists(path))
            {
                ByteBuffer buffer = new ByteBuffer(File.ReadAllBytes(path));

                Data.Root root = Data.Root.GetRootAsRoot(buffer);

                Data.Data dataType = root.DataType;

                switch (dataType)
                {
                    case Data.Data.SettingsDataV1:
                        Data.SettingsDataV1 settings = root.GetData(new Data.SettingsDataV1());
                        version = settings.Version;
                        musicVolume = settings.MusicVolume;
                        effectsVolume = settings.EffectsVolume;
                        language = settings.Language;
                        break;
                }
            }

            version++;
            musicVolume += 1f;
            effectsVolume += 1f;
            language++;

            // 書き込み
            {
                FlatBufferBuilder builder = new FlatBufferBuilder(1);

                int offestData;
                Data.Data dataType;

                Offset<Data.SettingsDataV1> data = Data.SettingsDataV1.CreateSettingsDataV1(builder, version, musicVolume, effectsVolume, language);
                offestData = data.Value;
                dataType = Data.Data.SettingsDataV1;

                Data.Root.StartRoot(builder);
                Data.Root.AddDataType(builder, dataType);
                Data.Root.AddData(builder, offestData);
                Offset<Data.Root> endOffset = Data.Root.EndRoot(builder);

                Data.Root.FinishRootBuffer(builder, endOffset);

                //byte[] bytes = builder.SizedByteArray();

                //File.WriteAllBytes(path, bytes);

                using (MemoryStream ms = new MemoryStream(builder.DataBuffer.Data, builder.DataBuffer.Position, builder.Offset))
                {
                    byte[] bytes = ms.ToArray();
                    File.WriteAllBytes(path, bytes);
                }
            }

            version = 0;
        }

        stopWatch.Stop();
        _flatbuffresElapsedTime = stopWatch.ElapsedMilliseconds;

        yield return null;
    }
}
```

このスクリプトをGameObjectへアタッチして
uGUIのテキストへ結果を出力して確認した。


#感想
EasySaveはその名の通り簡単に保存ができるところがポイント。
速度重視でなければ、とくにEasySaveでも問題なさそう。

FlatBuffersはIDLからコード出力やデータの書き込み、読み込みの実装がやや複雑。
速度重視ならばFlatBuffersをお勧め。