boxmoe_header_banner_img

菜就多练喵

文章导读

[C#][Unity][Ib_Core]Ib_Save——同步序列化存储系统


avatar
Ib_Mccf 2026年3月27日 22

依赖:使用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)

查看评论列表

暂无评论


发表评论

表情 颜文字

插入代码