Unity の自作セーブ機能を無料公開してみる

今回のテーマは
「Unity の自作セーブ機能を無料公開してみる」
です。

「PlayerPrefs と同じように使用できる」
「多くの型を保存できる」
「高速で動作する」
という特徴を持っています。興味があれば是非お試し下さい。

***

こんにちは uni928 です。
今回は Unity の自作のセーブデータ管理機能である
SephirothSaveReducedVersion を
無料公開したいと思います。

SephirothSave.cs という
8 キロバイトのファイルだけで動作するため
デモシーンや HowToUse 等
他のファイルは削除して頂いて構いません。

SephirothSaveReducedVersion の
ライセンスは MIT とさせて頂きます。
ソースを公開する目的でない限り
クレジットの必要はないものとします。
(例：アプリに組み込む場合はクレジット不要)

また、SephirothSaveReducedVersion で生じたあらゆる損害に対して
我々は責任を負わないものとさせて下さい。
何卒よろしくお願いします。

***

配布先

[https://booth.pm/ja/items/5330615](https://booth.pm/ja/items/5330615)

気に入って頂けたならば
高評価を付けに行って頂けると
非常に嬉しいです。(pixiv アカウントが必要です)

pixiv アカウントをお持ちでない方は
下記の URL からダウンロードして下さい。

[https://drive.google.com/file/d/1YFOhD6cxwEBgF5N4dUcW4vJsz6u1gPVD/view?usp=sharing](https://drive.google.com/file/d/1YFOhD6cxwEBgF5N4dUcW4vJsz6u1gPVD/view?usp=sharing)

***

SephirothSaveReducedVersion の使用方法

SephirothSaveReducedVersion の
unitypackage をインポートした後に
下記の流れを行います。

```Csharp:一部抜粋
int saveNum = 32;
SephirothTools.SephirothSave.Save<int>("key1", saveNum); //保存
int loadedNum1 = SephirothTools.SephirothSave
          .Load<int>("key1"); //読み込み1
int loadedNum2 = SephirothTools.SephirothSave
          .Load<int>("key1", defaultValue: 64); //読み込み2
```

多くの型を保存できるため
List＜int＞ 型も保存できます。

```Csharp:一部抜粋
List<int> saveList = new List<int>(){1, 2, 3};
SephirothTools.SephirothSave.Save<List<int>>("key1", saveList); //保存
List<int> loadedList = SephirothTools.SephirothSave
          .Load<List<int>>("key1"); //読み込み
```

Save メソッドを実行することで
ロード機能を利用できるようになります。

また、その状態で Flush メソッドを実行すると
再起動後もロード機能が使用できます。
逆に言うと Flush メソッドを実行しないと
Save した内容はアプリ終了と同時に失われてしまいます。
なので定期的な Flush メソッドの実行が必須になります。

Save の度に Flush を実行すると動作が遅くなるため
ある程度のまとまりで Flush を実行しましょう。

実行速度も高速です。
例えば下記の処理は 1000 ミリ秒(1秒) を
超えることがありませんでした。
(CPU：第 10 世代 インテル Core i7 10510U、メモリ：DDR4 16GB のノート PC で確認)

20万回の Save
20万回の Load
100 回の Flush
を実行して 1000 ミリ秒なので
速度は早い方だと自負しています。

```Csharp:一部抜粋
class SpeedTest {
 public void Exec() {
 Human1 save = new(20, 175, "Tom");
 for (int i = 0; i < 100; i++)
 {
  for (int j = 0; j < 2000; j++)
  {
   SephirothTools.SephirothSave.Save<Human1>("key" + j, save);
   SephirothTools.SephirothSave.Load<Human1>("key" + j);
  }
 SephirothTools.SephirothSave.Flush();
 }
}
[System.Serializable]
public class Human1
{
 [System.NonSerialized]
 public bool isInit;
 private int age;
 public int height;
 public string name;
 public Human1(int age, int height, string name)
 {
  this.age = age;
  this.height = height;
  this.name = name;
  isInit = true;
 }
 public int GetAge()
 {
  return age;
 }
}
```

***

あとがき

SephirothSaveReducedVersion はいかがでしたか？

PlayerPrefs と同じような記述でプログラミングできるため
プログラミングの難易度は限りなく低いと自負しています。

PlayerPrefs.Save に相当する Flush の実行が
必須な点だけは難点でしょうか？
(PlayerPrefs.Save も本来は必須です)

SephirothSaveReducedVersion という名前の通り
この機能は SephirothSave の機能縮小版になります。
SephiorthSave は有料のアセットですが
違いはセーブデータを暗号化するかどうかがほとんどなので
使用感に違いはありません。
機能縮小版も同じ使い方ができます。

この無料版の方のアセットを使用することで
皆さんの開発の助けになれたならば
非常に嬉しいです。

閲覧ありがとうございました。

***

追記

SephirothSaveReducedVersion の
コードを載せておきます。

SephirothSave.cs を作成して
中身をこの内容にして頂いても
動作します。

```Csharp:SephirothSave.cs
using UnityEngine;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using System.IO.Compression;

/**
 * SephirothSave is a function to manage save data.
 * Many type can be saved. For example, you can also store List<int> types.
 * 
 * However, when reading, it is necessary to read in the type when it was saved.
 * Data saved in string type cannot be retrieved in int type.
 * So you need to remember the type when you save it.
 * 
 * 
 * SephirothSave will be able to use the load function by executing the Save method.
 * Also, if the Flush method is executed in that state, the load function can be used even after restarting.
 * However, while the save and load methods are both fast, the flush method is slow.
 * So you don't have to run the Flush method every time you run the Save method.
 * The Flush method should be executed at an appropriate time, such as when switching scenes.
 * 
 * Example of use：
 * List<int> saveList = ...;
 * SephirothTools.SephirothSave.Save<List<int>>("key1", saveList);
 * List<int> loadedList = SephirothTools.SephirothSave.Load<List<int>>("key1");
 * 
 * Example of use：
 * SephirothTools.SephirothSave.Save<int>("key2", 32);
 * int loadNum = SephirothTools.SephirothSave.Load<int>("key2", defaultValue:0);
 */

namespace SephirothTools
{
    public static class SephirothSave
    {
        /**
         * load of any type
         * 
         * Example of use：
         * List<int> loadList = SephirothTools.SephirothSave.Load<List<int>>("key1");
         * string s = SephirothTools.SephirothSave.Load<string>("key2");
         * int num = SephirothTools.SephirothSave.Load<int>("key3", defaultValue:0);
         */
        public static T Load<T>(string key, T defaultValue = default)
        {
            return SephirothSaveCore1.Load<T>(key, defaultValue);
        }

        /**
         * save of any type
         * 
         * Example of use：
         * SephirothTools.SephirothSave.Save<List<int>>("key1", saveList);
         * SephirothTools.SephirothSave.Save<string>("key2", "aaa");
         */
        public static void Save<T>(string key, T value)
        {
            SephirothSaveCore1.Save<T>(key, value);
        }

        /**
         * Carrying out preservation
         * 
         * if the Flush method is executed in that state, the load function can be used even after restarting.
         */
        public static void Flush()
        {
            SephirothSaveCore1.Flush();
        }

        /**
         * whether the key exists
         */
        public static bool HasKey(string key)
        {
            return SephirothSaveCore1.HasKey(key);
        }


        /**
         * delete key
         */
        public static void DeleteKey(string key)
        {
            SephirothSaveCore1.DeleteKey(key);
        }

        /**
         * delete all
         */
        public static void DeleteAll()
        {
            SephirothSaveCore1.DeleteAll();
        }
    }

    public class SephirothSaveCore1
    {
        private static readonly object Lock = new();

        public static T Load<T>(string key, T defaultValue = default)
        {
            lock (Lock)
            {
                return SephirothSaveCore2.instance.Load<T>(key, defaultValue);
            }
        }

        public static void Save<T>(string key, T value)
        {
            lock (Lock)
            {
                SephirothSaveCore2.instance.Save<T>(key, value);
            }
        }

        public static void Flush()
        {
            lock (Lock)
            {
                SephirothSaveCore2.instance.Flush();
            }
        }

        public static bool HasKey(string key)
        {
            lock (Lock)
            {
                return SephirothSaveCore2.instance.HasKey(key);
            }
        }

        public static void DeleteKey(string key)
        {
            lock (Lock)
            {
                SephirothSaveCore2.instance.DeleteKey(key);
            }
        }

        public static void DeleteAll()
        {
            lock (Lock)
            {
                SephirothSaveCore2.instance.DeleteAll();
            }
        }
    }

    public class SephirothSaveCore2
    {
        public static SephirothSaveCore2 instance = new();

        private System.Collections.Hashtable playerPrefsHashtable = new(); //BinaryFomatter is faster for Hashtable than Dictionary

        private byte[] serializedOutput = null;
        private bool hashTableChanged = false;

        private readonly string fileName = Application.persistentDataPath + "/SephirothPlayerPrefs.txt";

        SephirothSaveCore2()
        {
            //File.Delete(Application.persistentDataPath + "/SephirothPlayerPrefs.txt"); File.Delete(Application.dataPath + "/SephirothPlayerPrefs.txt");
#if UNITY_IOS
		Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER", "yes");
#endif
            Deserialize();
            Save<string>("SephirothSaveReducedVersionVersion", "1.0.0");
            hashTableChanged = false;
        }

        [RuntimeInitializeOnLoadMethod]
        private static void Init()
        {
            instance.HasKey(""); //I want to run the constructor
        }

        public bool HasKey(string key)
        {
            return playerPrefsHashtable.ContainsKey(key);
        }

        public void Save<T>(string key, T value)
        {
            playerPrefsHashtable[key] = value;
            hashTableChanged = true;
        }

        public T Load<T>(string key, T defaultValue)
        {
            object value = playerPrefsHashtable[key];
            if (value != null)
            {
                return (T)value;
            }
            else
            {
                return defaultValue;
            }
        }

        public void DeleteKey(string key)
        {
            playerPrefsHashtable.Remove(key);
            hashTableChanged = true;
        }

        public void DeleteAll()
        {
            playerPrefsHashtable.Clear();
            hashTableChanged = true;
        }

        public void Flush()
        {
            if (hashTableChanged)
            {
                Serialize();
                try
                {
                    File.WriteAllBytes(fileName, serializedOutput);
                }
                catch
                {
                    Flush();
                }
                hashTableChanged = false;
            }
        }

        private void Serialize()
        {
            System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter = new();
            using (MemoryStream ms = new())
            {
                formatter.Serialize(ms, playerPrefsHashtable);
                serializedOutput = ms.ToArray();
            }
        }

        private void Deserialize()
        {
            if (!File.Exists(fileName))
            {
                return;
            }
            try
            {
                byte[] readByte = File.ReadAllBytes(fileName);
                System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter = new();
                using (MemoryStream ms = new(readByte))
                {
                    ms.Position = 0;
                    playerPrefsHashtable = formatter.Deserialize(ms) as System.Collections.Hashtable;
                }
            }
            catch
            {
                Deserialize();
            }
        }
    }
}
```