1. 项目概述:为什么存档系统是游戏开发的“命门”
在游戏开发圈子里摸爬滚打十几年,我见过太多因为存档系统设计不当而“翻车”的项目。一个看似简单的“保存/加载”功能,背后牵扯到的是整个游戏状态的管理、数据持久化策略、以及玩家体验的连贯性。很多新手开发者,甚至一些有经验的团队,初期都会用PlayerPrefs对付一下,结果到了项目中后期,存档数据膨胀、加载卡顿、版本兼容性差、甚至被玩家轻易修改作弊等问题接踵而至,这时候再想重构,成本就非常高了。
这次我们聊的“备忘录模式”(Memento Pattern),就是解决这类问题的经典设计模式。它不是什么新潮的技术,但却是构建一个健壮、灵活、可维护的存档与状态管理系统的绝佳思想武器。尤其在 Unity 开发中,游戏对象的状态复杂多变(位置、血量、背包、任务进度等),如何在不破坏对象封装性的前提下,捕获其内部状态并在未来某个时刻恢复,这正是备忘录模式的核心价值。
简单来说,这个项目就是将备忘录模式的思想,与 Unity 引擎的特性深度结合,打造一套从架构设计到具体实现的存档系统解决方案。它不仅要解决“怎么存”的问题,更要解决“存什么”、“怎么高效存”、“怎么安全存”以及“怎么优雅地管理状态快照”这一系列问题。无论你是在做一款 Roguelike 地牢探险,还是一款开放世界 RPG,这套思路都能帮你构建起游戏数据的“时光机”。
2. 备忘录模式精解:不只是“保存状态”
在深入 Unity 实现之前,我们必须先吃透备忘录模式本身。很多资料把它讲得太抽象,我们直接用一个游戏开发中最常见的场景来理解。
2.1 核心角色与游戏场景映射
备忘录模式通常包含三个核心角色:
- 发起人 (Originator):需要被保存状态的对象。在游戏中,这可以是
Player(玩家)、Enemy(敌人)、Inventory(背包)等任何包含重要状态数据的游戏对象或管理器。 - 备忘录 (Memento):用于存储发起人内部状态的对象。它就是那个“存档点”或“快照”。关键点在于,它通常只暴露给发起人,对外界(特别是管理者)是黑盒,这保护了发起人状态的封装性。
- 管理者 (Caretaker):负责保存和管理备忘录的对象,但它不能也不应该操作或检查备忘录的内容。在游戏中,它就是我们的
SaveLoadManager(存档管理器)。
一个生活化的类比:想象你在玩一个复杂的策略游戏。
- 你控制的英雄单位就是“发起人”,它有攻击力、防御力、魔法值、装备列表等内部状态。
- 你手动点击“快速存档”时,游戏会为这个英雄创建一个“备忘录”,这个备忘录里封存了英雄那一刻的所有状态数据。
- 游戏的存档系统就是“管理者”,它负责把这个备忘录(存档文件)保存到硬盘的某个位置(比如
slot1.save)。 - 当你打不过 BOSS,读档时,管理者把
slot1.save这个备忘录交还给英雄单位,英雄单位自己从备忘录中读取数据,恢复状态。存档系统(管理者)从头到尾都不知道你的英雄具体有多少血、穿了什么装备,它只负责保管“黑盒子”。
2.2 为何在游戏开发中非它不可?
你可能觉得,我直接让Player脚本把自己所有属性写进一个SaveData类,然后让SaveLoadManager把这个类序列化成 JSON 存盘不就行了?为什么非要绕个弯子用备忘录模式?
这里面的区别,正是设计模式的精髓所在:
- 保持封装,降低耦合:如果
SaveLoadManager知道Player内部所有属性的细节(比如player.health,player.equippedWeapon),那么一旦Player类的结构发生变化(比如把health拆分成currentHealth和maxHealth),SaveLoadManager也必须跟着改。它们耦合在了一起。而备忘录模式中,SaveLoadManager只接触IMemento接口,它不关心具体内容,Player的内部变化被隔离了。 - 支持复杂的内部状态:
Player的状态可能不仅仅是几个基础变量。它可能包含对场景中其他对象的引用(如homeTown)、复杂的集合(如List<Quest>),甚至是私有字段。备忘录模式允许Player自己决定哪些状态需要保存、以及如何将这些状态转换成可序列化的形式(备忘录),这个过程对外是隐藏的。 - 实现“撤销/重做”等高级功能:备忘录模式天然适合需要历史状态回溯的场景。比如在策略游戏中撤销一步操作,在编辑器中撤销一次地形修改。管理者可以维护一个备忘录栈,轻松实现这些功能。
注意:备忘录模式的一个潜在缺点是,如果发起人对象非常庞大,频繁创建备忘录(快照)可能会消耗大量内存。在游戏中,我们需要有策略地创建快照,例如只在关键节点(检查点、手动存档)或定时(自动存档)时进行,而不是每帧都创建。
3. Unity 存档系统架构设计
理解了模式,我们开始在 Unity 里搭架子。一个好的架构应该职责清晰、易于扩展、性能可控。
3.1 核心架构图与模块职责
我们的系统主要分为四层:
[游戏运行时对象] (Originator) ↓ (创建/恢复) [状态备忘录] (Memento) —— 纯数据类,可序列化 ↓ (保存/加载) [存档管理器] (Caretaker) —— 单例,负责IO和缓存 ↓ (读写) [持久化存储] (磁盘文件、云存储等)各模块详细职责:
可存档接口 (IOriginator):这是一个我们自定义的接口,任何需要被存档的游戏对象或管理器都应实现它。它定义了两个核心方法:
public interface IOriginator { // 创建当前状态的备忘录 IMemento CreateMemento(); // 从给定的备忘录恢复状态 void RestoreFromMemento(IMemento memento); }这强制所有可存档对象遵循统一的协议。
备忘录接口与基类 (IMemento / MementoBase):
IMemento接口可能只包含一个Guid用于标识。我们更常用一个抽象基类MementoBase,它包含一些元数据,如存档时间、版本号、关联的 Originator ID 等。[System.Serializable] // 关键!必须可序列化 public abstract class MementoBase : IMemento { public string OriginatorId; // 哪个对象创建的 public System.DateTime SaveTime; public int DataVersion; // 用于处理版本兼容 // ... 其他元数据 }具体的状态数据,由派生类定义。例如
PlayerMemento : MementoBase,里面包含health,position,inventoryItemIds等。存档管理器 (SaveLoadManager):这是系统的中枢,通常设计为单例。
- 注册中心:维护一个
Dictionary<string, IOriginator>,用于通过 ID 查找可存档对象。 - 快照捕获:遍历所有已注册的
IOriginator,调用其CreateMemento(),收集所有备忘录。 - 序列化与IO:将收集到的备忘录列表(通常包装在一个
GameSaveData容器类里)序列化(如转为 JSON),然后加密、压缩,最后写入文件。 - 反序列化与恢复:读取文件,解密解压,反序列化得到
GameSaveData,然后遍历数据,找到对应的IOriginator,调用其RestoreFromMemento。 - 存档槽管理:管理多个存档文件。
- 注册中心:维护一个
持久化策略:决定数据如何最终落盘。Unity 提供了
Application.persistentDataPath作为跨平台的持久化数据路径。我们可以在此路径下创建Saves/目录来存放存档文件。
3.2 序列化方案选型:JSON vs. 二进制
这是实战中的关键抉择。上面网络资料提到了JsonUtility,MessagePack,Protobuf等,我们分析一下在 Unity 存档场景下的选择。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
UnityJsonUtility | 无需第三方库,Unity 内置支持;对[Serializable]类型和ISerializationCallbackReceiver支持好;在 AOT 平台(如 iOS)上稳定。 | 功能较弱(不支持字典、多态);序列化结构需严格匹配;性能不是最优。 | 新手项目、中小型数据、快速原型开发。推荐大多数项目起步使用。 |
| Newtonsoft.Json (JSON.Net) | 功能极其强大,支持几乎所有 C# 特性;高度可配置;社区熟悉度高。 | 需要导入第三方 DLL(需注意 Unity 兼容版本);在 IL2CPP 下可能需额外处理;序列化/反序列化速度比JsonUtility慢。 | 数据结构非常复杂、需要灵活序列化策略的项目。 |
| MessagePack-CSharp | 极高的性能,序列化后体积小(二进制);对 Unity 和 IL2CPP 支持良好。 | 数据为二进制,不可读,调试不便;需要为待序列化的类添加[MessagePackObject]等属性。 | 对性能和存档文件大小有严格要求的商业项目,尤其是移动端。 |
| Protobuf-net | 高性能,跨语言支持好,协议清晰。 | 在 Unity 中使用需要处理代码生成,配置稍复杂。 | 需要与后端(非 C#)通信,或对协议有严格要求的项目。 |
我的实战建议:
- 从
JsonUtility开始。它的易用性和稳定性是最大的优点。先把存档功能跑通,架构搭好。 - 遇到性能瓶颈或数据膨胀时,再考虑迁移到 MessagePack。MessagePack 的性能提升是实实在在的,特别是当你的存档数据包含大量数组或列表时。迁移成本主要是给数据类添加属性,但架构(
IOriginator/IMemento)通常不需要大改。 - 绝对不要使用
BinaryFormatter。正如 Unity 官方警告,它有严重的安全漏洞,未来版本中可能会被移除。
3.3 状态管理的边界与粒度
“存什么”和“怎么存”同样重要。不是所有数据都值得放进备忘录。
- 静态数据 vs. 动态数据:场景引用、预制体、配置表(如物品属性)这些属于静态数据,不应该存档。存档里只存 ID 或 Key,加载时根据 ID 去静态配置里查找还原。例如,存档里存
itemId: “sword_001”,而不是整个SwordItem对象。 - 引用类型与循环引用:直接序列化对场景中
GameObject或MonoBehaviour的引用是行不通的。我们需要将其转换为可序列化的标识符,如InstanceID或自定义的Guid,并在恢复时通过管理器重新解析。同时要小心对象间的循环引用导致序列化失败,JsonUtility对此处理能力很弱。 - 状态粒度:是为整个游戏世界创建一个巨大的
WorldStateMemento,还是为每个实体(Player, Enemy, Chest)创建独立的备忘录?我推荐混合策略。核心玩家数据、全局游戏进度用一个全局备忘录。而场景中大量的、可动态创建销毁的实体(怪物、掉落物),适合用独立的备忘录,并由一个EntityManager统一管理它们的创建与恢复。这有助于实现分块加载和保存。
4. 实战:构建基于备忘录模式的 Unity 存档系统
理论说再多,不如一行代码。我们开始动手实现一个最核心的流程。
4.1 第一步:定义可存档接口与备忘录基类
// 1. 可存档对象接口 public interface IOriginator { string GetOriginatorId(); // 获取唯一标识符 IMemento CreateMemento(); void RestoreFromMemento(IMemento memento); } // 2. 备忘录接口(可以很简单) public interface IMemento { string OriginatorId { get; } } // 3. 可序列化的备忘录基类 [System.Serializable] public abstract class MementoBase : IMemento { public string OriginatorId; public long saveTimestamp; // 使用时间戳便于管理 string IMemento.OriginatorId => OriginatorId; protected MementoBase(string originatorId) { OriginatorId = originatorId; saveTimestamp = System.DateTime.UtcNow.Ticks; } }4.2 第二步:实现具体的游戏对象备忘录
以玩家为例:
[System.Serializable] public class PlayerMemento : MementoBase { // 只保存必要的数据,而非引用 public float health; public float mana; public SerializableVector3 position; // 需要自定义可序列化的Vector3 public string[] equippedItemIds; // 装备的物品ID数组 public Dictionary<string, int> inventory; // 物品ID到数量的映射 (JsonUtility需处理) public PlayerMemento(string playerId, float health, float mana, Vector3 pos, string[] equipped, Dictionary<string, int> inv) : base(playerId) { this.health = health; this.mana = mana; this.position = new SerializableVector3(pos); this.equippedItemIds = equipped; this.inventory = inv; } } // 辅助类:因为Unity的Vector3不可直接序列化 [System.Serializable] public struct SerializableVector3 { public float x, y, z; public SerializableVector3(Vector3 v) { x = v.x; y = v.y; z = v.z; } public Vector3 ToVector3() { return new Vector3(x, y, z); } }4.3 第三步:让玩家对象实现 IOriginator
public class PlayerController : MonoBehaviour, IOriginator { public string playerId = "Player_01"; public float health = 100f; public float mana = 50f; private List<string> equippedItems = new List<string>(); private Dictionary<string, int> inventory = new Dictionary<string, int>(); public string GetOriginatorId() => playerId; public IMemento CreateMemento() { // 捕获当前状态,创建备忘录 return new PlayerMemento( playerId, health, mana, transform.position, equippedItems.ToArray(), new Dictionary<string, int>(inventory) // 创建副本 ); } public void RestoreFromMemento(IMemento memento) { if (memento is PlayerMemento playerMemento) { // 从备忘录恢复状态 health = playerMemento.health; mana = playerMemento.mana; transform.position = playerMemento.position.ToVector3(); equippedItems = new List<string>(playerMemento.equippedItemIds); inventory = new Dictionary<string, int>(playerMemento.inventory); Debug.Log($"Player state restored. Health: {health}, Position: {transform.position}"); } } // ... 玩家其他的逻辑代码 }4.4 第四步:构建存档管理器 (Caretaker)
这是最复杂的一部分,我们实现一个简单的单例管理器:
using UnityEngine; using System.Collections.Generic; using System.IO; public class SaveLoadManager : MonoBehaviour { public static SaveLoadManager Instance { get; private set; } // 存储所有可存档对象的注册表 private Dictionary<string, IOriginator> originatorRegistry = new Dictionary<string, IOriginator>(); // 存档文件路径 private string saveDirectoryPath; private string currentSaveFilePath; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); saveDirectoryPath = Path.Combine(Application.persistentDataPath, "Saves"); if (!Directory.Exists(saveDirectoryPath)) { Directory.CreateDirectory(saveDirectoryPath); } currentSaveFilePath = Path.Combine(saveDirectoryPath, "savegame.json"); } // 注册/注销可存档对象 public void RegisterOriginator(IOriginator originator) { string id = originator.GetOriginatorId(); if (!originatorRegistry.ContainsKey(id)) { originatorRegistry.Add(id, originator); } else { Debug.LogWarning($"Originator with id '{id}' is already registered."); } } public void UnregisterOriginator(string id) { originatorRegistry.Remove(id); } // 创建游戏存档数据容器 [System.Serializable] private class GameSaveData { public List<MementoBase> mementos = new List<MementoBase>(); public string gameVersion; public long saveTime; } // 保存游戏 public void SaveGame() { GameSaveData saveData = new GameSaveData(); saveData.gameVersion = Application.version; saveData.saveTime = System.DateTime.UtcNow.Ticks; // 1. 捕获所有状态 foreach (var kvp in originatorRegistry) { var memento = kvp.Value.CreateMemento() as MementoBase; if (memento != null) { saveData.mementos.Add(memento); } } // 2. 序列化为 JSON (使用 JsonUtility) string jsonString = JsonUtility.ToJson(saveData, true); // prettyPrint 为 true 便于调试 // 3. (可选) 加密和压缩 // byte[] encryptedData = YourEncryptionMethod(jsonString); // 4. 写入文件 try { File.WriteAllText(currentSaveFilePath, jsonString); Debug.Log($"Game saved successfully to: {currentSaveFilePath}"); } catch (System.Exception e) { Debug.LogError($"Failed to save game: {e.Message}"); } } // 加载游戏 public void LoadGame() { if (!File.Exists(currentSaveFilePath)) { Debug.LogWarning("No save file found."); return; } try { // 1. 读取文件 string jsonString = File.ReadAllText(currentSaveFilePath); // 2. (可选) 解密和解压 // string decryptedString = YourDecryptionMethod(encryptedData); // 3. 反序列化 GameSaveData saveData = JsonUtility.FromJson<GameSaveData>(jsonString); // 4. 恢复状态 // 注意:这里假设所有需要的 Originator 已经注册(例如在场景Awake/Start中注册)。 // 更健壮的做法是先反序列化所有备忘录,然后根据 OriginatorId 匹配并恢复。 Dictionary<string, MementoBase> mementoMap = new Dictionary<string, MementoBase>(); foreach (var memento in saveData.mementos) { mementoMap[memento.OriginatorId] = memento; } foreach (var kvp in originatorRegistry) { string id = kvp.Key; if (mementoMap.TryGetValue(id, out var memento)) { kvp.Value.RestoreFromMemento(memento); } else { Debug.LogWarning($"No save data found for originator: {id}"); } } Debug.Log("Game loaded successfully."); } catch (System.Exception e) { Debug.LogError($"Failed to load game: {e.Message}"); } } }4.5 第五步:在场景中集成与测试
- 将
SaveLoadManager脚本挂载到一个空的 GameObject 上,并确保它在场景中唯一且常驻 (DontDestroyOnLoad)。 - 确保你的
PlayerController或其他IOriginator在Start()或OnEnable()中向管理器注册,在OnDisable()或OnDestroy()中注销。void Start() { SaveLoadManager.Instance?.RegisterOriginator(this); } void OnDestroy() { if (SaveLoadManager.Instance != null) { SaveLoadManager.Instance.UnregisterOriginator(GetOriginatorId()); } } - 创建 UI 按钮,分别调用
SaveLoadManager.Instance.SaveGame()和LoadGame()。 - 运行游戏,操作玩家(移动、扣血、拾取物品),点击保存。退出游戏再重新运行,点击加载,观察玩家状态是否被正确恢复。
5. 高级议题与性能优化
基础系统搭建完毕后,我们面临的就是工程化难题和性能挑战。
5.1 版本兼容性:当数据结构发生变化
这是线上游戏必须考虑的问题。你今天存档的数据结构,在下个版本可能就变了(比如给PlayerMemento增加了一个stamina字段)。直接加载旧存档会反序列化失败。
解决方案:
- 版本号:在
GameSaveData和每个MementoBase中强制加入dataVersion字段。 - 增量迁移:编写迁移器 (
SaveDataMigrator)。加载存档时,检查版本号,如果低于当前版本,则按顺序执行一系列迁移函数,将旧数据结构逐步升级到新结构。
这种方法要求你保留所有历史版本的数据结构定义和迁移逻辑。public GameSaveData MigrateSaveData(GameSaveData oldData, int oldVersion) { GameSaveData currentData = oldData; for (int v = oldVersion; v < CURRENT_DATA_VERSION; v++) { currentData = ApplyMigrationStep(currentData, v); } return currentData; } private GameSaveData ApplyMigrationStep(GameSaveData data, int fromVersion) { if (fromVersion == 1) { // 假设V2版本增加了玩家耐力值,默认给100 foreach (var memento in data.mementos) { if (memento is PlayerMemento playerMem) { // 为旧的PlayerMemento添加默认耐力值 // 这里需要反射或更安全的方式,一种做法是先将memento转为Json,再反序列化为新类 } } data.dataVersion = 2; } // ... 其他版本迁移 return data; }
5.2 部分保存与增量更新
对于大型开放世界游戏,每次全量保存所有实体状态开销巨大。可以采用“脏标记”机制。
- 每个
IOriginator维护一个bool isDirty标记。 - 只有当状态发生改变时,才将
isDirty设为true。 - 存档管理器保存时,只为
isDirty的对象创建备忘录,保存后清除标记。 - 同时,维护一个全局的“基础存档”和多个“增量存档”。加载时,先加载基础存档,再按顺序应用增量存档。这类似于版本控制系统。
5.3 异步保存与防卡顿
文件 IO 和复杂的序列化(尤其是 JSON 序列化大量数据)可能造成主线程卡顿。
优化策略:
- 使用
JsonUtility.ToJson的重载:它可以将对象序列化到System.Text.StringBuilder,比返回字符串在内存分配上更高效。 - 分帧处理:如果可存档对象非常多,不要在单帧内遍历所有对象并创建备忘录。可以将注册表分成多个批次,每帧处理一批,使用协程 (
StartCoroutine) 或async/await来分散计算压力。 - 异步文件写入:使用
File.WriteAllTextAsync(.NET Standard 2.1 / C# 8.0 以上,需在 Unity 2021.2+ 并启用兼容性设置)或System.Threading.Tasks将文件写入操作放到后台线程。但要注意,Unity API(如Transform.position)必须在主线程访问,因此“捕获状态”(CreateMemento)仍需在主线程完成,只有最后的字节写入文件可以异步。public async Task SaveGameAsync() { // 在主线程捕获状态 GameSaveData saveData = CaptureGameState(); // 在后台线程序列化和写入(注意:JsonUtility不能在子线程用,需提前在主线程序列化) string jsonString = JsonUtility.ToJson(saveData); await Task.Run(() => { File.WriteAllText(currentSaveFilePath, jsonString); }); Debug.Log("Save async complete."); }
5.4 数据安全与防作弊
如网络资料所述,本地存档没有绝对安全。但我们可以增加作弊成本:
- 加密:对序列化后的 JSON 字符串或二进制流进行 AES 等对称加密。密钥可以硬编码(容易被反编译)或由服务器下发(在线游戏)。这能防止普通玩家直接记事本修改。
- 校验和:在存档数据中加入一个校验和(如 CRC32 或 MD5),该校验和基于数据本身和某个盐值计算。加载时重新计算并比对,不一致则说明数据被篡改。
- 关键逻辑服务器校验:对于在线游戏,所有关键逻辑(如抽奖结果、伤害计算)都应在服务器进行,客户端只发送操作指令。存档可以保存在服务器,本地只存缓存。
6. 常见问题排查与实战心得
踩过无数坑后,我总结了一些典型问题和处理技巧。
6.1 问题排查速查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 加载后对象状态未恢复 | 1. Originator 未正确注册。 2. OriginatorId 不匹配。 3. Memento 序列化/反序列化失败。 | 1. 检查RegisterOriginator是否在Awake/Start中调用,且早于LoadGame。2. 打印存档和恢复时的 OriginatorId进行比对。3. 检查 Memento类是否标记为[System.Serializable],所有字段是否都可序列化(避免UnityEngine.Object引用)。 |
| 存档文件为空或损坏 | 1. 序列化过程出错(如循环引用)。 2. 文件写入权限问题。 3. 加密/解密逻辑错误。 | 1. 先禁用加密,将序列化后的 JSON 字符串打印出来 (Debug.Log),看是否完整。2. 检查 Application.persistentDataPath路径,确保可写。3. 逐步调试加密解密流程,对比输入输出。 |
| 存档/加载时游戏卡顿 | 1. 一次性处理对象过多。 2. JsonUtility序列化大型复杂结构慢。3. 文件 IO 阻塞主线程。 | 1. 实现分帧异步保存。 2. 考虑使用 MessagePack等二进制序列化器。3. 使用异步文件 API ( WriteAllTextAsync)。 |
| 版本更新后旧存档无法加载 | 存档数据结构发生不兼容变更。 | 实现版本号和迁移系统,见 5.1 节。 |
| WebGL 平台存档失败 | WebGL 对文件系统访问限制大,Application.persistentDataPath实际是浏览器 IndexedDB。 | 使用PlayerPrefs存储小量数据,或使用UnityEngine.Networking.UnityWebRequest将存档数据发送到服务器。对于纯本地,可使用PlayerPrefs存储序列化后的字符串(注意大小限制)。 |
6.2 我的实战心得与技巧
- 为 MonoBehaviour 使用
[System.Serializable]的陷阱:很多人以为给 MonoBehaviour 加上[System.Serializable]就能被JsonUtility序列化。其实不然,JsonUtility只能序列化其字段,不能序列化 MonoBehaviour 本身(它是一个组件)。我们的备忘录必须是纯粹的 C# 类 (System.Object),存储数据,而不是组件引用。 - 处理 Unity 特有类型:
Vector3,Quaternion,Color,AnimationCurve等都不能直接被JsonUtility序列化。需要像上面例子一样,为它们创建可序列化的包装结构体 (SerializableVector3)。Unity 官方也有一个JsonVectorConverter的方案,但自定义结构体更直观可控。 - 字典
Dictionary的序列化:JsonUtility不支持直接序列化字典。常用解决方案有:- 序列化前转换为两个并列的数组(
List<TKey>和List<TValue>)。 - 使用
[Serializable]包装一个List<KeyValuePair>。 - 如果使用
Newtonsoft.Json或MessagePack,则原生支持字典。
- 序列化前转换为两个并列的数组(
- ScriptableObject 作为静态配置库:这是管理物品、技能、任务等静态数据的绝佳方式。存档里只存
ScriptableObject的GUID或自定义string ID,加载时通过一个中央AssetManager根据 ID 加载对应的ScriptableObject来还原。这完美分离了动态存档数据和静态配置。 - 在编辑器中调试存档:在
Editor文件夹下创建一个编辑器工具,可以一键读取、解析、甚至修改存档文件内容,这对于调试复杂的状态问题至关重要。你可以直接打印出存档的 JSON 内容,直观查看哪里出了问题。
最后,记住没有银弹。备忘录模式提供了优秀的架构指导,但具体实现细节需要根据你的项目需求不断调整。从简单的JsonUtility开始,逐步迭代,处理好版本兼容和性能问题,你的游戏存档系统就能成为坚实可靠的后盾,而不是项目后期的“技术债”。这套模式的价值,会在你需要实现快速存档、关卡重玩、观战系统甚至回放功能时,愈发凸显出来。