跳到主要内容

存档系统

MCE 使用基于 JSON 的存档系统,具备具名存档、原子写入、自动存档轮换、版本控制和可扩展性。存档系统会保存玩家的全部游戏状态 —— 队伍、背包、位置、徽章、任务进度和游戏变量。

与存档相关的选项

那个界面上的三个开关属于这个系统:Partida guardada 选择序列化 格式,Autoguard. historia 决定剧情节点是否自动写入,而 Subir a la nube 则是在数据离开本机之前会先询问的那个。

架构​

存档系统由若干组件构成:

组件用途
SavegameManager统筹保存/加载操作(通过 SavegameInstaller 绑定的 ScriptableObject)
SavableObject任何需要持久化数据的对象的基类(ScriptableObject):SaveToText、LoadFromText 和 ResetSave
MCESavesSerializer用于所有已保存文本的 JSON 序列化器
GameVariables具名变量的存储:布尔、整数、浮点和字符串
GameVariableHolder拥有 GameVariables 存储的 SavableObject(GlobalGameData 和每个 SceneInfoAsset 都是持有者)
GameVariableReference对单个变量的 Inspector 引用,由 CommandGraph 节点使用
Migrations/ISaveMigration、SaveMigrationPipeline 和 SaveFormatVersions:存档版本控制

保存的内容​

SavegameManager.SavegameObjects 是进入每个存档的可保存对象列表。引擎自带的有:

对象包含
GlobalGameData(保存为 SavableGlobalGameData)玩家位置、金钱、已获得的徽章、游戏变量、格式版本标记、秘密基地和玩家道馆
Roster带有完整状态的队伍怪物(物种、等级、个体值、努力值、性格、招式、PP、HP、状态、亲密度……)
MonsterStorage箱子里的怪物
Bag道具及数量
Dex每个物种的已见/已捕捉记录
QuestManager进行中的任务、已完成的目标、已领取的奖励
CharacterData玩家角色
TimeManager一天中的时段
Nursery繁殖育儿所
SceneInfoAsset每张地图自己的游戏变量

存档文件​

存档是一个文件夹,而不是单个文件。存档位于 Unity 的 Application.persistentDataPath 下的 Saves/ 中:

平台persistentDataPath
Windows%USERPROFILE%/AppData/LocalLow/<CompanyName>/<ProductName>/
macOS~/Library/Application Support/<CompanyName>/<ProductName>/
Linux~/.config/unity3d/<CompanyName>/<ProductName>/
Android内部存储(应用私有)
iOS应用沙盒
<persistentDataPath>/Saves/
2026-10-10-18-30-02/ a manual save, named with the current date
GlobalGameData.sav one .sav per savable object
Roster.sav
...
Manifest.txt the list of files a COMPLETE save must contain
Autosave/
OlderAutosave/

存档的写入方式被设计为防崩溃:

  • 每个文件都以原子方式写入(先写入临时文件,再替换)。
  • 整个存档首先写入 <name>.staging 文件夹,只有在所有文件和清单都写完之后才会交换到位。被中断的存档绝不会留下新旧状态混杂的半成品。
  • 加载时,如果清单中列出的文件缺失,就说明存档被中断了,因此会被拒绝,而不是加载一个不完整的存档。

保存与加载​

玩家从暂停菜单保存(Exit 条目会提示保存,菜单的保存键会询问并写入一个以当前日期命名的存档),并从主菜单加载(Continue 加载最新的存档,Load 列出全部)。当游戏在线运行(MCE Online)时,进度由服务器保存,不会写入本地存档。

在代码中,通过 SavegameManager(Source 和 Studio 版本):

// A new save named with the current date
yield return savegameManager.SaveGameWithCurrentDate(playerCharacter);

// Rotate the autosave
yield return savegameManager.Autosave(playerCharacter);

// Load by name, or the most recent one
yield return savegameManager.LoadGame(saveName);
yield return savegameManager.LoadLastSavegame();

SavegameManager.GetAllSavegames(out list) 和 AreThereSavegames() 告诉你磁盘上有什么。测试用的存档状态(SaveStates)与玩家的存档放在一起,使用自己的前缀,并从玩家能看到的所有列表中隐藏。

SDK 的存档外观层

engine.SaveSystem(ISaveSystem)公开了 Save(slot)、Load(slot)、HasSaveData(slot)、DeleteSave(slot)、OnSaved 和 OnLoaded,但在当前的引擎中,它没有连接到 SavegameManager:没有任何东西处理它的保存和加载请求,而 HasSaveData 会在 persistentDataPath 中查找一个引擎并不会写入的 slot{N}.sav 文件。真正的存档请使用 SavegameManager(Source 和 Studio 版本)或游戏内菜单。参见 ISaveSystem。

自动存档​

选项界面中的两个选项(存储在 GameplayConfigurationFile 中)控制自动存档:

选项何时保存
AutoSaveOnTeleporting每次玩家传送到另一张地图时(加载游戏期间除外)
AutoSaveOnStory在某个图中带有 AutoSaveIfEnabled 节点的剧情点

每次自动存档都写入 Autosave/;上一个存档仅在新存档成功写入之后才会轮换到 OlderAutosave/,因此被中断的自动存档绝不会毁掉最后一个完好的存档。

自动存档最佳实践
  • 把 AutoSaveIfEnabled 放在剧情节点之后,绝不要放在战斗或过场动画中间。
  • 保存时显示自动存档指示器。

存档版本控制​

存档带有格式版本,保存时写入 SavableGlobalGameData,加载时由 SaveMigrationPipeline 进行比较,并运行较旧存档所需的迁移:

版本名称含义
0LegacyHashes版本标记出现之前的存档:物种、形态、特性、招式和道具由其资产名称的运行时哈希识别
1StableNames通过它们的资产名称(稳定的字符串)识别。这是当前版本

当格式变化时,会向流水线添加一个新的 ISaveMigration,并提升 SaveFormatVersions.Current。

序列化字段名与资产名

没有提供迁移时,绝不要重命名与存档相关的类中的序列化字段;并且要把物种、招式、特性和道具的资产名称当作存档的键:重命名资产会破坏引用该名称的存档。如果你重命名了字段,现有的存档文件在加载时会丢失该数据;字段重命名请使用 [FormerlySerializedAs("oldName")]。

GameVariables​

GameVariables 系统是 MCE 持久化具名值的存储。它由 CommandGraph 节点和脚本读写,并随拥有它的对象一起保存。

变量类型​

类型说明示例
Boolean真/假标志defeatedBrock = true
Integer数值计数器fishCaught = 42
Float小数rivalTrust = 0.5
String文本值rivalName = "Gary"

每个变量都存在于一个持有者中:全局的(GlobalGameData)或某张地图的 SceneInfoAsset。在地图上定义的变量仅限该地图使用;全局变量由整个游戏共享。

在 CommandGraph 中使用​

  • SetBoolVariable、SetIntVariable:设置变量的值。
  • IfBool、IfIntEquals、IfIntCompare、SwitchIntVariable:根据变量分支。

这些节点中的每一个都带有一个变量引用,你在其中选择持有者、类型和名称。

在代码中使用​

// globalGameData is the injected GlobalGameData (a GameVariableHolder);
// the class that receives it implements IPlayerDataReceiver
bool beatBrock = globalGameData.GetVariable<bool>("defeatedBrock");
int fishCount = globalGameData.GetVariable<int>("fishCaught");

globalGameData.SetVariable("defeatedBrock", true);
globalGameData.SetVariable("fishCaught", fishCount + 1);

自定义存档数据​

使用 GameVariables(所有版本)​

随存档一起持久化你自己的标志和计数器,最简单的方法是把它们作为游戏变量存储在 GlobalGameData 上:它们会与其他所有内容一起被保存、加载、迁移和重置。

扩展 SavableObject(Source 和 Studio 版本)​

对于整个数据结构,编写一个新的 SavableObject,并把它的一个资产添加到 SavegameManager 资产的 Savegame Objects 列表中:

[CreateAssetMenu(menuName = "MyGame/Custom Save Data")]
public class CustomSaveData : SavableObject
{
[Serializable] private class Data { public int counter; public List<string> unlocked; }
private Data data = new() { unlocked = new List<string>() };

public override string SaveToText(ISerializer<string> serializer, PlayerCharacter playerCharacter) =>
serializer.To(data);

public override IEnumerator LoadFromText(ISerializer<string> serializer, string data,
MCESettings mceSettings, MonsterDatabaseInstance monsterDatabase)
{
this.data = serializer.From<Data>(data);
yield break;
}

public override void ResetSave() => data = new Data { unlocked = new List<string>() };
}

它会在每个存档中作为自己的 .sav 文件写入,并列入清单。ResetSave 会在新游戏开始时让它回到干净状态。

存档文件格式​

每个 .sav 文件都是由 MCESavesSerializer 写出的纯 JSON,因此调试时可以检查。具体形态取决于它所属的对象(全局数据、队伍等各有自己的文件)。

不要编辑存档文件

虽然存档文件是人类可读的 JSON,但如果你引入了无效数据(例如数据库中不存在的招式),手动编辑可能会导致问题。请改用游戏内工具或调试命令。

最佳实践​

  1. 测试保存/加载往返。保存、退出、重新加载,并验证一切完好无损。
  2. 用 GameVariables 保存事件标志。它们会自动持久化,并与 CommandGraph 集成。
  3. 规划存档迁移。如果你在更新中更改了格式,请添加一个迁移步骤。
  4. 一旦玩家有了存档,就不要重命名物种、招式、特性或道具的资产名称。
  5. 妥善处理加载失败。损坏或被中断的存档会被拒绝并显示错误对话框,而不是被部分加载。