boxmoe_header_banner_img

菜就多练喵

文章导读

[C#][Untiy][Ib_Core]Ib_SaveAsync——同步/异步序列化存储系统


avatar
Ib_Mccf 2026年3月27日 23

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)

查看评论列表

暂无评论


发表评论

表情 颜文字

插入代码