存档与恢复
在当前结算完成后保存 Run 和战斗进度,下次进入游戏时,从存档中的手牌、单位状态、敌人意图和待选奖励继续
用 GCSSaveController 可以把保存、保存并退出和继续游戏绑定到界面按钮,如果项目已有存档系统,可以用 GCSApi.BeginSaveAsync 取得 GcsCheckpoint,把它与地图、金币等进度写进同一条存档记录
确定保存时机
收到保存请求后,GCS 会先阻止新的玩家操作,等当前效果执行完、显式 Gate 释放后再采集检查点,此后 Save Session 会继续锁住 GCS 输入,普通保存在存档提交后即可释放会话,保存并退出则要等到离开战斗场景时再释放
| 发起保存时的状态 | 保存结果 |
|---|---|
| 手动玩家操作阶段,当前结算已完成 | 保存当前战斗与 Run |
| 战斗胜利后等待领取奖励 | 保存已经生成的奖励候选,继续后仍等待选择 |
| 胜利或失败结果已经完成结算 | 保留已结束的结果,不会再次发放奖励 |
| Manager 已初始化,没有当前战斗 | 保存 Master Deck 与 Run 能量加成 |
| 正在执行效果或等待显式 Gate | 等待战斗稳定,30 秒内未能稳定则返回 Timeout |
| 效果正在等待玩家 Choice 或反应窗口 | 返回 NeedsPlayerInput,完成选择或反应后重新保存 |
| 正在执行队伍自动行动 | 返回 Busy,等待自动行动结束 |
| 无法表示为稳定检查点的运行状态 | 返回 UnsupportedState,不写入不完整存档 |
已经安排在后续回合执行的效果可以随当前检查点保存,恢复后会沿用剩余回合数和执行上下文,协程执行到的位置、动画播放进度和尚未完成的交互选择都在检查点的范围之外
配置本地保存与继续
要把检查点写入本地槽位,在战斗场景和主菜单中分别添加 TinyGiants > GCS > Save Controller 组件,并把两个场景加入当前构建的场景列表,接着在 Inspector 的 Local Save 中为两个组件设置相同的 Save Slot,这样主菜单才能读到战斗场景写入的存档
| 配置 | 战斗场景 | 主菜单 |
|---|---|---|
Save Slot | slot-1,可使用 1 至 64 个英文字母、数字、连字符或下划线 | slot-1 |
Current Scene Key | battle-party,表示当前游戏地点 | 只执行继续时可以留空 |
After Saving & Quitting | Main Menu 返回菜单,或 Quit Application 退出应用 | 只执行继续时不使用 |
Main Menu | 选择 GCS Web Demo | 选择同一个主菜单场景,恢复失败时返回这里 |
Continue Destinations | 如果战斗场景也提供继续入口,在这里配置它能恢复的场景 | 点击 + Add Scene,将 Scene Key 设为 battle-party,在 Gameplay Scene 选择 Party Showcase |
Restore Timeout Seconds | 默认 30 秒 | 默认 30 秒 |
截图以 Demo 场景为例,保存时会把当前场景的 Key battle-party 写入 slot-1,从 GCS Web Demo 的继续入口读取这个槽位时,控制器会按 Continue Destinations 中的映射加载 Party Showcase

存档会用 battle-party 这样的场景 Key 记住玩家所在的地点,移动或重命名场景后,保留原 Key,并在 Inspector 中更新它对应的场景资产,配置好映射后,就可以把界面按钮的 On Click 绑定到对应的组件方法
| 按钮 | 绑定方法 | 完成后的行为 |
|---|---|---|
| 保存 | SaveGame() | 写入成功后恢复当前游戏操作 |
| 保存并退出 | SaveAndQuit() | 写入成功后返回主菜单或退出应用 |
| 继续游戏 | ContinueGame() | 读取同一个 Slot,加载映射场景并恢复检查点 |
| 取消 | CancelOperation() | 请求取消当前操作,已经提交的存档继续保留 |
按钮绑定后,在 Interface Events 中用 On Busy Changed 控制等待遮罩和按钮状态,保存或继续期间重复调用会返回 Busy,操作失败时用 On Error 显示原因,存档提交后会收到 On Saved,整个继续流程完成后会收到 On Continued
如果保存和继续由项目脚本发起,可以在脚本中引用场景里的 GCSSaveController,调用同一个组件的异步方法并等待结果
using System.Threading;
using System.Threading.Tasks;
using TinyGiants.GCS.Runtime;
public GCSSaveController SaveController;
public Task<GcsSaveResult> SaveAndExitAsync(CancellationToken ct)
=> SaveController.SaveAndQuitAsync(ct);
public Task<GcsSaveResult> ContinueAsync(CancellationToken ct)
=> SaveController.ContinueGameAsync(ct);
异步调用结束后,先检查结果的 Success,失败时可从 Error.Code 判断原因,并显示 Error.Message,写入失败时玩家会留在当前游戏,如果存档已经写入但主菜单加载失败,错误信息会注明进度已保存
在支持文件存储的平台上,GCS 会先写入并刷新临时文件,再替换 Application.persistentDataPath 下的 TinyGiants/GCS/Saves/<slotId>.json,WebGL 上则会等 IndexedDB 写入事务完成,两种存储方式都将单条记录限制在 32 MiB,并在读取时通过校验和检查数据完整性
由于 WebGL 存档保存在当前浏览器的站点数据中,清除站点数据也会删除这些存档,可用配额和隐私模式下的保留时间由浏览器决定,存储失败时会返回 IoError,需要账户存档或云同步时,可以把检查点交给项目自己的存档系统
检查点保存的内容
本地存档会把场景 Key、保存时间和格式版本连同 GcsCheckpoint 一起写入槽位,其中 GcsCheckpoint 保存了继续 Run 和战斗所需的状态,战斗数据还通过独立的对象表保留共享引用,因此接入自己的存档系统时也要保存完整的检查点
| 数据 | 检查点内容 |
|---|---|
| Run | Master Deck 的卡牌、升级与费用变化,Run 能量加成 |
| 战斗与行动 | Encounter、规则、当前回合、阶段、活动单位、可行动成员与策略进度 |
| 单位 | 身份、队伍与槽位、HP、护甲、能量、死亡状态、状态层数与施加者 |
| 卡牌与牌堆 | 实例身份、升级、费用变化、四个牌堆的顺序、当前回合记录与出牌上下文 |
| 持续规则 | 费用规则、Hook、状态衰减进度与跨回合延迟触发上下文 |
| 敌人 | 已计划的目标、Intent、Pattern 计数和行为路径 |
| 奖励 | 已生成的奖励候选与等待领取状态 |
| 其他运行数据 | 战斗变量、受支持的扩展状态、GCS 玩法随机状态与内容兼容信息 |
恢复战斗时,GCS 会直接装载存档里的手牌、奖励和敌人意图,跳过起手抽牌、战斗开始和回合开始效果,UI、音效、动画、事件订阅和运行缓存则根据恢复后的状态重新建立
接入已有存档系统
如果项目已有存档系统,可以在运行时存档类型中加入 GcsCheckpoint 字段,并沿用项目的文件位置、编码、账户、云同步和提交策略,例如将金币 Gold、地图节点 MapNode 和同一时刻的检查点 Gcs 放进一条 AdventureSave 记录,提交时就能一起写入
using System;
using TinyGiants.GCS.Runtime;
[Serializable]
public sealed class AdventureSave
{
public int Version = 1;
public string SceneKey;
public int Gold;
public string MapNode;
public GcsCheckpoint Gcs;
}
要保存这条记录,先由项目的存档协调器暂停购买、地图推进等会改变进度的操作,再用 GCSApi.BeginSaveAsync 获取 Save Session,拿到检查点后,在 captureWorld 中同步读取项目状态,接着由 commit 提交完整记录,确认提交成功后再调用 leave 离开场景
using System;
using System.Threading;
using System.Threading.Tasks;
using TinyGiants.GCS.Runtime;
public static class AdventurePersistence
{
public static async Task<GcsSaveResult> SaveAndLeaveAsync(
string sceneKey,
Func<AdventureSave> captureWorld,
Func<AdventureSave, CancellationToken, Task> commit,
Func<CancellationToken, Task> leave,
CancellationToken ct)
{
var begin = await GCSApi.BeginSaveAsync(ct);
if (!begin.Success) return begin;
using (var session = begin.Value)
{
bool saved = false;
try
{
var record = captureWorld();
record.SceneKey = sceneKey;
record.Gcs = session.Checkpoint;
await commit(record, ct);
saved = true;
ct.ThrowIfCancellationRequested();
await leave(ct);
return GcsSaveResult.Ok();
}
catch (OperationCanceledException)
{
return GcsSaveResult.Fail(GcsSaveErrorCode.Cancelled,
saved ? "Progress saved; leaving cancelled."
: "Save cancelled before completion.");
}
catch (Exception error)
{
return GcsSaveResult.Fail(
saved ? GcsSaveErrorCode.SceneLoadFailed
: GcsSaveErrorCode.IoError,
saved ? "Progress saved; leaving failed: " + error.Message
: error.Message);
}
}
}
}
为了让检查点和项目状态对应同一时刻,captureWorld 应当同步读取数据,期间避免修改战斗,commit 要等整条记录持久写入后才返回成功,写入失败时要保留上一份有效存档,Save Session 会锁住 GCS 输入,购买和地图推进等操作仍由项目协调器负责暂停和解锁
继续游戏时,项目协调器先读取 AdventureSave 并校验项目字段,再把记录中的 Gcs 和 SceneKey 交给已配置场景映射的 GCSSaveController.RestoreGameAsync,同时传入应用项目状态的回调
public static Task<GcsSaveResult> RestoreExistingAsync(
GCSSaveController controller,
AdventureSave record,
Action<AdventureSave> applyPreparedWorld,
CancellationToken ct)
{
return controller.RestoreGameAsync(
record.Gcs,
record.SceneKey,
() => applyPreparedWorld(record),
ct);
}
目标场景加载后,GCS 会先校验检查点,再调用 applyPreparedWorld 同步应用已经准备好的项目数据,回调成功后才会发布检查点并发出恢复通知,等表现层完成绑定后再恢复输入,如果回调失败,控制器会尝试返回配置的主菜单,回调已经写入的外部状态由项目负责回滚
恢复场景与自定义 UI
GCSSaveController 会在加载目标场景前建立 GcsRestoreContext,目标场景的 Bootstrap 和 Demo 开场屏会据此跳过新战斗的启动,让检查点接管战斗状态,传入的取消令牌应覆盖完整的保存或继续流程,例如从主菜单继续时,令牌需要在菜单对象销毁后仍然有效,否则正常切换场景也会取消恢复
订阅 GCSApi.OnCheckpointRestored 后,可以在回调中清除旧对象引用,再用恢复后的状态重建手牌、单位和待选奖励,如果恢复的是仅包含 Run 进度的检查点,GCSApi.Battle 会是 null,此时清空战斗界面,并从 GCSApi 读取 Run 数据
这些界面可以在回调中同步刷新,如果需要异步创建表现对象,就在回调中调用 GcsRestoreContext.HoldPresentation() 取得句柄,等对象准备好后再释放,控制器会等所有句柄释放后才结束继续流程
如果场景由项目自己的加载器管理,在加载前调用 GcsRestoreContext.Begin(scenePath),让目标场景也能识别这次加载是在恢复存档,等 GameCardManager、数据库和项目扩展就绪后,调用 GCSApi.RestoreCheckpointAsync,再用 WaitForPresentationAsync 等待界面完成绑定,最后释放恢复上下文,如果恢复失败,清理目标场景并保留存档文件
处理错误与内容更新
保存或恢复失败时,根据 Error.Code 确定玩家能否重试,以及项目需要修正哪一项配置
| 错误 | 处理方式 |
|---|---|
NotFound | 显示当前槽位没有存档,不开始新战斗 |
NotReady | 初始化目标场景的 Manager 与内容数据库 |
Busy | 等待当前保存或恢复完成 |
NeedsPlayerInput | 让玩家完成当前 Choice 或反应窗口,再重新保存 |
Timeout | 检查未释放的 Gate、表现等待或场景初始化 |
MissingContent | 恢复存档依赖的内容和数据库配置 |
IncompatibleVersion | 使用兼容的内容与格式,或由项目提供明确迁移 |
UnsupportedState | 检查当前阶段及自定义数据、策略的存档支持 |
InvalidData | 报告损坏或不合法记录,保留原文件供处理 |
IoError | 显示写入失败并允许重试,不离开当前游戏 |
Cancelled | 结束等待,已经提交的记录继续有效 |
SceneLoadFailed | 检查场景映射与构建列表,若错误说明已保存,不要删除有效存档 |
遇到 MissingContent 或 IncompatibleVersion 时,检查目标场景的数据库是否仍包含存档依赖的内容,GCS 会根据检查点的格式版本和已注册内容集合的指纹判断兼容性,删除、改名或调整规则后的对应关系需要由项目明确迁移,发布更新前应使用新版数据库验证已有存档
自定义战斗变量或策略使用 GCS 默认无法保存的数据类型时,可以实现 IGcsSaveValueCodec,再用 GcsSaveValueCodecs.Register 注册带有稳定标识和版本的编解码器,确保恢复出的值保留原有语义,解码过程要保持当前游戏状态不变,任意 Unity 场景对象、委托和协程都不在可保存的数据范围内