当前位置: 首页 > news >正文

【unity框架开发12】从零手搓unity存档存储数据持久化系统,实现对存档的创建,获取,保存,加载,删除,缓存,加密,支持多存档

文章目录

  • 前言
  • 一、Unity对Json数据的操作
    • 方法一、JsonUtility
    • 方法二、Newtonsoft
  • 二、持久化的数据路径
  • 三、数据加密/解密
    • 加密方法
    • 解密方法
  • 四、条件编译指令限制仅在编辑器模式下进行加密/解密
  • 四、数据持久化管理器
    • 1、存档工具类
    • 2、一个存档数据
    • 3、存档系统数据类
    • 4、数据存档存储持久化管理器
    • 5、测试调用
  • 完结

前言

游戏存档不言而喻,是游戏设计中的重要元素,可以提高游戏的可玩性,为玩家提供更多的自由和控制权。

我们主要是实现对存档的创建,获取,保存,加载,删除,缓存,加密,支持多存档。存档有两类,一类是用户型存档,存储着某个游戏用户具体的信息,如血量,武器,游戏进度,一类是设置型存档,与任何用户存档都无关,是通用的存储信息,比如屏幕分辨率、音量设置等。

存档文件支持很多数据类似,这里我选择Json,可读性较强,易修改。

一、Unity对Json数据的操作

方法一、JsonUtility

它是Unty引擎中提供的一个用于序列化和反序列化JSON数据的工具类,该类提供了三个方法。

第一个函数ToJson可以将可序列化的对象转换为JSON字符串
第二个可选参数表示是否花费更多的性能将字符转换为更适合阅读的方式

//通过JS0N表示形式创建对象
string json = JsonUtility.ToJson(object obj, bool prettyPrint);

第二个函数FromJson可以将字符转换为对象
Playerlnfo是一个泛型参数,用于指定要将JSoN字符串转换为的对象类型

//生成对象的公共字段的JS0N表示形式
PlayerInfo playerInfo = JsonUtility.FromJson<PlayerInfo>(string json);

第三个函数则可以传入新的数据覆盖到已有对象中,而不是创建新的对象,在频繁读取数据时可以节省开销

//通过读取对象的JS0N表示形式覆盖其数据
JsonUtility.FromJsonOverwrite(string json, object objectTooverwrite);

方法二、Newtonsoft

前面使用JsonUtility进行json转化,遇到字典等无法序列化的数据是存不进去的,这里推荐使用Newtonsoft类库进行转化,可以很好解决这个问题。

安装插件支持
在这里插入图片描述

转换用户数据为JSON字符串

string jsonData = JsonConvert.SerializeObject(playerInfo);

将JSON字符串转换为unity对象数据

PlayerInfo playerInfo = JsonConvert.DeserializeObject<PlayerInfo>(jsonData);

二、持久化的数据路径

Application.persistentDataPath是一个持久化的数据路径,在不同的操作系统上,unity会为我们分配不同的持久化数据路径,这样可以确保应用程序在不同平台上都能正确保存和访问

//设置存档文件路径
savePath = Path.Combine(Application.persistentDataPath, "saveData.json");

不同平台存储的路径不一样,我们可以打印savePath查看自己的存储路径

比如我的就是
在这里插入图片描述

三、数据加密/解密

这里实现一个简单的加密和解密方法,使用了字符的异或(XOR)操作。

// 定义密钥字符
public static char[] keyChars = { 'a', 'b', 'c', 'd', 'e' };// 加密方法
public static string Encrypt(string data)
{char[] dataChars = data.ToCharArray();for (int i = 0; i < dataChars.Length; i++){char dataChar = dataChars[i];char keyChar = keyChars[i % keyChars.Length];// 重点: 通过亦或得到新的字符char newChar = (char)(dataChar ^ keyChar);dataChars[i] = newChar;}return new string(dataChars);
}// 解密方法
public static string Decrypt(string data)
{return Encrypt(data);
}

加密方法

  • Encrypt 方法接收一个字符串 data 作为输入。
  • 将输入字符串转换为字符数组 dataChars。
  • 使用一个 for 循环遍历每个字符:
    • dataChar 是当前字符。
    • keyChar 是从密钥字符数组中获取的字符,使用 i % keyChars.Length 确保循环使用密钥字符,即如果数据长度超过密钥长度,会重复使用密钥。
    • newChar 是通过 dataChar 和 keyChar 的异或操作得到的新字符。异或操作的特点是相同的字符异或结果为0,而不同的字符异或结果为1,因此可以用这个特性进行简单的加密。
      最后将加密后的字符数组重新转换为字符串并返回。

解密方法

  • Decrypt 方法实际上调用了 Encrypt 方法。由于异或操作的特性,调用两次相同的异或操作可以恢复原始数据。因此,加密和解密过程是相同的。

四、条件编译指令限制仅在编辑器模式下进行加密/解密

为了保证数据的可读性,我们可能不希望在编辑器模式下,数据还被加密了,这样会影响我们测试

我们可以使用#if UNITY_EDITOR 条件编译指令,用于在 Unity 中编写代码时进行特定的编译和执行控制。这段代码只会在 Unity 编辑器中编译和执行,而不会在构建后的游戏中包含。这意味着你可以在编辑器环境中添加一些特定的功能,比如自定义工具、调试信息等,而这些功能在最终发布的版本中不会出现。

示例用法

#if !UNITY_EDITOR// 放置加密/解密操作,这段代码不会在 Unity 编辑器中编译和执行#endif

四、数据持久化管理器

1、存档工具类

/// <summary>
/// 存档工具类
/// </summary>
public static class IOTool
{// 定义密钥字符public static char[] keyChars = { 'x', 'y', 'f', 'r', 'a', 'm', 'e' };/// <summary>/// 保存Json数据/// </summary>/// <param name="saveObject">保存的对象</param>/// <param name="path">路径</param>public static void SaveJson(object saveObject, string path){// string jsonData = JsonUtility.ToJson(saveObject);string jsonData = JsonConvert.SerializeObject(saveObject);
#if !UNITY_EDITORjsonData = Encrypt(jsonData);//加密
#endifFile.WriteAllText(path, jsonData);}/// <summary>/// 读取Json为指定的类型对象/// </summary>public static T LoadJson<T>(string path) where T : class{if (!File.Exists(path)){return null;}string jsonData = File.ReadAllText(path);
#if !UNITY_EDITORjsonData = Decrypt(jsonData);//解密
#endif// return JsonUtility.FromJson<T>(jsonData);return JsonConvert.DeserializeObject<T>(jsonData);}/// <summary>/// 加密方法/// </summary>public static string Encrypt(string data){char[] dataChars = data.ToCharArray();for (int i = 0; i < dataChars.Length; i++){char dataChar = dataChars[i];char keyChar = keyChars[i % keyChars.Length];// 重点: 通过亦或得到新的字符char newChar = (char)(dataChar ^ keyChar);dataChars[i] = newChar;}return new string(dataChars);}/// <summary>/// 解密方法/// </summary>public static string Decrypt(string data){return Encrypt(data);}
}

2、一个存档数据

/// <summary>
/// 一个存档数据
/// </summary>
[Serializable]
public class SaveItem
{// 存档IDpublic int saveID;// 私有字段,最后保存时间private DateTime lastSaveTime;// 公共属性,获取最后保存时间public DateTime LastSaveTime{get{// 如果 lastSaveTime 是默认值,则尝试将字符串解析为 DateTimeif (lastSaveTime == default(DateTime)){DateTime.TryParse(lastSaveTimeString, out lastSaveTime);}return lastSaveTime; // 返回最后保存时间}}// 用于持久化的字符串,Json不支持 DateTime 类型[SerializeField] private string lastSaveTimeString;// 构造函数,初始化存档ID和最后保存时间public SaveItem(int saveID, DateTime lastSaveTime){this.saveID = saveID; // 设置存档IDthis.lastSaveTime = lastSaveTime; // 设置最后保存时间lastSaveTimeString = lastSaveTime.ToString(); // 将 DateTime 转换为字符串}// 更新最后保存时间public void UpdateTime(DateTime lastSaveTime){this.lastSaveTime = lastSaveTime; // 更新最后保存时间lastSaveTimeString = lastSaveTime.ToString(); // 将新的 DateTime 转换为字符串}
}

3、存档系统数据类

/// <summary>
/// 存档系统数据类
/// </summary>
[Serializable]
public class SaveSystemData
{// 当前的存档IDpublic int currID = 0;// 所有存档的列表public List<SaveItem> saveItemList = new List<SaveItem>();
}

4、数据存档存储持久化管理器

/// <summary>
/// 数据存档存储持久化管理器
/// </summary>
public class SaveManager : Singleton<SaveManager>
{private SaveSystemData saveSystemData;//存档系统数据// 存档的保存private const string saveDirName = "saveData";// 设置的保存:1.全局数据的保存(分辨率、按键设置) 2.存档的设置保存。// 常规情况下,存档系统自行维护private const string settingDirName = "setting";// 存档文件夹路径private string saveDirPath = Application.persistentDataPath + "/" + saveDirName;private string settingDirPath = Application.persistentDataPath + "/" + settingDirName;// 存档中对象的缓存字典 // <存档ID,<文件名称,实际的对象>>private Dictionary<int, Dictionary<string, object>> cacheDic = new Dictionary<int, Dictionary<string, object>>();#region 初始化public SaveManager(){Debug.Log("存储管理器初始化成功");Init();}public void Init(){//检查路径并创建目录CheckAndCreateDir();// 初始化SaveSystemDataInitSaveSystemData();// 清除存档缓存CleanCache();}#endregion#region 保存、获取全局设置存档/// <summary>/// 加载设置,全局生效,不关乎任何一个存档/// </summary>public T LoadSetting<T>(string fileName) where T : class{return LoadFile<T>(settingDirPath + "/" + fileName);}/// <summary>/// 加载设置,全局生效,不关乎任何一个存档/// </summary>public T LoadSetting<T>() where T : class{return LoadSetting<T>(typeof(T).Name);}/// <summary>/// 保存设置,全局生效,不关乎任何一个存档/// </summary>public void SaveSetting(object saveObject, string fileName){SaveFile(saveObject, settingDirPath + "/" + fileName);}/// <summary>/// 保存设置,全局生效,不关乎任何一个存档/// </summary>public void SaveSetting(object saveObject){SaveSetting(saveObject, saveObject.GetType().Name);}/// <summary>/// 删除所有设置存档/// </summary>public void DeleteAllSetting(){if (Directory.Exists(settingDirPath)){// 直接删除目录Directory.Delete(settingDirPath, true);}CheckAndCreateDir();}#endregion#region 创建、获取、删除某一项用户存档/// <summary>/// 获取SaveItem/// </summary>public SaveItem GetSaveItem(int id){for (int i = 0; i < saveSystemData.saveItemList.Count; i++){if (saveSystemData.saveItemList[i].saveID == id){return saveSystemData.saveItemList[i];}}return null;}/// <summary>/// 获取SaveItem/// </summary>public SaveItem GetSaveItem(SaveItem saveItem){GetSaveItem(saveItem.saveID);return null;}/// <summary>/// 添加一个存档/// </summary>/// <returns></returns>public SaveItem CreateSaveItem(){SaveItem saveItem = new SaveItem(saveSystemData.currID, DateTime.Now);saveSystemData.saveItemList.Add(saveItem);saveSystemData.currID += 1;// 更新SaveSystemData 写入磁盘UpdateSaveSystemData();return saveItem;}/// <summary>/// 删除存档/// </summary>/// <param name="saveID">存档的ID</param>public void DeleteSaveItem(int saveID){string itemDir = GetSavePath(saveID, false);// 如果路径存在 且 有效if (itemDir != null){// 把这个存档下的文件递归删除Directory.Delete(itemDir, true);}saveSystemData.saveItemList.Remove(GetSaveItem(saveID));// 移除缓存RemoveCache(saveID);// 更新SaveSystemData 写入磁盘UpdateSaveSystemData();}/// <summary>/// 删除存档/// </summary>public void DeleteSaveItem(SaveItem saveItem){DeleteSaveItem(saveItem.saveID);}#endregion#region 保存、获取、删除用户存档中某一对象/// <summary>/// 保存对象至某个存档中/// </summary>/// <param name="saveObject">要保存的对象</param>/// <param name="saveFileName">保存的文件名称</param>/// <param name="saveID">存档的ID</param>public void SaveObject(object saveObject, string saveFileName, int saveID = 0){// 存档所在的文件夹路径string dirPath = GetSavePath(saveID, true);// 具体的对象要保存的路径string savePath = dirPath + "/" + saveFileName;// 具体的保存SaveFile(saveObject, savePath);// 更新存档时间GetSaveItem(saveID).UpdateTime(DateTime.Now);// 更新SaveSystemData 写入磁盘UpdateSaveSystemData();// 更新缓存SetCache(saveID, saveFileName, saveObject);}/// <summary>/// 保存对象至某个存档中/// </summary>/// <param name="saveObject">要保存的对象</param>/// <param name="saveFileName">保存的文件名称</param>public void SaveObject(object saveObject, string saveFileName, SaveItem saveItem){SaveObject(saveObject, saveFileName, saveItem.saveID);}/// <summary>/// 保存对象至某个存档中/// </summary>/// <param name="saveObject">要保存的对象</param>/// <param name="saveID">存档的ID</param>public void SaveObject(object saveObject, int saveID = 0){SaveObject(saveObject, saveObject.GetType().Name, saveID);}/// <summary>/// 保存对象至某个存档中/// </summary>/// <param name="saveObject">要保存的对象</param>/// <param name="saveID">存档的ID</param>public void SaveObject(object saveObject, SaveItem saveItem){SaveObject(saveObject, saveObject.GetType().Name, saveItem);}/// <summary>/// 从某个具体的存档中加载某个对象/// </summary>/// <typeparam name="T">要返回的实际类型</typeparam>/// <param name="saveFileName">文件名称</param>/// <param name="id">存档ID</param>public T LoadObject<T>(string saveFileName, int saveID = 0) where T : class{T obj = GetCache<T>(saveID, saveFileName);if (obj == null){// 存档所在的文件夹路径string dirPath = GetSavePath(saveID);if (dirPath == null) return null;// 具体的对象要保存的路径string savePath = dirPath + "/" + saveFileName;obj = LoadFile<T>(savePath);SetCache(saveID, saveFileName, obj);}return obj;}/// <summary>/// 从某个具体的存档中加载某个对象/// </summary>/// <typeparam name="T">要返回的实际类型</typeparam>/// <param name="saveFileName">文件名称</param>public T LoadObject<T>(string saveFileName, SaveItem saveItem) where T : class{return LoadObject<T>(saveFileName, saveItem.saveID);}/// <summary>/// 从某个具体的存档中加载某个对象/// </summary>/// <typeparam name="T">要返回的实际类型</typeparam>/// <param name="id">存档ID</param>public T LoadObject<T>(int saveID = 0) where T : class{return LoadObject<T>(typeof(T).Name, saveID);}/// <summary>/// 从某个具体的存档中加载某个对象/// </summary>/// <typeparam name="T">要返回的实际类型</typeparam>/// <param name="saveItem">存档项</param>public T LoadObject<T>(SaveItem saveItem) where T : class{return LoadObject<T>(typeof(T).Name, saveItem.saveID);}/// <summary>/// 删除某个存档中的某个对象/// </summary>/// <param name="saveID">存档的ID</param>public void DeleteObject<T>(string saveFileName, int saveID) where T : class{//清空缓存中对象if (GetCache<T>(saveID, saveFileName) != null){RemoveCache(saveID, saveFileName);}// 存档对象所在的文件路径string dirPath = GetSavePath(saveID);string savePath = dirPath + "/" + saveFileName;//删除对应的文件File.Delete(savePath);}/// <summary>/// 删除某个存档中的某个对象/// </summary>/// <param name="saveID">存档的ID</param>public void DeleteObject<T>(string saveFileName, SaveItem saveItem) where T : class{DeleteObject<T>(saveFileName, saveItem.saveID);}/// <summary>/// 删除某个存档中的某个对象/// </summary>/// <param name="saveID">存档的ID</param>public void DeleteObject<T>(int saveID) where T : class{DeleteObject<T>(typeof(T).Name, saveID);}/// <summary>/// 删除某个存档中的某个对象/// </summary>/// <param name="saveID">存档的ID</param>public void DeleteObject<T>(SaveItem saveItem) where T : class{DeleteObject<T>(typeof(T).Name, saveItem.saveID);}#endregion#region 获取、删除所有用户存档/// <summary>/// 获取所有存档/// 最新的在最后面/// </summary>/// <returns></returns>public List<SaveItem> GetAllSaveItem(){return saveSystemData.saveItemList;}/// <summary>/// 获取所有存档/// 创建最新的在最前面/// </summary>/// <returns></returns>public List<SaveItem> GetAllSaveItemByCreatTime(){List<SaveItem> saveItems = new List<SaveItem>(saveSystemData.saveItemList.Count);for (int i = 0; i < saveSystemData.saveItemList.Count; i++){saveItems.Add(saveSystemData.saveItemList[saveSystemData.saveItemList.Count - (i + 1)]);}return saveItems;}/// <summary>/// 获取所有存档/// 最新更新的在最上面/// </summary>/// <returns></returns>public List<SaveItem> GetAllSaveItemByUpdateTime(){List<SaveItem> saveItems = new List<SaveItem>(saveSystemData.saveItemList.Count);for (int i = 0; i < saveSystemData.saveItemList.Count; i++){saveItems.Add(saveSystemData.saveItemList[i]);}OrderByUpdateTimeComparer orderBy = new OrderByUpdateTimeComparer();saveItems.Sort(orderBy);return saveItems;}private class OrderByUpdateTimeComparer : IComparer<SaveItem>{public int Compare(SaveItem x, SaveItem y){if (x.LastSaveTime > y.LastSaveTime){return -1;}else{return 1;}}}/// <summary>/// 获取所有存档/// 万能解决方案/// </summary>public List<SaveItem> GetAllSaveItem<T>(Func<SaveItem, T> orderFunc, bool isDescending = false){if (isDescending){return saveSystemData.saveItemList.OrderByDescending(orderFunc).ToList();}else{return saveSystemData.saveItemList.OrderBy(orderFunc).ToList();}}/// <summary>/// 删除所有用户存档信息/// </summary>public void DeleteAllSaveItem(){if (Directory.Exists(saveDirPath)){            // 直接删除目录Directory.Delete(saveDirPath, true);}CheckAndCreateDir();InitSaveSystemData();}/// <summary>/// 删除所有存档信息/// </summary>public void DeleteAll(){CleanCache();DeleteAllSaveItem();DeleteAllSetting();}#endregion#region 更新、获取、删除用户存档缓存/// <summary>/// 设置缓存/// </summary>/// <param name="saveID">存档ID</param>/// <param name="fileName">文件名称</param>/// <param name="saveObject">要缓存的对象</param>private void SetCache(int saveID, string fileName, object saveObject){// 缓存字典中是否有这个SaveIDif (cacheDic.ContainsKey(saveID)){// 这个存档中有没有这个文件if (cacheDic[saveID].ContainsKey(fileName)){cacheDic[saveID][fileName] = saveObject;}else{cacheDic[saveID].Add(fileName, saveObject);}}else{cacheDic.Add(saveID, new Dictionary<string, object>() { { fileName, saveObject } });}}/// <summary>/// 获取缓存/// </summary>/// <param name="saveID">存档ID</param>/// <param name="saveObject">要缓存的对象</param>private T GetCache<T>(int saveID, string fileName) where T : class{// 缓存字典中是否有这个SaveIDif (cacheDic.ContainsKey(saveID)){// 这个存档中有没有这个文件if (cacheDic[saveID].ContainsKey(fileName)){return cacheDic[saveID][fileName] as T;}else{return null;}}else{return null;}}/// <summary>/// 移除缓存/// </summary>private void RemoveCache(int saveID){cacheDic.Remove(saveID);}/// <summary>/// 移除缓存中的某一个对象/// </summary>private void RemoveCache(int saveID, string fileName){cacheDic[saveID].Remove(fileName);}/// <summary>/// 清除存档缓存/// </summary>public void CleanCache(){cacheDic.Clear();}#endregion#region 内部工具函数/// <summary>/// 获取存档系统数据/// </summary>/// <returns></returns>private void InitSaveSystemData(){saveSystemData = LoadFile<SaveSystemData>(saveDirPath + "/SaveSystemData");if (saveSystemData == null){saveSystemData = new SaveSystemData();UpdateSaveSystemData();}}/// <summary>/// 更新存档系统数据/// </summary>private void UpdateSaveSystemData(){SaveFile(saveSystemData, saveDirPath + "/SaveSystemData");}/// <summary>/// 检查路径并创建目录/// </summary>private void CheckAndCreateDir(){// 确保路径的存在if (Directory.Exists(saveDirPath) == false){Directory.CreateDirectory(saveDirPath);}if (Directory.Exists(settingDirPath) == false){Directory.CreateDirectory(settingDirPath);}}/// <summary>/// 获取某个存档的路径/// </summary>/// <param name="saveID">存档ID</param>/// <param name="createDir">如果不存在这个路径,是否需要创建</param>/// <returns></returns>private string GetSavePath(int saveID, bool createDir = true){// 验证是否有某个存档if (GetSaveItem(saveID) == null) Debug.LogWarning("saveID 存档不存在!");string saveDir = saveDirPath + "/" + saveID;// 确定文件夹是否存在if (Directory.Exists(saveDir) == false){if (createDir){Directory.CreateDirectory(saveDir);}else{return null;}}return saveDir;}/// <summary>/// 保存文件/// </summary>/// <param name="saveObject">保存的对象</param>/// <param name="path">保存的路径</param>private void SaveFile(object saveObject, string path){IOTool.SaveJson(saveObject, path);}/// <summary>/// 加载文件/// </summary>/// <typeparam name="T">加载后要转为的类型</typeparam>/// <param name="path">加载路径</param>private T LoadFile<T>(string path) where T : class{return IOTool.LoadJson<T>(path);}#endregion
}

5、测试调用

// GameSetting类中存储着游戏名称,作为全局数据
[Serializable]
public class GameSetting
{public string gameName;
}[Serializable]
public class GameSetting2
{public string gameName;
}public class SaveTest : MonoBehaviour {GameSetting gameSetting;GameSetting2 gameSetting2;SaveItem saveItem;Dictionary<string, string> info = new Dictionary<string,string>();private void Awake() {//添加测试数据gameSetting = new GameSetting();gameSetting.gameName = "测试";gameSetting2 = new GameSetting2();gameSetting2.gameName = "测试2"; }private void OnGUI(){GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);   buttonStyle.fontSize = 25; // 设置字体大小int width = 400;int height = 150;//设置型存档 if (GUI.Button(new Rect(0, 0, width, height), "保存设置数据1", buttonStyle)){SaveManager.Instance.SaveSetting(gameSetting);}if (GUI.Button(new Rect(0, height, width, height), "追加设置数据2", buttonStyle)){SaveManager.Instance.SaveSetting(gameSetting2);}if (GUI.Button(new Rect(0, height*2, width, height), "加载设置数据1", buttonStyle)){string gameName = SaveManager.Instance.LoadSetting<GameSetting>().gameName;Debug.Log("gameName: " + gameName);}if (GUI.Button(new Rect(0, height*3, width, height), "删除设置存档", buttonStyle)){SaveManager.Instance.DeleteAllSetting();}//用户存档if (GUI.Button(new Rect(width, 0, width, height), "添加一个存档", buttonStyle)){saveItem = SaveManager.Instance.CreateSaveItem();}if (GUI.Button(new Rect(width, height, width, height), "保存用户存档数据1", buttonStyle)){info["name"] = "小明";info["age"] = "15";info["heright"] = "180";SaveManager.Instance.SaveObject(info, saveItem);}if (GUI.Button(new Rect(width, height*2, width, height), "追加数据2", buttonStyle)){SaveManager.Instance.SaveObject(gameSetting, saveItem);}if (GUI.Button(new Rect(width, height*3, width, height), "打印用户存档数据1", buttonStyle)){Dictionary<string, string> info = SaveManager.Instance.LoadObject<Dictionary<string, string>>(saveItem);Debug.Log("姓名:" + info["name"] + ",年龄:" + info["age"] + ",身高:" + info["heright"]);}if (GUI.Button(new Rect(width, height*4, width, height), "获取所有用户存档", buttonStyle)){SaveManager.Instance.GetAllSaveItem();//最新的在最后面SaveManager.Instance.GetAllSaveItemByCreatTime();//最近创建的在最前面SaveManager.Instance.GetAllSaveItem();//最近更新的在最前面}//删除用户存档if (GUI.Button(new Rect(width*2, 0, width, height), "删除用户存档数据1", buttonStyle)){SaveManager.Instance.DeleteObject<Dictionary<string, string>>(saveItem);}if (GUI.Button(new Rect(width*2, height, width, height), "删除所有用户存档", buttonStyle)){SaveManager.Instance.DeleteAllSaveItem();}if (GUI.Button(new Rect(width*2, height*2, width, height), "删除某一个用户存档", buttonStyle)){SaveManager.Instance.DeleteSaveItem(saveItem);}//删除所有存档 用户+设置if (GUI.Button(new Rect(width*3, 0, width, height), "删除所有存档", buttonStyle)){SaveManager.Instance.DeleteAll();}}
}

运行效果
在这里插入图片描述

SaveData和setting分别存储用户存档和设置型存档。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
用户存档下根据saveID分成若干文件夹用于存储具体的对象。

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇,https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

相关文章:

【unity框架开发12】从零手搓unity存档存储数据持久化系统,实现对存档的创建,获取,保存,加载,删除,缓存,加密,支持多存档

文章目录 前言一、Unity对Json数据的操作方法一、JsonUtility方法二、Newtonsoft 二、持久化的数据路径三、数据加密/解密加密方法解密方法 四、条件编译指令限制仅在编辑器模式下进行加密/解密四、数据持久化管理器1、存档工具类2、一个存档数据3、存档系统数据类4、数据存档存…...

YOLOv11进行图像与视频的目标检测

一、AI应用系统实战项目 项目名称项目名称1.人脸识别与管理系统2.车牌识别与管理系统...

SpinalHDL之错误集(一)

本文作为SpinalHDL学习笔记第七十六篇&#xff0c;作为错误集使用&#xff0c;类似高中生的错题集&#xff0c;记录使用SpinalHDL过程中遇到的问题&#xff0c;小到语法错误、版本兼容问题&#xff0c;大到SpinalHDL库函数错误等等&#xff0c;持续更新。 SpinalHDL学习笔记总…...

【arcgis】ArcGIS中如何避免标注压盖要素

ArcGIS中如何避免标注压盖要素 在制图工作中&#xff0c;标注&#xff08;Label&#xff09;是传达地理信息的重要方式。然而&#xff0c;在复杂的地图上&#xff0c;标注容易出现压盖要素的情况&#xff0c;影响地图的美观性和信息的准确传达。ArcGIS提供了Maplex标注引擎&am…...

数通--3

一、动态路由 内部 路由器之间要互联互通&#xff0c;必须遵循相同的协议 企业内部用 IGP&#xff0c;企业之间用BGP RIP&#xff08;已淘汰&#xff0c;不考&#xff09; 距离就是长短&#xff0c;矢量就是方向&#xff0c;即路由的出接口 一台路由器 A 配好RIP&#xff0c;…...

SpringBoot基础(五):集成JUnit5

SpringBoot基础系列文章 SpringBoot基础(一)&#xff1a;快速入门 SpringBoot基础(二)&#xff1a;配置文件详解 SpringBoot基础(三)&#xff1a;Logback日志 SpringBoot基础(四)&#xff1a;bean的多种加载方式 SpringBoot基础(五)&#xff1a;集成JUnit5 目录 一、JUnit…...

正点原子学习笔记之汇编LED驱动实验

1 汇编LED原理分析 为什么要写汇编     需要用汇编初始化一些SOC外设     使用汇编初始化DDR、I.MX6U不需要     设置sp指针&#xff0c;一般指向DDR&#xff0c;设置好C语言运行环境 1.1 LED硬件分析 可以看到LED灯一端接高电平&#xff0c;一端连接了GPIO_3上面…...

网络学习第二篇

认识网关和路由器 这里大家先了解一下什么三层设备。 三层设备 三层设备是指在网络架构中能够工作在第三层&#xff08;网络层&#xff09;的设备&#xff0c;通常包括三层交换机和路由器。这些设备可以根据IP地址进行数据包的转发和路由选择&#xff0c;从而在不同的网络之间…...

一些小结汇总

1.常用浏览器及对应内核 Safari浏览器&#xff08;苹果浏览器&#xff09;:Webkit内核 Google浏览器&#xff1a;Blink内核 Firefox&#xff1a;Gecko内核 Edge浏览器&#xff1a;Chromium内核 IE浏览器&#xff1a;Trident内核 2.10个块标签和10个行标签 …...

20240720 科大讯飞 笔试

文章目录 1、选择题1.11.21.31.41.51.61.71.81.91.101.111.122、编程题2.12.22.33、选择题(Linux 选做)3.13.23.34、选择题(C++ 选做)4.14.24.34.44.54.64.75、选择题(数据库选做)5.15.25.3岗位:嵌入式开发工程师 题型: 必做:12 道选择题,3 道编程题 选做:Linux(3…...

【AIGC】寻找ChatGPT最佳推理步骤:CoT思维链技术的探索与应用

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;CoT思维链概述&#x1f4af;CoT思维链在大型语言模型中的应用&#x1f4af;CoT思维链改变对模型推理能力的理解和改进方式多样化应用场景挑战与未来发展总结 &#x1f4a…...

一、安装VMWARE和CentOS

一、安装VMware 1、基本说明 学习Linux需要一个环境&#xff0c;我们需要创建一个虚拟机&#xff0c;然后在虚拟机上安装一个Centos系统来学习 先安装virtual machine 15.5再安装Linux(CentOS 7.6/centos8.1)原理示意图 2、vmware15.5下载 官方地址:https://www.vmware.com/…...

岩石分类检测数据集 4700张 岩石检测 带标注 voc yolo 9类

岩石分类检测数据集 4700张 岩石检测 带标注 voc yolo 9类 岩石分类检测数据集 (Rock Classification and Detection Dataset) 描述: 本数据集旨在支持对不同类型的岩石进行自动分类和检测&#xff0c;特别适用于地质勘探、矿物识别、环境监测等领域。通过使用该数据集训练的模…...

电脑基础知识:mfc110.dll丢失的解决方法

1.mfc110.dll 丢失常见原因 mfc110.dll 文件的丢失或损坏是Windows系统中常见的问题&#xff0c;它可能由多种原因引起&#xff0c;以下是一些主要的因素&#xff1a; 不完全的软件卸载 在卸载程序时&#xff0c;如果相关的 DLL 文件没有被正确移除&#xff0c;可能会导致文件…...

Lua 协同程序(coroutine)

Lua 协同程序(coroutine) 概述 Lua 语言以其轻量级和易于嵌入的特点,在游戏开发、脚本编写等领域广受欢迎。Lua 中的协同程序(coroutine)是其并发编程的核心特性之一。协同程序提供了一种不同于多线程的并发执行方式,它允许多个代码段交替执行,而不是同时执行。这种机制…...

NASA:ARCTAS 区域的二级 FIRSTLOOK 气溶胶产品子集。 它包含气溶胶光学深度和粒子类型,以及相关的大气数据

目录 简介 信息 代码 引用 网址推荐 知识星球 机器学习 MISR L2 FIRSTLOOK Aerosol Product subset for the ARCTAS region V001 简介 这是 ARCTAS 区域的二级 FIRSTLOOK 气溶胶产品子集。 它包含气溶胶光学深度和粒子类型&#xff0c;以及相关的大气数据&#xff0c;…...

go clean command

文章目录 1.简介2.格式3.选项4.示例5.应用场景6.小结参考文献 1.简介 在 Go 语言的开发过程中&#xff0c;管理依赖和构建缓存是非常重要的。随着项目的迭代&#xff0c;旧的缓存和不再需要的依赖可能会影响构建的效率和准确性。 Go 提供了一个非常实用的命令 go clean&#…...

鸿蒙NEXT开发-动画(基于最新api12稳定版)

注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注&#xff0c;博主会一直更新鸿蒙next相关知识 专栏地址: https://blog.csdn.net/qq_56760790/…...

HTML 入门

1. 什么是 HTML HTML&#xff08;Hyper Text Markup Language&#xff09;&#xff0c;超文本标记语言 超文本&#xff1a;比文本要强大&#xff0c;通过链接和交互方式来组织和呈现信息的文本形式&#xff0c;不仅有文本&#xff0c;还可能包含图片、音频、或者自己已经审阅…...

前端面试题(十五)

83. ES6 中的 let 和 const let 和 const 的区别是什么&#xff1f; let 和 const 是 ES6 引入的用于声明变量的新方式&#xff0c;相比于传统的 var&#xff0c;它们具有以下特性&#xff1a; 块级作用域&#xff1a;let 和 const 声明的变量在其所在的块级作用域内有效&…...

【杂谈】-递归进化:人工智能的自我改进与监管挑战

递归进化&#xff1a;人工智能的自我改进与监管挑战 文章目录 递归进化&#xff1a;人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管&#xff1f;3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...

智慧医疗能源事业线深度画像分析(上)

引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》

引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

线程同步:确保多线程程序的安全与高效!

全文目录&#xff1a; 开篇语前序前言第一部分&#xff1a;线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分&#xff1a;synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分&#xff…...

《通信之道——从微积分到 5G》读书总结

第1章 绪 论 1.1 这是一本什么样的书 通信技术&#xff0c;说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号&#xff08;调制&#xff09; 把信息从信号中抽取出来&am…...

srs linux

下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935&#xff0c;SRS管理页面端口是8080&#xff0c;可…...

Module Federation 和 Native Federation 的比较

前言 Module Federation 是 Webpack 5 引入的微前端架构方案&#xff0c;允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...

css的定位(position)详解:相对定位 绝对定位 固定定位

在 CSS 中&#xff0c;元素的定位通过 position 属性控制&#xff0c;共有 5 种定位模式&#xff1a;static&#xff08;静态定位&#xff09;、relative&#xff08;相对定位&#xff09;、absolute&#xff08;绝对定位&#xff09;、fixed&#xff08;固定定位&#xff09;和…...

工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配

AI3D视觉的工业赋能者 迁移科技成立于2017年&#xff0c;作为行业领先的3D工业相机及视觉系统供应商&#xff0c;累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成&#xff0c;通过稳定、易用、高回报的AI3D视觉系统&#xff0c;为汽车、新能源、金属制造等行…...

蓝桥杯3498 01串的熵

问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798&#xff0c; 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...