跳到主要内容

存档与恢复

Guide

在当前结算完成后保存 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 Slotslot-1,可使用 1 至 64 个英文字母、数字、连字符或下划线slot-1
Current Scene Keybattle-party,表示当前游戏地点只执行继续时可以留空
After Saving & QuittingMain 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

Save Controller 的本地槽位为 slot-1,主菜单为 GCS Web Demo,battle-party 映射到 Party Showcase,恢复超时为 30 秒

存档会用 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 和战斗所需的状态,战斗数据还通过独立的对象表保留共享引用,因此接入自己的存档系统时也要保存完整的检查点

数据检查点内容
RunMaster 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 场景对象、委托和协程都不在可保存的数据范围内

完整签名与生命周期约定见 API 参考,UI 的状态读取方式见自定义 UI 接入