LoginSignup
3
1

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

Last updated at Posted at 2024-03-26

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

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


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

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

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

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


配布先

https://booth.pm/ja/items/5330615

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

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

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


SephirothSaveReducedVersion の使用方法

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

一部抜粋
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> 型も保存できます。

一部抜粋
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 ミリ秒なので
速度は早い方だと自負しています。

一部抜粋
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 を作成して
中身をこの内容にして頂いても
動作します。

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();
            }
        }
    }
}
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1