UniTask项目链接:https://github.com/Cysharp/UniTask
特点:保留Ib_Save同步存储功能,增加基于UniTask的异步存储。
依赖:Newton soft.Json(序列化),UniTask(异步),Ib_Event(事件通信),Ib_Async(延迟调用),Ib_Log(日志输出),Ib_Singleton(泛型单例)
在Ib_Save同步序列化存储系统的基础上,基于UniTask实现异步存储功能,提供SaveAsync和LoadAsync方法,需要传入CancellationToken取消令牌,用于控制异步的生命周期,如不需要则直接传入CancellationToken.None即可。
注意事项:请确保你的工程导入了UniTask,并在PlayerSetting中加入UNITASK_SUPPORT的宏定义,否则#if UNITASK_SUPPORT … #endif部分不会被编译

源码(实现):
using Cysharp.Threading.Tasks;
namespace Ib_Core.Ib_Save
{
using System.Threading;
using Ib_Core;
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> { }
#if UNITASK_SUPPORT
public class OnSaveAsync : Ib_Event<OnSaveAsync,CancellationToken> { }
public class OnLoadAsync : Ib_Event<OnLoadAsync,CancellationToken> { }
#endif
public interface ISaveData
{
}
public interface ISaveable
{
//基于Ib_Event,延迟一帧注册。保证SaveSystem在注册前完成初始化
public void Register(CancellationToken ct) => OnRegisterSaveable.InvokeNextFrame(this, ct);
public void Deregister(CancellationToken ct) => OnDeregisterSaveable.InvokeNextFrame(this, ct);
/// <summary>
/// 存档ID,唯一标识存档
/// </summary>
public string SaveID { get; set; }
/// <summary>
/// 存档优先级,数值越大越优先存档
/// </summary>
public int SavePriority { get; set; }
ISaveData GenerateSaveData();
public void LoadSaveData(ISaveData saveData);
#if UNITASK_SUPPORT
public virtual UniTask<ISaveData> GenerateSaveDataAsync(CancellationToken cts)
=> UniTask.FromResult(GenerateSaveData());
public virtual UniTask LoadSaveDataAsync(ISaveData saveData,CancellationToken cts)
{
LoadSaveData(saveData);
return UniTask.CompletedTask;
}
#endif
}
/// <summary>
/// 存档系统扩展方法
/// </summary>
public static class Ib_SaveExtension
{
/// <summary>
/// 注册到存档系统
/// </summary>
/// <param name="saveable"></param>
/// <param name="cts">取消令牌</param>
public static void Register2SaveSystem(this ISaveable saveable, CancellationToken cts = default) => saveable.Register(cts);
/// <summary>
/// 从存档系统注销
/// </summary>
/// <param name="saveable"></param>
/// <param name="cts">取消令牌</param>
public static void Deregister2SaveSystem(this ISaveable saveable, CancellationToken cts = default) => saveable.Deregister(cts);
}
}
存储模块
using System.Threading;
using Cysharp.Threading.Tasks;
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);
#if UNITASK_SUPPORT
public UniTask WriteAsync(string resultPath,string data,CancellationToken cts);
public UniTask<string> ReadAsync(string resultPath,CancellationToken cts);
#endif
}
public enum ESaveType
{
Local,PlayerPrefs
}
}
using System.Threading;
using Cysharp.Threading.Tasks;
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;
}
#if UNITASK_SUPPORT
public async UniTask WriteAsync(string resultPath, string data,CancellationToken cts)
{
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";
//先把新数据写入临时文件
await File.WriteAllTextAsync(tempPath, data,cts);
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 async UniTask<string> ReadAsync(string resultPath,CancellationToken cts)
{
if (File.Exists(resultPath))
{
return await File.ReadAllTextAsync(resultPath,cts);
}
Ib_Log.Warning($"存档文件不存在: {resultPath}");
return string.Empty;
}
#endif
}
public class PlayerPrefsDataStorage : IDataStorage
{
public void Write(string resultPath, string data)
{
PlayerPrefs.SetString(resultPath, data);
}
public string Read(string resultPath)
{
return PlayerPrefs.GetString(resultPath, "");
}
#if UNITASK_SUPPORT
public async UniTask WriteAsync(string resultPath, string data,CancellationToken cts)
{
await UniTask.Yield(cts);
Write(resultPath, data);
}
public async UniTask<string> ReadAsync(string resultPath,CancellationToken cts)
{
await UniTask.Yield(cts);
return Read(resultPath);
}
#endif
}
#endregion
}
加密模块
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 System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
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>();
public bool IsBusy { get; private set; } = false;
#region 事件订阅
protected override void Awake()
{
base.Awake();
ChangeJsonFolder(Application.persistentDataPath +"/Data/");
OnRegisterSaveable.Register(Register);
OnDeregisterSaveable.Register(Deregister);
OnSave.Register(Save);
OnLoad.Register(Load);
#if UNITASK_SUPPORT
OnSaveAsync.Register(ExecuteSaveAsync);
OnLoadAsync.Register(ExecuteLoadAsync);
#endif
}
protected override void OnDestroy()
{
base.OnDestroy();
OnRegisterSaveable.Deregister(Register);
OnDeregisterSaveable.Deregister(Deregister);
OnSave.Deregister(Save);
OnLoad.Deregister(Load);
#if UNITASK_SUPPORT
OnSaveAsync.Deregister(ExecuteSaveAsync);
OnLoadAsync.Deregister(ExecuteLoadAsync);
#endif
}
private void Register(ISaveable saveable) => saveableList.Add(saveable);
private void Deregister(ISaveable saveable) => saveableList.Remove(saveable);
#endregion
#region 存储与载入
/// <summary>
/// 存储数据
/// </summary>
public void Save()
{
if(IsBusy)return;
IsBusy = true;
try
{
dataDict.Clear();
//save时不排序,保持原顺序,如有需求可改为排序
foreach (var saveable in saveableList)
{
if (dataDict.ContainsKey(saveable.SaveID))
{
Ib_Log.Error(saveable.SaveID + " 重复存储");
continue;
}
Ib_Log.Info($"正在存储{saveable.SaveID}");
dataDict.Add(saveable.SaveID, saveable.GenerateSaveData());
}
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}");
}
catch (System.Exception ex)
{
Ib_Log.Error($"存储过程中发生崩溃: {ex.Message}");
}
finally
{
IsBusy = false;
}
}
/// <summary>
/// 载入数据
/// </summary>
public void Load()
{
IsBusy = true;
try
{
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}载入");
}
catch (System.Exception ex)
{
Ib_Log.Error($"载入过程中发生崩溃: {ex.Message}");
}
finally
{
IsBusy = false;
}
}
#if UNITASK_SUPPORT
private void ExecuteSaveAsync(CancellationToken cts) => SaveAsync(cts).Forget();
private void ExecuteLoadAsync(CancellationToken cts) => LoadAsync(cts).Forget();
/// <summary>
/// 异步存储数据
/// </summary>
/// <param name="cts">取消令牌</param>
public async UniTask SaveAsync(CancellationToken cts)
{
if (IsBusy) return;
IsBusy = true;
try
{
dataDict.Clear();
//save时不排序,保持原顺序,如有需求可改为排序
var tasks = saveableList.Select(async s =>
{
var data = await s.GenerateSaveDataAsync(cts);
return (s.SaveID, data);
});
var results = await UniTask.WhenAll(tasks);
foreach (var (id, data) in results)
{
if (!dataDict.TryAdd(id, data))
{
Ib_Log.Error(id + " 重复存储");
continue;
}
}
var jsonData = await UniTask.RunOnThreadPool(() =>
{
string json = JsonConvert.SerializeObject(dataDict, Formatting.Indented, jsonSerializerSettings);
return Ib_EncryptionFactory.CreateEncryptor(eEncryptionType).Encrypt(json, key);
},true,cts);
await Ib_StorageFactory.CreateDataStorage(eSaveType).WriteAsync(ResultPath, jsonData,cts);
Ib_Log.Info($"异步数据存储成功.存储至{ResultPath}");
}
catch (System.Exception ex)
{
Ib_Log.Error($"异步存储过程中发生崩溃: {ex.Message}");
}
finally
{
IsBusy = false; // 无论成功失败,必须释放锁
}
}
/// <summary>
/// 异步载入数据
/// </summary>
/// <param name="cts">取消令牌</param>
public async UniTask LoadAsync(CancellationToken cts)
{
if (IsBusy) return;
IsBusy = true;
try
{
string stringData = await Ib_StorageFactory.CreateDataStorage(eSaveType).ReadAsync(ResultPath, cts);
if (string.IsNullOrEmpty(stringData))
{
Ib_Log.Error("未找到存档文件或存档为空!");
return;
}
// 在后台线程解密和反序列化
var jsonData = await UniTask.RunOnThreadPool(() =>
{
string decrypted = Ib_EncryptionFactory.CreateEncryptor(eEncryptionType).Decrypt(stringData, key);
return JsonConvert.DeserializeObject<Dictionary<string, ISaveData>>(decrypted, jsonSerializerSettings);
},true,cts);
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))
{
await saveable.LoadSaveDataAsync(saveData,cts);
}
}
Ib_Log.Info($"异步数据载入成功.从{ResultPath}载入");
}
catch (System.Exception ex)
{
Ib_Log.Error($"异步载入过程中发生崩溃: {ex.Message}");
}
finally
{
IsBusy = false; // 释放锁
}
}
#endif
#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();
}
[MenuItem("Ib_Tools/Ib_Save/SaveDataAsync")]
public static void SaveDataAsync()
{
if (!Application.isPlaying)
{
Ib_Log.Warning("请在游戏运行(Play)模式下使用存档功能!");
return;
}
OnSaveAsync.Invoke(CancellationToken.None);
}
[MenuItem("Ib_Tools/Ib_Save/LoadDataAsync")]
public static void LoadDataAsync()
{
if (!Application.isPlaying)
{
Ib_Log.Warning("请在游戏运行(Play)模式下使用存档功能!");
return;
}
OnLoadAsync.Invoke(CancellationToken.None);
}
}
评论(0)
暂无评论