【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学习笔记第七十六篇,作为错误集使用,类似高中生的错题集,记录使用SpinalHDL过程中遇到的问题,小到语法错误、版本兼容问题,大到SpinalHDL库函数错误等等,持续更新。 SpinalHDL学习笔记总…...

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

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

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

正点原子学习笔记之汇编LED驱动实验
1 汇编LED原理分析 为什么要写汇编 需要用汇编初始化一些SOC外设 使用汇编初始化DDR、I.MX6U不需要 设置sp指针,一般指向DDR,设置好C语言运行环境 1.1 LED硬件分析 可以看到LED灯一端接高电平,一端连接了GPIO_3上面…...

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

一些小结汇总
1.常用浏览器及对应内核 Safari浏览器(苹果浏览器):Webkit内核 Google浏览器:Blink内核 Firefox:Gecko内核 Edge浏览器:Chromium内核 IE浏览器: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思维链技术的探索与应用
博客主页: [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 💯前言💯CoT思维链概述💯CoT思维链在大型语言模型中的应用💯CoT思维链改变对模型推理能力的理解和改进方式多样化应用场景挑战与未来发展总结 Ὂ…...

一、安装VMWARE和CentOS
一、安装VMware 1、基本说明 学习Linux需要一个环境,我们需要创建一个虚拟机,然后在虚拟机上安装一个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) 描述: 本数据集旨在支持对不同类型的岩石进行自动分类和检测,特别适用于地质勘探、矿物识别、环境监测等领域。通过使用该数据集训练的模…...

电脑基础知识:mfc110.dll丢失的解决方法
1.mfc110.dll 丢失常见原因 mfc110.dll 文件的丢失或损坏是Windows系统中常见的问题,它可能由多种原因引起,以下是一些主要的因素: 不完全的软件卸载 在卸载程序时,如果相关的 DLL 文件没有被正确移除,可能会导致文件…...

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

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

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

鸿蒙NEXT开发-动画(基于最新api12稳定版)
注意:博主有个鸿蒙专栏,里面从上到下有关于鸿蒙next的教学文档,大家感兴趣可以学习下 如果大家觉得博主文章写的好的话,可以点下关注,博主会一直更新鸿蒙next相关知识 专栏地址: https://blog.csdn.net/qq_56760790/…...

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

前端面试题(十五)
83. ES6 中的 let 和 const let 和 const 的区别是什么? let 和 const 是 ES6 引入的用于声明变量的新方式,相比于传统的 var,它们具有以下特性: 块级作用域:let 和 const 声明的变量在其所在的块级作用域内有效&…...

如何成为 Rust 核心贡献者?Rust 开发的核心是什么?Rust 重要技术专家揭秘
10 月 17 - 18日,由 GOSIM 开源创新汇主办、CSDN 承办的 GOSIM CHINA 2024 将在北京盛大启幕。作为 GOSIM 开源年度大会的第三届盛会,本次活动邀请了 60 多位国际开源专家,汇聚了来自全球百余家顶尖科技企业、知名高校及开源社区的技术大咖、…...

springboot + nacos + sofarpc 整合后报错403
springboot版本 2.2.2 nacos 版本 1.4.2 rpc-sofa-boot 版本 3.2.0 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.2.RELEASE</version></parent>…...

小米路由器R3Gv2安装openwrt记录
前言 小米路由器R3Gv2的硬件配置与小米路由器4A千兆版一致,但bootloader有所不同,因此openwrt的固件不要互刷。另外,R3Gv2和R3G、4A百兆版是不同的设备,切勿混淆。 硬件信息 OpenWrt参数页-Xiaomi MiWiFi 3G v2 CPU:…...

记录一下,android studio 登录不上github的问题
android studio 2023.3.1.18 版本的编译器,出现问题,之前连接过的项目可以正常提交和拉取到github。 但是新建立的项目无法上传到github,提示错误cannot load information for github.com/:request response;access to this site,…...

springcloud之基于github webhook动态刷新服务配置
前言 在实际开发中经常会有一个叫做配置中心的服务,这个服务经过变更参数来动态刷新线上业务数据行为配置。比如;行为开关、活动数据、黑白名单、本地/预发/线上环境切换等等,这些配置信息往往需要在我们不重启系统的时候就可以被更新执行。那…...

qt+opengl 实现纹理贴图,平移旋转,绘制三角形,方形
1 首先qt 已经封装了opengl,那么我们就可以直接用了,这里面有三个函数需要继承 virtual void initializeGL() override; virtual void resizeGL(int w,int h) override; virtual void paintGL() override; 这三个函数是实现opengl的重要函数。 2 我们…...

【动态规划】子数组系列(下)
1. 等差数列划分 413. 等差数列划分 状态表示:以 i 位置为结尾时的等差数列的个数 状态转移方程:由于至少需要三个元素才符合题目中等差数列的要求,所以需要判断 i - 2,i - 1,i 三个元素,当这三个元素符合…...

macos mendeley Unable to install the Microsoft Word Plugin 解决
windows也是相似的原理,这里主要说macos, 本质是 找到mendeley的插件启动项,放在word启动目录下, GPT-o1的解决方案: 3. Manual Installation (If Automatic Installation Fails) If the automatic installation doe…...

【Linux进程间通信】Linux信号机制深度解析:保存与处理技巧
📝个人主页🌹:Eternity._ ⏩收录专栏⏪:Linux “ 登神长阶 ” 🌹🌹期待您的关注 🌹🌹 ❀Linux进程间通信 📒1. 信号的保存🌊在内核中的表示🍂sigs…...

常见开源组件的详解
文章目录 RPCRPC架构和工作流程为什么有了HTTP还要用RPC底层协议数据格式连接管理错误处理 使用场景常见的RPC框架 Web应用框架主要功能常见的Web应用框架Spring Boot (Java)Django (Python)Express.js (Node.js) Redis主要特点应用场景缓存问题Redis集群架构主从复制Redis Clu…...