[Unity] ZeroFormatterのデータ振り分けテスト

#内容
ローカルにセーブデータを保存する処理としてZeroFormatterが要件に合うかテストする。

- 保存する値をいつでも変えることができる
- 将来性を考慮してフォーマット変更時も同じファイルからロードできるようにする

FlatBuffersのテストのとき同様なフォーマットを目指す。
(http://qiita.com/akerusoft/items/8c10f8a40fee722e6d1b)


ZeroFormatter
[ZeroFormatter - C#の最速かつ無限大高速な .NET, .NET Core, Unity用シリアライザー](http://neue.cc/2016/11/08_542.html)
https://github.com/neuecc/ZeroFormatter


#環境
Unity 5.4.1p4
Windows10
ZeroFormatter v1.0
(nugetからUnity用のものをゲットした)

ポイントはデータ振り分けのことさえ確認できれば良いということで
速度面の考慮はしていないし、実装に無駄が多いかもしれませんが、、、


#結果
データにバージョン情報を埋め込んで適時振り分けることができた。

テスト実装内容
https://github.com/akerusoft/UnityTestZeroFormatter

#実装内容

```csharp:MonsterDataBase.cs
public abstract class MonsterDataBase
{
}
```

```csharp:MonsterDataV1.cs
using ZeroFormatter;

[ZeroFormattable]
public class MonsterDataV1 : MonsterDataBase
{
    [Index(0)]
    public virtual string Name { get; set; }

    [Index(1)]
    public virtual uint HitPoint { get; set; }

    [Index(2)]
    public virtual float HitRate { get; set; }

    [Index(3)]
    public virtual uint Speed { get; set; }

    [Index(4)]
    public virtual uint Luck { get; set; }
}
```

```csharp:MonsterDataV2.cs
using ZeroFormatter;

[ZeroFormattable]
public class MonsterDataV2 : MonsterDataBase
{
    [Index(0)]
    public virtual string Name { get; set; }

    [Index(1)]
    public virtual uint HitPoint { get; set; }

    [Index(2)]
    public virtual float HitRate { get; set; }

    [Index(3)]
    public virtual uint Speed { get; set; }

    [Index(4)]
    public virtual uint Luck { get; set; }

    [Index(5)]
    public virtual uint Defense { get; set; }
}
```

```csharp:DataRoot.cs
using ZeroFormatter;
using ZeroFormatter.Segments;

[ZeroFormattable]
public class DataRoot
{
    public enum DataTypeVersion
    {
        MonsterDataV1,
        MonsterDataV2,
    }

    [Index(0)]
    public virtual DataTypeVersion DataType { get; set; }

    [Index(1)]
    public virtual byte[] Data { get; set; }

    public bool SetMonsterData(MonsterDataBase data)
    {
        bool ret = false;

        if(data is MonsterDataV1)
        {
            this.DataType = DataTypeVersion.MonsterDataV1;
            this.Data = ZeroFormatterSerializer.Serialize((MonsterDataV1)data);
            ret = true;
        }
        else if(data is MonsterDataV2)
        {
            this.DataType = DataTypeVersion.MonsterDataV2;
            this.Data = ZeroFormatterSerializer.Serialize((MonsterDataV2)data);
            ret = true;
        }

        return ret;
    }

    public MonsterDataBase LoadMonsterData()
    {
        switch(DataType)
        {
            case DataTypeVersion.MonsterDataV1:
                return ZeroFormatterSerializer.Deserialize<MonsterDataV1>(Data);
            case DataTypeVersion.MonsterDataV2:
                return ZeroFormatterSerializer.Deserialize<MonsterDataV2>(Data);
            default:
                return null;
        }
    }
}

public class DataRootFormatter : ZeroFormatter.Formatters.Formatter<DataRoot>
{
    public override int? GetLength()
    {
        return null;
    }

    public override DataRoot Deserialize(ref byte[] bytes, int offset, DirtyTracker tracker, out int byteSize)
    {
        DataRoot dataRoot = new DataRoot();
        dataRoot.DataType = ZeroFormatter.Formatters.Formatter<DataRoot.DataTypeVersion>.Default.Deserialize(ref bytes, offset, tracker, out byteSize);
        dataRoot.Data = ZeroFormatter.Formatters.Formatter<byte[]>.Default.Deserialize(ref bytes, offset, tracker, out byteSize);
        return dataRoot;
    }

    public override int Serialize(ref byte[] bytes, int offset, DataRoot value)
    {
        int startOffset = offset;
        offset += ZeroFormatter.Formatters.Formatter<DataRoot.DataTypeVersion>.Default.Serialize(ref bytes, offset, value.DataType);
        offset += ZeroFormatter.Formatters.Formatter<byte[]>.Default.Serialize(ref bytes, offset, value.Data);
        return offset - startOffset;
    }
}
```

```csharp:GameController.cs
using UnityEngine;
using UnityEngine.Assertions;
using System.Collections;
using ZeroFormatter;

public class GameController : MonoBehaviour
{
    void Start()
    {
        MonsterDataV1 dataV1 = new MonsterDataV1()
        {
            Name = "スライム",
            HitPoint = 10,
            HitRate = 80f,
            Speed = 2,
            Luck = 3,
        };

        {
            byte[] bytes = ZeroFormatterSerializer.Serialize(dataV1);
            MonsterDataV1 loadData = ZeroFormatterSerializer.Deserialize<MonsterDataV1>(bytes);

            Debug.Log("Name:" + loadData.Name);
            Debug.Log("HitPoint:" + loadData.HitPoint);
            Debug.Log("HitRate:" + loadData.HitRate);
            Debug.Log("Speed:" + loadData.Speed);
            Debug.Log("Luck:" + loadData.Luck);
        }

        Debug.Log("------------------------------------");

        MonsterDataV2 dataV2 = new MonsterDataV2()
        {
            Name = "スライムベッキー",
            HitPoint = 100,
            HitRate = 95f,
            Speed = 200,
            Luck = 300,
            Defense = 400,
        };

        {
            byte[] bytes = ZeroFormatterSerializer.Serialize(dataV2);
            MonsterDataV2 loadData = ZeroFormatterSerializer.Deserialize<MonsterDataV2>(bytes);

            Debug.Log("Name:" + loadData.Name);
            Debug.Log("HitPoint:" + loadData.HitPoint);
            Debug.Log("HitRate:" + loadData.HitRate);
            Debug.Log("Speed:" + loadData.Speed);
            Debug.Log("Luck:" + loadData.Luck);
            Debug.Log("Defense:" + loadData.Defense);
        }

        Debug.Log("------------------------------------");

        {
            DataRoot dataRoot = new DataRoot();

            dataRoot.SetMonsterData(dataV1);

            {
                byte[] bytes = ZeroFormatterSerializer.Serialize(dataRoot);
                DataRoot loadData = ZeroFormatterSerializer.Deserialize<DataRoot>(bytes);

                MonsterDataBase dataBase = loadData.LoadMonsterData();

                if (dataBase is MonsterDataV1)
                {
                    MonsterDataV1 data = dataBase as MonsterDataV1;
                    Debug.Log("Name:" + data.Name);
                    Debug.Log("HitPoint:" + data.HitPoint);
                    Debug.Log("HitRate:" + data.HitRate);
                    Debug.Log("Speed:" + data.Speed);
                    Debug.Log("Luck:" + data.Luck);
                }
                else
                {
                    Debug.LogError("Failed load data version1.");
                }
            }
        }

        Debug.Log("------------------------------------");

        {
            DataRoot dataRoot = new DataRoot();

            dataRoot.SetMonsterData(dataV2);

            {
                byte[] bytes = ZeroFormatterSerializer.Serialize(dataRoot);
                DataRoot loadData = ZeroFormatterSerializer.Deserialize<DataRoot>(bytes);

                MonsterDataBase dataBase = loadData.LoadMonsterData();

                if (dataBase is MonsterDataV2)
                {
                    MonsterDataV2 data = dataBase as MonsterDataV2;
                    Debug.Log("Name:" + data.Name);
                    Debug.Log("HitPoint:" + data.HitPoint);
                    Debug.Log("HitRate:" + data.HitRate);
                    Debug.Log("Speed:" + data.Speed);
                    Debug.Log("Luck:" + data.Luck);
                    Debug.Log("Defense:" + data.Defense);
                }
                else
                {
                    Debug.LogError("Failed load data version2.");
                }
            }
        }
    }
}
```

```shell-session
zfc.exe -i (プロジェクトファイル名).csproj -o (UnityのAssetフォルダ配下)/(ファイル名).cs
```


#実行結果

```shell-session
Name:スライム
HitPoint:10
HitRate:80
Speed:2
Luck:3
------------------------------------
Name:スライムベッキー
HitPoint:100
HitRate:95
Speed:200
Luck:300
Defense:400
------------------------------------
Name:スライム
HitPoint:10
HitRate:80
Speed:2
Luck:3
------------------------------------
Name:スライムベッキー
HitPoint:100
HitRate:95
Speed:200
Luck:300
Defense:400
```

#感想
FlatBuffersのAPIは少し複雑で直感的ではありませんでした。
(書き込む順番や文字列の書き込み等)
IDLに宣言すれば自動でフィールドにIDをつけてくれるということはありがたいことですが
その手間が増えるとは言え、ZeroFormatterのように明示的にIndexをコード上に宣言する方が
やはり直感的ではないでしょうか？
(明示的な方が逆にわかりやすい)

データの振り分けをする手段はあるということはわかりました。
あとは各自かっこいい実装をすればいいんですね！

直感的っていいもんだ！:blush: