
1. 项目缘起为什么Unity存档系统值得你投入精力做Unity开发的朋友尤其是独立开发者或者中小团队的技术负责人应该都经历过存档这个“小功能”带来的“大麻烦”。项目初期你可能随手写个PlayerPrefs把金币、等级存一下觉得够用了。但随着项目推进你会发现需要保存的数据越来越多复杂的角色属性、背包里几十上百种物品、任务进度、地图探索状态、甚至是整个场景中可交互物体的实时数据。这时候PlayerPrefs的局限性就暴露无遗存储容量小、数据结构扁平、安全性为零玩家用记事本就能改、跨平台兼容性也可能出问题。更头疼的是数据安全。我见过不少游戏因为存档是明文存储被玩家轻易修改导致经济系统崩坏或者排行榜被作弊数据刷屏。这对于任何一款希望长期运营的游戏来说都是致命的。所以一个健壮、安全、可扩展的存档系统绝不是锦上添花而是项目地基的一部分。基于这些痛点我决定自己动手构建一个在Unity中通用的进阶存档系统。它的核心目标很明确第一要能灵活序列化复杂的C#对象为文本方便存储和调试第二要对存档数据进行加密防止轻易篡改第三要易于集成和使用不能给日常开发带来太大负担。最终我选择了Json序列化作为数据交换格式用AES对称加密来保障数据安全。这套方案经过多个项目的实战检验稳定可靠今天就把其中的设计思路、关键实现和踩过的坑毫无保留地分享出来。2. 核心架构设计从数据对象到加密字节流在动手写代码之前我们先要把数据流转的整个链路想清楚。一个完整的存档过程本质上是将内存中的游戏状态一个个C#对象转化为可以安全持久化存储的字节流反之亦然。这个过程可以清晰地分为几个层次。2.1 数据模型层定义你要存什么这是整个系统的基石。你需要用普通的C#类Class来定义你的游戏数据。这里有一个非常重要的原则这些数据类必须是纯粹的“数据容器”Data Container。它们只包含字段Fields或属性Properties不包含或尽量少包含游戏逻辑方法。这样做是为了确保序列化过程的纯净和可预测。例如一个玩家存档数据类可能长这样[System.Serializable] // 关键标记为可序列化 public class PlayerSaveData { public string playerName; public int level; public float experience; public Vector3 lastCheckpointPosition; // Unity内置类型如Vector3需要特殊处理 public ListInventoryItemData inventory; // 支持嵌套复杂对象和集合 public Dictionarystring, bool unlockedAchievements; // 字典的序列化需要额外注意 } [System.Serializable] public class InventoryItemData { public string itemId; public int count; }注意务必为你的数据类加上[System.Serializable]特性。这是告诉Unity的序列化系统以及后续的Json序列化库这个类的实例可以被序列化。忘记加这个特性是导致序列化失败的最常见原因之一。2.2 序列化层对象到Json字符串有了数据对象下一步就是把它转换成字符串。JsonJavaScript Object Notation因其轻量、易读、跨语言的特性成为游戏开发中数据交换的首选。在Unity中我们通常不直接用 .NET 自带的JavaScriptSerializer功能较弱而是选择更强大的第三方库比如Newtonsoft.JsonJson.NET或 Unity 2020.3以后内置的UnityEngine.JsonUtility。这里我详细对比一下两者这也是一个关键的选型决策点特性Newtonsoft.Json (需通过Package Manager安装)UnityEngine.JsonUtility (Unity内置)功能丰富度极其丰富。支持复杂对象图、循环引用、多态类型、自定义转换器、忽略属性等。基础。仅支持标记了[Serializable]的纯数据类/结构体。不支持字典、多态等。性能较好但功能多带来一定开销。极佳。Unity针对其序列化格式深度优化速度最快。对Unity类型支持需要额外配置或自定义转换器来处理Vector3,Quaternion,Color等。原生完美支持。直接序列化/反序列化Unity内置类型。易用性API灵活但需要学习更多概念。API极其简单只有ToJson和FromJson两个主要方法。数据可读性输出标准、格式化的Json易于调试。输出紧凑的Json可参数化美化但字段名可能与C#略有不同。我的选择与理由 对于大多数不涉及极端复杂对象关系如循环引用、继承树序列化的存档系统我强烈推荐使用JsonUtility。原因如下零依赖无需管理额外插件包项目更干净。性能最优存档加载/保存通常是IO密集型操作但序列化本身的速度在移动端等资源受限环境下依然重要。对Unity生态友好直接处理Vector3等类型省去了大量转换代码。足够使用存档数据模型通常是设计良好的、无循环引用的树状结构JsonUtility完全能满足需求。当然如果你的存档数据必须包含Dictionary或者需要序列化接口类型的集合那么Newtonsoft.Json是更合适的选择。你需要通过Unity的Package Manager安装它。本篇分享将基于JsonUtility进行因为这是更通用、更“Unity原生”的路径。2.3 加密层Json字符串到安全字节流得到Json明文字符串后直接写入文件就等于向玩家敞开了修改的大门。因此加密是必须的。对称加密算法AESAdvanced Encryption Standard是目前公认安全且高效的算法非常适合本地存档加密。AES加密需要几个关键要素明文Plain Text我们的Json字符串。密钥Key一个用于加密和解密的秘密字符串。必须是固定长度AES-128为16字节AES-256为32字节。初始化向量IV, Initialization Vector一个随机生成的字节数组用于确保即使相同明文、相同密钥每次加密产生的密文也不同增强安全性。长度通常为16字节。加密模式Cipher Mode如CBCCipher Block Chaining这是最常用的模式。填充模式Padding Mode如PKCS7用于处理数据长度不是块大小整数倍的情况。加密流程是C#对象- (JsonUtility.ToJson) -Json字符串- (Encoding.UTF8.GetBytes) -字节数组(明文)- (AES加密) -字节数组(密文)- (File.WriteAllBytes) -存档文件。解密则是完全相反的流程。这里有一个至关重要的安全实践绝对不要将密钥硬编码在代码里编译后的DLL或IL2CPP代码可以被反编译硬编码的密钥会暴露。一个更安全的做法是将密钥拆分成多个部分在运行时动态组合例如从资源文件读取一部分通过一个简单的算法生成另一部分。或者对于单机游戏可以使用设备相关的信息如设备ID的哈希值派生出一个密钥这样每个设备的存档都无法在其他设备上使用增加了篡改难度。对于网络游戏存档验证应该放在服务器端。2.4 文件IO层字节流到持久化存储最后一步就是将加密后的字节数组写入磁盘。Unity提供了Application.persistentDataPath这个属性它指向一个跨平台、有读写权限的持久化数据目录。这是存放存档文件的正确位置而不是Resources或StreamingAssets文件夹。string saveFilePath Path.Combine(Application.persistentDataPath, “player.sav”); File.WriteAllBytes(saveFilePath, encryptedData);读取时使用File.ReadAllBytes即可。3. 分步实现手把手构建核心管理器理论清晰了我们开始编码。我将创建一个名为SaveSystem的单例管理器类来封装所有功能。3.1 步骤一创建可序列化的数据模型首先根据你的游戏需求定义好存档数据结构。这里我们用一个综合的例子// SaveData.cs using System; using System.Collections.Generic; using UnityEngine; [Serializable] // 使用System.Serializable注意和UnityEngine.SerializeField区分 public class GameSaveData { public PlayerData player; public WorldData world; public SettingsData settings; } [Serializable] public class PlayerData { public string name; public int health; public int maxHealth; public float[] position; // 注意Vector3不能直接序列化我们用float数组表示 public ListItemData inventory; // 上次保存的时间戳 public long saveTimestamp; } [Serializable] public class WorldData { public int currentSceneIndex; public bool[] switchStates; // 场景中某些开关的状态 } [Serializable] public class SettingsData { public float musicVolume; public float sfxVolume; public int languageIndex; } [Serializable] public class ItemData { public string id; public int count; public int durability; }关键细节注意到PlayerData中的position字段了吗Vector3是Unity的结构体JsonUtility虽然能处理它但序列化出来的格式是{“x”: 0.0, “y”: 0.0, “z”: 0.0}。有时为了更紧凑的控制或兼容性我们选择用float[3]来存储。你需要编写辅助方法在Vector3和float[]之间转换。这是一个典型的“数据结构设计适应序列化方案”的案例。3.2 步骤二实现AES加密解密工具类接下来我们创建一个静态工具类AESEncryptor来处理加密解密。我们将使用C#的System.Security.Cryptography命名空间下的Aes类。// AESEncryptor.cs using System; using System.IO; using System.Security.Cryptography; using System.Text; public static class AESEncryptor { // 密钥和IV。警告此为示例生产环境必须使用更安全的方式管理密钥 // AES-128 密钥长度为16字节 (128位) private static readonly string DefaultKey “Your16ByteKeyStr!”; // 必须16个字符 private static readonly string DefaultIV “Your16ByteIV_Str!”; // 必须16个字符 /// summary /// 使用AES CBC模式加密字符串 /// /summary /// param name“plainText”明文/param /// param name“key”密钥Base64字符串或普通字符串/param /// param name“iv”初始化向量Base64字符串或普通字符串/param /// returnsBase64编码的密文/returns public static string Encrypt(string plainText, string key null, string iv null) { if (string.IsNullOrEmpty(plainText)) throw new ArgumentNullException(nameof(plainText)); byte[] encrypted; using (Aes aesAlg Aes.Create()) { // 配置AES算法参数 aesAlg.Key GetValidKey(key ?? DefaultKey); aesAlg.IV GetValidIV(iv ?? DefaultIV); aesAlg.Mode CipherMode.CBC; // 最常用的模式 aesAlg.Padding PaddingMode.PKCS7; // 兼容性最好的填充模式 // 创建加密器 ICryptoTransform encryptor aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); // 执行加密 using (MemoryStream msEncrypt new MemoryStream()) { using (CryptoStream csEncrypt new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt new StreamWriter(csEncrypt)) { swEncrypt.Write(plainText); } encrypted msEncrypt.ToArray(); } } } // 返回Base64字符串方便作为文本处理如果需要直接存二进制可跳过此步 return Convert.ToBase64String(encrypted); } /// summary /// 使用AES CBC模式解密字符串 /// /summary /// param name“cipherText”Base64编码的密文/param /// param name“key”密钥/param /// param name“iv”初始化向量/param /// returns明文/returns public static string Decrypt(string cipherText, string key null, string iv null) { if (string.IsNullOrEmpty(cipherText)) throw new ArgumentNullException(nameof(cipherText)); string plaintext null; using (Aes aesAlg Aes.Create()) { aesAlg.Key GetValidKey(key ?? DefaultKey); aesAlg.IV GetValidIV(iv ?? DefaultIV); aesAlg.Mode CipherMode.CBC; aesAlg.Padding PaddingMode.PKCS7; ICryptoTransform decryptor aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); // 将Base64密文转为字节数组 byte[] cipherBytes Convert.FromBase64String(cipherText); using (MemoryStream msDecrypt new MemoryStream(cipherBytes)) { using (CryptoStream csDecrypt new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) { using (StreamReader srDecrypt new StreamReader(csDecrypt)) { plaintext srDecrypt.ReadToEnd(); } } } } return plaintext; } /// summary /// 确保密钥是合法的32字节256位或16字节128位数组。 /// 这里示例简单处理将字符串UTF8编码后取MD5哈希值得到16字节或SHA256得到32字节。 /// 更安全的做法应由游戏服务器分发或通过密钥派生函数生成。 /// /summary private static byte[] GetValidKey(string keyString) { // 示例使用SHA256生成32字节密钥AES-256 using (SHA256 sha256 SHA256.Create()) { return sha256.ComputeHash(Encoding.UTF8.GetBytes(keyString)); } // 如果需要AES-128可以使用MD5生成16字节密钥注意MD5已不推荐用于安全场景此处仅演示 // using (MD5 md5 MD5.Create()) { return md5.ComputeHash(Encoding.UTF8.GetBytes(keyString)); } } private static byte[] GetValidIV(string ivString) { // IV通常需要是16字节。这里简单用字符串UTF8编码如果不足16字节则填充超过则截断。 // 更好的做法是每次加密生成随机IV并和密文一起存储。 byte[] ivBytes Encoding.UTF8.GetBytes(ivString); Array.Resize(ref ivBytes, 16); // 确保长度为16 return ivBytes; } }重要警告与进阶技巧密钥管理上面代码将密钥硬编码在类中这是极不安全的仅用于演示。真实项目中你应该考虑运行时生成首次运行时用System.Security.Cryptography.RandomNumberGenerator生成一个随机密钥和IV并将其存储在设备上一个隐蔽的位置如另一个加密文件或系统注册表。代码混淆对关键的安全相关代码进行混淆增加反编译后理解的难度。白盒加密高级对于防破解要求极高的游戏可以考虑白盒加密技术将密钥与算法融合但实现复杂。IV的使用上面的示例使用了固定的IV。实际上为了更安全每次加密都应该使用一个随机的IV。这个随机IV不需要保密可以和密文一起保存通常放在密文开头。解密时先读取IV再用它和密钥解密剩余部分。修改Encrypt方法可以很容易实现这一点。异常处理加密解密过程可能抛出多种异常如CryptographicException,FormatException等。在生产代码中你需要用try-catch包裹这些调用并提供友好的错误处理如提示“存档损坏”。3.3 步骤三整合成SaveSystem管理器现在我们将序列化、加密、文件IO整合到一个易于使用的单例类中。// SaveSystem.cs using System; using System.IO; using UnityEngine; public class SaveSystem : MonoBehaviour { public static SaveSystem Instance { get; private set; } private const string SAVE_FILE_NAME “game_save.sav”; private string SaveFilePath Path.Combine(Application.persistentDataPath, SAVE_FILE_NAME); // 当前内存中的存档数据 public GameSaveData CurrentSaveData { get; private set; } private void Awake() { if (Instance ! null Instance ! this) { Destroy(this.gameObject); return; } Instance this; DontDestroyOnLoad(this.gameObject); // 通常存档系统常驻 LoadGame(); // 游戏启动时尝试加载 } /// summary /// 创建一份新的存档数据例如开始新游戏时 /// /summary public void CreateNewSave() { CurrentSaveData new GameSaveData { player new PlayerData { name “New Player”, health 100, maxHealth 100, position new float[] { 0, 0, 0 }, inventory new ListItemData(), saveTimestamp DateTime.UtcNow.Ticks }, world new WorldData { currentSceneIndex 1, switchStates new bool[10] // 假设有10个开关 }, settings new SettingsData { musicVolume 0.8f, sfxVolume 0.8f, languageIndex 0 } }; Debug.Log(“[SaveSystem] New save data created.”); } /// summary /// 将当前内存中的存档数据保存到磁盘 /// /summary public bool SaveGame() { if (CurrentSaveData null) { Debug.LogError(“[SaveSystem] Cannot save: No save data in memory.”); return false; } try { // 1. 序列化对象 - Json字符串 string jsonString JsonUtility.ToJson(CurrentSaveData, prettyPrint: true); // prettyPrint用于调试正式发布可设为false Debug.Log($“[SaveSystem] JSON to save: {jsonString}”); // 2. 加密Json字符串 - 加密后的Base64字符串 string encryptedString AESEncryptor.Encrypt(jsonString); // 注意这里加密的是整个Json字符串。你也可以选择对Json中的特定敏感字段如金币数进行二次加密。 // 3. 写入文件Base64字符串 - 字节 - 文件 // 我们直接写入加密后的Base64字符串文本。也可以将Base64解码回字节后存二进制。 File.WriteAllText(SaveFilePath, encryptedString); Debug.Log($“[SaveSystem] Game saved successfully to: {SaveFilePath}”); return true; } catch (Exception e) { Debug.LogError($“[SaveSystem] Save failed: {e.Message}”); return false; } } /// summary /// 从磁盘加载存档数据到内存 /// /summary public bool LoadGame() { if (!File.Exists(SaveFilePath)) { Debug.LogWarning($“[SaveSystem] Save file not found at {SaveFilePath}. Creating new save.”); CreateNewSave(); return false; // 不是从文件加载的 } try { // 1. 读取文件文件 - 加密的Base64字符串 string encryptedString File.ReadAllText(SaveFilePath); // 2. 解密Base64字符串 - Json字符串 string jsonString AESEncryptor.Decrypt(encryptedString); Debug.Log($“[SaveSystem] JSON loaded: {jsonString}”); // 3. 反序列化Json字符串 - 对象 CurrentSaveData JsonUtility.FromJsonGameSaveData(jsonString); // 4. 数据验证与修复可选但重要 ValidateLoadedData(); Debug.Log($“[SaveSystem] Game loaded successfully from: {SaveFilePath}”); return true; } catch (CryptographicException) { Debug.LogError(“[SaveSystem] Load failed: Decryption error. The save file might be corrupted or tampered with.”); // 可以在这里处理存档损坏的情况比如重置存档或给玩家提示 CreateNewSave(); return false; } catch (Exception e) { Debug.LogError($“[SaveSystem] Load failed: {e.Message}”); CreateNewSave(); // 加载失败也创建新存档 return false; } } /// summary /// 验证并修复加载的数据防止因版本升级导致的结构不一致 /// /summary private void ValidateLoadedData() { if (CurrentSaveData.player null) CurrentSaveData.player new PlayerData(); if (CurrentSaveData.player.inventory null) CurrentSaveData.player.inventory new ListItemData(); if (CurrentSaveData.world.switchStates null) CurrentSaveData.world.switchStates new bool[10]; // 可以添加更多默认值检查和修复逻辑 } /// summary /// 删除存档文件 /// /summary public void DeleteSave() { if (File.Exists(SaveFilePath)) { File.Delete(SaveFilePath); Debug.Log($“[SaveSystem] Save file deleted: {SaveFilePath}”); } CurrentSaveData null; } // 提供一些便捷方法供游戏其他部分调用 public void SavePlayerPosition(Vector3 position) { if (CurrentSaveData?.player ! null) { CurrentSaveData.player.position new float[] { position.x, position.y, position.z }; } } public Vector3 LoadPlayerPosition() { if (CurrentSaveData?.player?.position ! null CurrentSaveData.player.position.Length 3) { return new Vector3(CurrentSaveData.player.position[0], CurrentSaveData.player.position[1], CurrentSaveData.player.position[2]); } return Vector3.zero; } }这个SaveSystem管理器提供了完整的生命周期管理创建、保存、加载、删除。它使用单例模式确保全局可访问并在加载失败时提供降级方案创建新存档。ValidateLoadedData方法是一个很好的实践用于处理存档数据版本迭代时可能出现的字段缺失问题。4. 实战中的关键问题与深度优化把基础功能跑通只是第一步。在实际项目中使用这套系统你会遇到一些更具体、更棘手的问题。下面是我在多个项目中总结出的核心要点和优化方案。4.1 版本兼容性与数据迁移游戏更新了旧存档怎么办这是存档系统设计中最容易被忽略也最致命的问题。假设你的游戏发布1.0版本后在1.1版本中为PlayerData增加了一个mana魔法值字段。一个1.0版本创建的存档在1.1版本中加载时JsonUtility.FromJson会直接反序列化但新增的mana字段会是默认值0。这可能导致玩家角色突然失去魔法能力或者更糟——如果这个字段被设计为不可为0游戏逻辑可能会崩溃。解决方案为存档数据添加版本号并实现迁移路径。在根数据类中添加版本字段[Serializable] public class GameSaveData { public int saveVersion 1; // 初始版本为1 // ... 其他原有字段 public PlayerData player; // ... }在加载后根据版本号执行迁移逻辑private void ValidateAndMigrateLoadedData() { int loadedVersion CurrentSaveData.saveVersion; int currentVersion 2; // 当前游戏代码对应的存档版本 if (loadedVersion currentVersion) { Debug.Log($“[SaveSystem] Migrating save data from version {loadedVersion} to {currentVersion}”); // 执行逐步迁移 if (loadedVersion 1) { // 从v1迁移到v2添加mana字段并赋予一个合理的初始值如根据等级计算 CurrentSaveData.player.mana CurrentSaveData.player.level * 10; CurrentSaveData.saveVersion 2; loadedVersion 2; } // 未来可以继续添加 if (loadedVersion 2) { ... } 来处理v2到v3的迁移 // 迁移完成后立即保存一次将新版本存档写回磁盘 SaveGame(); } else if (loadedVersion currentVersion) { // 存档版本比游戏客户端还新可能是测试版存档或损坏需要特殊处理 Debug.LogError($“[SaveSystem] Save file version ({loadedVersion}) is newer than game client ({currentVersion}).”); // 可以选择拒绝加载或尝试一个保守的兼容模式 } }然后在LoadGame成功反序列化后调用ValidateAndMigrateLoadedData()而不是简单的ValidateLoadedData()。4.2 性能考量存档卡顿与异步操作当存档数据量非常大时例如一个开放世界游戏保存了成千上万个物体的状态序列化和加密过程可能会在主线程上造成可感知的卡顿。特别是移动设备上IO操作和加密计算都可能成为瓶颈。优化策略分块保存不要每次保存都把整个世界的数据都存一遍。将数据按逻辑分块例如按场景、按区域。玩家触发保存时只保存发生变化的数据块。增量保存记录自上次保存以来哪些数据发生了改变只序列化和写入这些“脏数据”。这需要更复杂的数据变更追踪机制。异步保存使用async/await或协程将耗时的序列化、加密、文件写入操作放到后台线程避免阻塞游戏主循环。public async Taskbool SaveGameAsync() { if (CurrentSaveData null) return false; // 在后台线程执行序列化和加密 string jsonString await Task.Run(() JsonUtility.ToJson(CurrentSaveData, false)); string encryptedString await Task.Run(() AESEncryptor.Encrypt(jsonString)); // 文件写入也可以异步但需要注意Unity主线程对某些路径的访问限制 await File.WriteAllTextAsync(SaveFilePath, encryptedString); return true; }注意Unity中很多API如JsonUtility并非线程安全但在Task.Run中仅用于计算不涉及Unity对象是安全的。文件写入异步APIFile.WriteAllTextAsync在 .NET Standard 2.0/2.1 中可用。记得在调用异步保存时给玩家一个“保存中...”的提示。4.3 安全增强对抗内存修改与更优的密钥策略文件加密防住了直接修改存档文件但无法防止运行时内存修改器如Cheat Engine。攻击者可以在游戏内存中定位到CurrentSaveData对象直接修改金币、生命值等属性然后再触发游戏保存加密后的存档就是被篡改后的数据。防御思路校验和Checksum在存档数据中增加一个校验和字段。保存时计算整个数据对象除校验和本身外的哈希值如MD5或CRC32存入校验和字段。加载时重新计算哈希并与存储的值对比如果不一致则说明数据在内存中或被加密前篡改可视为无效存档。[Serializable] public class GameSaveData { public int saveVersion; public string checksum; // 用于存储哈希值 // ... 其他数据字段 } // 在SaveGame中先计算checksum再序列化 // 在LoadGame解密后验证checksum关键数据服务器验证在线游戏对于金币、钻石等影响游戏经济系统的关键数据其最终合法性应由游戏服务器验证。本地存档只是一个缓存重要交易必须与服务器通信。密钥动态化如前所述避免硬编码密钥。可以考虑将密钥与设备硬件信息如SystemInfo.deviceUniqueIdentifier绑定这样存档无法在不同设备间通用增加了作弊成本。4.4 调试与开发便利性在开发阶段你可能需要频繁查看存档内容来调试。每次都加解密很麻烦。开发辅助工具编辑器扩展创建一个Unity Editor窗口集成在Window/Save System菜单下。这个窗口可以显示当前内存中CurrentSaveData的所有内容使用EditorGUILayout动态绘制。提供按钮直接调用SaveGame()和LoadGame()。提供一个文本区域显示当前存档文件的解密后Json明文用于调试。提供一个“删除存档”按钮。条件编译使用#if UNITY_EDITOR预处理指令在编辑器中禁用加密或者使用一个固定的、简单的测试密钥方便查看明文。public string EncryptForSave(string json) { #if UNITY_EDITOR !FORCE_ENCRYPTION // 开发模式下可能不加密或使用简单加密 Debug.LogWarning(“[SaveSystem] EDITOR MODE: Encryption is disabled or simplified.”); // return json; // 直接返回明文 return SimpleXOR(json); // 或一个简单的混淆 #else return AESEncryptor.Encrypt(json); #endif }记得在发布版本时确保FORCE_ENCRYPTION或类似符号被定义以启用完整的AES加密。5. 扩展思考应对更复杂的场景基础的单一存档文件系统可以满足大部分需求。但随着项目复杂化你可能需要以下扩展多存档槽位这本质上就是管理多个存档文件。你可以修改SaveFilePath属性使其接受一个slotIndex参数Path.Combine(Application.persistentDataPath, $save_slot_{slotIndex}.sav”)。SaveSystem需要维护一个当前选中槽位的索引并提供列表、加载、删除特定槽位存档的方法。存档元数据为了在存档选择界面显示预览信息如游戏时间、角色截图、存档时间你不可能每次都去加载并解密整个存档文件。一个常见的做法是在保存主存档game_save.sav的同时额外保存一个小的、未加密或简单加密的元数据文件game_save.meta里面只包含预览所需的基本信息。这样读取列表时会非常快。云存档与跨平台对于Steam、Epic或移动平台iCloud、Google Play Games你需要使用平台提供的云存储API。你的SaveSystem可以抽象出一个存储接口然后为本地存储和云存储分别提供实现。在保存时先保存到本地然后异步同步到云端加载时尝试从云端拉取最新版本如果失败或离线则回退到本地版本。这里会涉及冲突解决例如同一账号在两个设备上玩了游戏产生两个存档等复杂问题。构建一个健壮的Unity存档系统远不止是调用JsonUtility.ToJson和File.WriteAllText那么简单。它涉及到数据建模、序列化选型、安全加密、异常处理、版本管理和性能优化等多个层面。本文提供的基于Json序列化和AES加密的方案是一个在安全性、易用性和性能之间取得很好平衡的起点。你可以根据自己项目的具体需求在此基础上进行裁剪和增强。记住好的存档系统是隐形的——玩家感觉不到它的存在但它却在默默无闻地守护着玩家的每一次冒险旅程。