
依赖:使用Newtonsoft.Json序列化数据(核心),Ib_Event进行事件通信(可以直接使用单例调用代替),Ib_Singleton泛型单例(可自行实现),Ib_Log打印日志(可替换为Debug.Log)
特点:
- 使用工厂模式拆分读写模块和加密模块,提供高拓展性,符合开闭原则。
- 使用Ib_Event事件通信,逻辑解耦。
- 使用接口(Interface)接收序列化对象和数据,并完美解决多态序列化问题,提高泛用性和可拓展性。
功能:序列化游戏数据,提供读写方法,为游戏全量存档提供解决方案。
更新记录:
2025-3-27 Ib_SaveAsync已更新,在Ib_Save的基础上提供同步/异步序列化存储解决方案。
示例:
存储Sample_Ib_Save对象的数据
Sample_Ib_Save对象需要继承并实现ISaveable接口,并实现一个继承ISaveData的数据类SampleSaveData用于序列化存储。
在实际游戏开发流程中,Sample_Ib_Save(ISaveable)可以是我们的玩家类,SampleSaveData(ISaveData)可以是角色的属性集合。
*注意事项:示例中在Start中使用Guid.NewGuid().ToString();作为唯一的SaveID,由对象自己生成了SaveID,这样的缺点是每次运行它的SaveID是不一样的,导致无法读取原存储的数据,这里这种写法仅供演示。
在正常开发流程中,若需要存储的对象是唯一的,如管理器,可以用类的Type作为唯一ID,保证每次运行,它的SaveID都是恒定的。对于其他的如角色,可以由唯一管理器存储管理他们的SaveID,为他们分配并存储,这样每次运行,所有对象的SaveID都能保证不变。
using System;
using System.Collections.Generic;
using Ib_Core;
using Ib_Core.Ib_Save;
using Sirenix.OdinInspector;
using UnityEngine;
/// <summary>
/// 示例存档数据,声明需要存储的字段,并继承ISaveData接口
/// </summary>
public class SampleSaveData : ISaveData
{
public int SampleValue;
public string SampleString;
public List<int> SampleList;
public Dictionary<string, int> SampleDict;
}
/// <summary>
/// 需要存储数据的对象,实现ISaveable接口
/// </summary>
public class Sample_Ib_Save : MonoBehaviour, ISaveable
{
#region 需要存储的字段
[SerializeField] private int sampleValue;
[SerializeField] private string sampleString;
[SerializeField] private List<int> sampleList;
private Dictionary<string, int> _sampleDict = new Dictionary<string, int>();
#endregion
private void Start()
{
//生成唯一ID
SaveID = Guid.NewGuid().ToString();
//设置存档优先级
SavePriority = 0;
_sampleDict.Add("SampleDictKey", 1);
_sampleDict.Add("SampleDictKey2", 2);
//注册到存档系统
this.Register2SaveSystem();
}
private void OnDestroy()
{
//从存档系统中注销,若不注销会内存泄漏!!!
this.Deregister2SaveSystem();
}
#region 实现ISaveable接口
public string SaveID { get; set; }
public int SavePriority{ get; set; }
public ISaveData GenerateSaveData()
{
SampleSaveData saveData = new SampleSaveData();
saveData.SampleValue = sampleValue;
saveData.SampleString = sampleString;
saveData.SampleList = sampleList;
saveData.SampleDict = _sampleDict;
return saveData;
}
public void LoadSaveData(ISaveData saveData)
{
if(saveData is not SampleSaveData sampleSaveData) return;
//载入数据
sampleValue = sampleSaveData.SampleValue;
sampleString = sampleSaveData.SampleString;
sampleList = sampleSaveData.SampleList;
_sampleDict = sampleSaveData.SampleDict;
}
#endregion
#region 测试
//改变字典中的值
[Button]
public void ChangeSampleDict()
{
_sampleDict["SampleDictKey"] = 114514;
_sampleDict["SampleDictKey2"] = 114514;
}
//打印字典中的值
[Button]
public void PrintSampleDict()
{
foreach (var item in _sampleDict)
Ib_Log.Info($"{item.Key} : {item.Value}");
}
#endregion
}
首先,保证场景中有我们的SaveSystem,演示中,我们不对序列化的数据进行异或加密,将加密类型EEncryptionType设为None。

然后,创建我们需要存储数据的对象,挂载Sample_Ib_Save脚本,为其数据赋值。

运行!然后调用OnSave.Invoke()事件,告知SaveSystem进行存储。
我们这边方便测试,直接使用工具类进行Save和Load
/// <summary>
/// 工具类
/// </summary>
public class Ib_Tools
{
[MenuItem("Ib_Tools/Ib_Save/SaveData")]
public static void SaveData()
{
if (!Application.isPlaying)
{
Ib_Log.Warning("请在游戏运行(Play)模式下使用存档功能!");
return;
}
OnSave.Invoke();
}
[MenuItem("Ib_Tools/Ib_Save/LoadData")]
public static void LoadData()
{
if (!Application.isPlaying)
{
Ib_Log.Warning("请在游戏运行(Play)模式下使用存档功能!");
return;
}
OnLoad.Invoke();
}
}

点击SaveData,Console中即可看到存储信息,我们可以到目标文件夹查看存储的文件


使用记事本或IDE打开后,可以看到我们的两个SampleSaveData的Json数据,我们的字段都被序列化保存了。

这时,不要急着停止运行,我们试着将Inspector中的数据修改。
我们改了Int的数值、字符串、List的元素数量甚至是Dictionary的元素值。

这时我们调用OnLoad.Invoke()尝试去加载之前存储的数据。
可以看到我们的数据恢复了,字典的元素值恢复了,我们的读写就此完成。

在正常的开发流程中,我们可以有更多的ISaveable对象和ISaveData数据,我们只需要照葫芦画瓢,即可实现游戏数据的全量存档,如果需要多个存档位置,只需要在Save和Load的时候传入存档名称即可!拓展文件的创建和删除逻辑,这就是一个可行的存档解决方案。
*注:若需要存储的数据量极大,可考虑异步存储,避免游戏运行时卡顿。
这是异或加密后的存储内容,若想要保护游戏数据不被轻易篡改,可以考虑拓展加密方式。

实现(源码):
ISaveable和ISaveData接口
namespace Ib_Core.Ib_Save
{
using System.Threading;
using Ib_Core;
//声明注册、存储事件,依赖Ib_Event
public class OnRegisterSaveable : Ib_Event<OnRegisterSaveable, ISaveable> { }
public class OnDeregisterSaveable : Ib_Event<OnDeregisterSaveable, ISaveable> { }
public class OnSave : Ib_Event<OnSave> { }
public class OnLoad : Ib_Event<OnLoad> { }
public interface ISaveData
{
}
public interface ISaveable
{
public void Register() => OnRegisterSaveable.InvokeNextFrame(this,CancellationToken.None);
public void Deregister() => OnDeregisterSaveable.InvokeNextFrame(this,CancellationToken.None);
/// <summary>
/// 存档ID,唯一标识存档
/// </summary>
public string SaveID { get; set; }
/// <summary>
/// 存档优先级,数值越大越优先存档
/// </summary>
public int SavePriority { get; set; }
ISaveData GenerateSaveData();
public void LoadSaveData(ISaveData saveData);
}
/// <summary>
/// 存档系统扩展方法
/// </summary>
public static class Ib_SaveExtension
{
/// <summary>
/// 注册到存档系统
/// </summary>
/// <param name="saveable"></param>
public static void Register2SaveSystem(this ISaveable saveable) => saveable.Register();
/// <summary>
/// 从存档系统注销
/// </summary>
/// <param name="saveable"></param>
public static void Deregister2SaveSystem(this ISaveable saveable) => saveable.Deregister();
}
}
DataStorage数据存储模块
namespace Ib_Core.Ib_Save
{
public interface IDataStorage
{
/// <summary>
/// 写入数据
/// </summary>
/// <param name="resultPath">目标路径</param>
/// <param name="data">数据</param>
public void Write(string resultPath,string data);
/// <summary>
/// 读取存储数据
/// </summary>
/// <param name="resultPath">目标路径</param>
/// <returns></returns>
public string Read(string resultPath);
}
public enum ESaveType
{
Local,PlayerPrefs
}
}
namespace Ib_Core.Ib_Save
{
using UnityEngine;
using System.IO;
using Ib_Core;
public class Ib_StorageFactory
{
public static IDataStorage CreateDataStorage(ESaveType eSaveType)
{
switch (eSaveType)
{
case ESaveType.Local:
return new JsonDataStorage();
case ESaveType.PlayerPrefs:
return new PlayerPrefsDataStorage();
}
return new JsonDataStorage();
}
}
#region 存储器实现
public class JsonDataStorage : IDataStorage
{
public void Write(string resultPath, string data)
{
try
{
string directory = Path.GetDirectoryName(resultPath);
if (string.IsNullOrEmpty(directory))
{
Ib_Log.Error($"存档写入失败!路径: {resultPath}\n原因: 目录为空");
return;
}
// 如果文件夹不存在,则自动创建该文件夹(及其所有父文件夹)
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
//先将数据写入临时文件,防止写入失败破坏原有数据
//临时文件和备份
string tempPath = resultPath + ".tmp";
string backupPath = resultPath + ".bak";
//先把新数据写入临时文件
File.WriteAllText(tempPath, data);
if (File.Exists(resultPath))
{
// 如果正式存档已存在,执行替换并生成备份
File.Replace(tempPath, resultPath, backupPath);
}
else
{
// 如果是第一次存档,正式文件还不存在,直接把临时文件改名即可
File.Move(tempPath, resultPath);
}
// (可选) 删除旧的备份
// File.Delete(backupPath);
}
catch (System.Exception ex)
{
Ib_Log.Error($"存档写入失败!路径: {resultPath}\n原因: {ex.Message}");
}
}
public string Read(string resultPath)
{
if (File.Exists(resultPath))
{
return File.ReadAllText(resultPath);
}
Ib_Log.Warning($"存档文件不存在: {resultPath}");
return string.Empty;
}
}
public class PlayerPrefsDataStorage : IDataStorage
{
public void Write(string resultPath, string data)
{
PlayerPrefs.SetString(resultPath, data);
}
public string Read(string resultPath)
{
return PlayerPrefs.GetString(resultPath, "");
}
}
#endregion
}
Encryption加密模块
namespace Ib_Core.Ib_Save
{
public interface IEncryption
{
/// <summary>
/// 加密字符串
/// </summary>
/// <param name="textToEncrypt"></param>
/// <param name="key"></param>
/// <returns></returns>
public string Encrypt(string textToEncrypt,string key);
/// <summary>
/// 解密字符串
/// </summary>
/// <param name="textToDecrypt"></param>
/// <param name="key"></param>
/// <returns></returns>
public string Decrypt(string textToDecrypt, string key);
}
public enum EEncryptionType
{
None,XOR
}
}
namespace Ib_Core.Ib_Save
{
using System.Text;
public class Ib_EncryptionFactory
{
public static IEncryption CreateEncryptor(EEncryptionType type)
{
switch (type)
{
case EEncryptionType.XOR:
return new XorEncryption();
case EEncryptionType.None:
default:
return new DummyEncryptor(); // 不做任何加密的实现
}
}
}
#region 加密器的实现
public class DummyEncryptor : IEncryption
{
public string Encrypt(string textToEncrypt, string key = null) => textToEncrypt;
public string Decrypt(string textToDecrypt, string key = null) => textToDecrypt;
}
public class XorEncryption : IEncryption
{
public string Encrypt(string textToEncrypt, string key)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < textToEncrypt.Length; i++)
{
char c = (char)(textToEncrypt[i] ^ key[i % key.Length]);
sb.Append(c);
}
return sb.ToString();
}
public string Decrypt(string textToDecrypt, string key)
{
return Encrypt(textToDecrypt, key);
}
}
#endregion
}
SaveSystem
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Newtonsoft.Json;
using Ib_Core;
using Ib_Core.Ib_Controller;
using Ib_Core.Ib_Save;
using UnityEditor;
public class SaveSystem : Ib_Singleton<SaveSystem>
{
#region SaveConfig
public ESaveType eSaveType = ESaveType.Local;
public EEncryptionType eEncryptionType = EEncryptionType.None;
#endregion
#region FileConfig
private const string key = "Hello World!";
private const string fileName = "data.sav";
private string jsonFolder;
private string ResultPath => Path.Combine(jsonFolder, fileName);
/// <summary>
/// 更改存储文件路径
/// </summary>
/// <param name="folder"></param>
public void ChangeJsonFolder(string folder)
{
jsonFolder = folder;
}
#endregion
private JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
{
//加上后才可以序列化接口
TypeNameHandling = TypeNameHandling.Auto
};
private HashSet<ISaveable> saveableList = new();
private List<ISaveable> saveablePriorityList = new();
private Dictionary<string, ISaveData> dataDict = new Dictionary<string, ISaveData>();
#region 事件订阅
protected override void Awake()
{
base.Awake();
ChangeJsonFolder(Application.persistentDataPath +"/Data/");
OnRegisterSaveable.Register(Register);
OnDeregisterSaveable.Register(Deregister);
OnSave.Register(Save);
OnLoad.Register(Load);
}
protected override void OnDestroy()
{
base.OnDestroy();
OnRegisterSaveable.Deregister(Register);
OnDeregisterSaveable.Deregister(Deregister);
OnSave.Deregister(Save);
OnLoad.Deregister(Load);
}
private void Register(ISaveable saveable) => saveableList.Add(saveable);
private void Deregister(ISaveable saveable) => saveableList.Remove(saveable);
#endregion
#region 存储与载入
/// <summary>
/// 存储数据
/// </summary>
public void Save()
{
dataDict.Clear();
foreach (var saveable in saveableList)
{
Ib_Log.Info($"正在存储{saveable.SaveID}");
if(!dataDict.TryAdd(saveable.SaveID, saveable.GenerateSaveData()))
{
Ib_Log.Error(saveable.SaveID + "重复存储");
}
}
var jsonData = JsonConvert.SerializeObject(dataDict, Formatting.Indented,jsonSerializerSettings);
jsonData = Ib_EncryptionFactory.CreateEncryptor(eEncryptionType).Encrypt(jsonData, key);
Ib_StorageFactory.CreateDataStorage(eSaveType).Write(ResultPath, jsonData);
Ib_Log.Info($"数据存储成功.存储至{ResultPath}");
}
/// <summary>
/// 载入数据
/// </summary>
public void Load()
{
string stringData = Ib_StorageFactory.CreateDataStorage(eSaveType).Read(ResultPath);
if(string.IsNullOrEmpty(stringData))
{
Ib_Log.Error("未找到存档文件或存档为空!");
return;
}
stringData = Ib_EncryptionFactory.CreateEncryptor(eEncryptionType).Decrypt(stringData, key);
if (string.IsNullOrEmpty(stringData))
{
Ib_Log.Error("无存档");
return;
}
var jsonData = JsonConvert.DeserializeObject<Dictionary<string, ISaveData>>(stringData,jsonSerializerSettings);
if (jsonData == null)
{
Ib_Log.Error("存档数据损坏或解析失败!");
return;
}
saveablePriorityList.Clear();
saveablePriorityList.AddRange(saveableList);
saveablePriorityList.Sort((a, b) => b.SavePriority.CompareTo(a.SavePriority));
foreach (var saveable in saveablePriorityList)
{
if(!jsonData.TryGetValue(saveable.SaveID, out var saveData)) continue;
saveable.LoadSaveData(saveData);
}
Ib_Log.Info($"数据载入成功.从{ResultPath}载入");
}
#endregion
}
/// <summary>
/// 工具类
/// </summary>
public class Ib_Tools
{
[MenuItem("Ib_Tools/Ib_Save/SaveData")]
public static void SaveData()
{
if (!Application.isPlaying)
{
Ib_Log.Warning("请在游戏运行(Play)模式下使用存档功能!");
return;
}
OnSave.Invoke();
}
[MenuItem("Ib_Tools/Ib_Save/LoadData")]
public static void LoadData()
{
if (!Application.isPlaying)
{
Ib_Log.Warning("请在游戏运行(Play)模式下使用存档功能!");
return;
}
OnLoad.Invoke();
}
}
评论(0)
暂无评论