今回のテーマは
「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 を作成して
中身をこの内容にして頂いても
動作します。
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();
}
}
}
}