[Unity] EasySave3 ネストしたデータの保存

また備忘です。長文です。reflection使えばprivateもできるかも。コストも読み書きの時だけで安いし。

今回はUnityで大人気の有償アセットEasySave3についてです。
保存できる型はデフォルトで基本の型のほとんどがサポートされています。

が、ネストしたデータやサポートにない独自の型のセーブロードどうすればいいの…ってときです。

大きく分けて対策２個あると思うんです。一長一短ある。
・ネストした型をJSONにパースしてstringに変換後に保存！
　[JsonUtility をつかって Unity で JSON を取り扱う方法](https://qiita.com/sea_mountain/items/6513b330983ffa003959 )
・独自のセーブロードタイプクラスを作成（今回はこ↑こ↓）

結論を書くと、自分でES3用のセーブロードタイプクラスを作るのです。

公式のマニュアルに書いてあるのでこちらを参考してください。
- https://docs.moodkie.com/easy-save-3/es3-guides/choosing-what-is-saved/

・独自クラス作成前セーブデータ（保存できてない）

```txt:OldSavedata.dat
	"LanguageDatabase" : {
		"__type" : "Databases.DatabaseObject,Assembly-CSharp",
		"value" : {
			"__type" : "Databases.LanguageDatabase,Assembly-CSharp",
			"_ES3Ref" : "1380498291805822379",
			"_ES3Ref" : "1380498291805822379"
		}
	},
```

・独自クラス作成後セーブデータ（どやぁ）

```txt:NewSavedata.dat
	"LanguageDatabase" : {
		"__type" : "Databases.DatabaseObject,Assembly-CSharp",
		"value" : {
			"__type" : "Databases.LanguageDatabase,Assembly-CSharp",
			"_ES3Ref" : "5303085288931533276",
			"_ES3Ref" : "5303085288931533276",
			"Items" : [
				{
					"Language" : "English",
					"CompoItems" : [
						{
							"Type" : 1,
							"LanguageData" : [
								{
									"ID" : 0,
									"SearchID" : "KeyName",
									"Word" : "Key Name"
								},{
									"ID" : 1,
									"SearchID" : "BindName",
									"Word" : "Bind Key"
								},{
							]
						},{
							"Type" : 2,
							"LanguageData" : [
								{
									"ID" : 60,
									"SearchID" : "TXT_HotKey",
									"Word" : "HotKey"
								},{
									"ID" : 61,
									"SearchID" : "TXT_Video",
									"Word" : "Video"
								},{
						},{
					]
				},{
					"Language" : "日本語",
					"CompoItems" : [
						{
							"Type" : 1,
							"LanguageData" : [
								{
									"ID" : 0,
									"SearchID" : "KeyName",
									"Word" : "名称"
								},{
									"ID" : 1,
									"SearchID" : "BindName",
									"Word" : "設定キー"
								},{
以下略
```
※SakuraEditorで開くと文字コードがShiftJisでに設定された文字化けするけど、
　UTF-8 Bom付でもう一度開けば文字化けは解消されます。（文字コードは怖いね）

保存対象のデータ構造（Odin使ってます。見にくい人いるかも、とりあえずネストした構造です。）
![構造.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353494/fc754205-3408-2d41-f2ae-d083872a19cd.png)
セーブを実行しているコードと、クラス・構造体もペタっとおいておきます。ソロ開発のコードなので好きに見ていきなペッ！

```csharp:SaveExecute.cs
    ES3.Save(GetType().Name, (LanguageDatabase.Instance)this, dataPath); //SaveData.csのinstanceされたデータ
```
```csharp:SaveData.cs
    [System.Serializable] // 保存対象
    public class LanguageDatabase : SingletonDatabase<LanguageDatabase>, IMasterData
    {
        // いろんな項目が多々ある
        public List<LanguageDataList> Items = new List<LanguageDataList>(); //保存されるデータ
        // いろんなメソッドが多々ある
    }
```
```csharp:Data.cs
    [System.Serializable] // Serializeなデータ
    public class LanguageDataList
    {
        public string Language;
        public List<LanguageCompoTypeData> CompoItems = new List<LanguageCompoTypeData>();
        public LanguageDataList(string val)
        {
            Language = val;
        }
    }
    [System.Serializable]
    public class LanguageCompoTypeData
    {
        public CompoLangType Type = CompoLangType.None;
        public List<LanguageData> LanguageItems = new List<LanguageData>();
        public LanguageCompoTypeData(CompoLangType type)
        {
            Type = type;
        }
    }
    [System.Serializable]
    public class LanguageData
    {
        public int ID;
        public string SearchID;
        public string Word;
        public string GetWord() { 
            return  @Word;
        }
    }
```

<h2>今回作ったもの（重要）</h2>
ネストした個数分タイプを作りました。
~~＃一番下層のタイプはもしかしたらいらないかも（ES3UserType_LanguageData.cs）~~
独自の型は全部必要なので定義してください。

```csharp:ES3UserType_LanguageDataList.cs
using Databases;
using System.Collections.Generic;

namespace ES3Types
{
    [UnityEngine.Scripting.Preserve]
	[ES3PropertiesAttribute()]
	public class ES3UserType_LanguageDataList : ES3ObjectType
	{
		public static ES3Type Instance = null;

		public ES3UserType_LanguageDataList() : base(typeof(LanguageDataList)){ Instance = this; priority = 1; }


		protected override void WriteObject(object obj, ES3Writer writer)
		{
			var instance = (LanguageDataList)obj;
			writer.WriteProperty<string>("Language", instance.Language);
			writer.WriteProperty<List<LanguageCompoTypeData>>("CompoItems", instance.CompoItems);
		}

		protected override void ReadObject<T>(ES3Reader reader, object obj)
		{
			var instance = (LanguageDataList)obj;
			foreach(string propertyName in reader.Properties)
			{
				switch(propertyName)
				{
					case "Language":
						instance.Language = reader.Read<string>(); break;
					case "CompoItems":
						instance.CompoItems = reader.Read<List<LanguageCompoTypeData>>(); break;
					default:
						reader.Skip();
						break;
				}
			}
		}

		protected override object ReadObject<T>(ES3Reader reader)
		{
			var instance = new LanguageDataList("Language");
			ReadObject<T>(reader, instance);
			return instance;
		}
	}
}
```
```csharp:ES3UserType_LanguageCompoTypeData.cs
namespace ES3Types
{
    [UnityEngine.Scripting.Preserve]
	[ES3PropertiesAttribute()]
	public class ES3UserType_LanguageCompoTypeData : ES3ObjectType
	{
		public static ES3Type Instance = null;

		public ES3UserType_LanguageCompoTypeData() : base(typeof(LanguageCompoTypeData)){ Instance = this; priority = 1; }


		protected override void WriteObject(object obj, ES3Writer writer)
		{
			var instance = (LanguageCompoTypeData)obj;
			writer.WriteProperty<CompoLangType>("Type", instance.Type);
			writer.WriteProperty<List<LanguageData>>("LanguageData", instance.LanguageItems);
		}

		protected override void ReadObject<T>(ES3Reader reader, object obj)
		{
			var instance = (LanguageCompoTypeData)obj;
			foreach(string propertyName in reader.Properties)
			{
				switch(propertyName)
				{
					case "Type":
						instance.Type = reader.Read<CompoLangType>(); break;
					case "LanguageData":
						instance.LanguageItems = reader.Read<List<LanguageData>>(); break;
					default:
						reader.Skip();
						break;
				}
			}
		}

		protected override object ReadObject<T>(ES3Reader reader)
		{
			var instance = new LanguageCompoTypeData(CompoLangType.None);
			ReadObject<T>(reader, instance);
			return instance;
		}
	}
}
```
```csharp:ES3UserType_LanguageData.cs
namespace ES3Types
{
    [UnityEngine.Scripting.Preserve]
	[ES3PropertiesAttribute()]
	public class ES3UserType_LanguageData : ES3ObjectType
	{
		public static ES3Type Instance = null;

		public ES3UserType_LanguageData() : base(typeof(LanguageData)){ Instance = this; priority = 1; }


		protected override void WriteObject(object obj, ES3Writer writer)
		{
			var instance = (LanguageData)obj;
			writer.WriteProperty<int>("ID", instance.ID);
			writer.WriteProperty<string>("SearchID", instance.SearchID);
			writer.WriteProperty<string>("Word", instance.Word);
		}

		protected override void ReadObject<T>(ES3Reader reader, object obj)
		{
			var instance = (LanguageData)obj;
			foreach(string propertyName in reader.Properties)
			{
				switch(propertyName)
				{
					case "ID":
						instance.ID = reader.Read<int>(); break;
					case "SearchID":
						instance.SearchID = reader.Read<string>(); break;
					case "Word":
						instance.Word = reader.Read<string>(); break;
					default:
						reader.Skip();
						break;
				}
			}
		}

		protected override object ReadObject<T>(ES3Reader reader)
		{
			var instance = new LanguageData();
			ReadObject<T>(reader, instance);
			return instance;
		}
	}
}
```

以上です。お疲れ様です。

解説はとくにありません・・・

WriteObjectとReadObjectがそれぞれ対で設定できてればオッケー！
ReadObjectは今回の場合はほとんどいらないかも。一応作ってるけど。

EasySave3のソースコードのぞいてみるとわかるけど、
プリミティブ型⇒Listとか汎用な型⇒Unity謹製型⇒独自型⇒ないならエラー！
ってとこでList構造見つけるまではいいけどListに指定されている型が謎だからできんってなってる。

そこに今回の独自型を作成して、参照を自動的にお願いしているだけです。

https://qiita.com/AllNight/items/cd8d7b6111a9f75afb55
上記の記事で良い感じのデータ定義と、データロードができるようになりました。
公式でぜひ採用してほしいレベル（何）