跳到主要内容

存取档流程

相关源文件(Relevant source files)

本页记录 Adventure-King 完整的存/读档流程:包括游戏状态如何被采集、持久化、恢复,以及在场景切换期间如何被管理。底层数据结构请参见 Save Data Structures。存储层实现细节(localStorage、JSON 文件、原子写)请参见 Storage Layer

概览

存/读档系统采用 双通道架构:其一是用于“零延迟”场景切换的 运行时缓存(runtime cache),其二是用于持久化存档的 存储层(persistent storage layer)。所有操作由 SaveManager 单例统一编排,在玩法代码(GameScene、PlayerCharacter)与存储后端之间进行协调。

关键特性:

  • 非对称流程:存档采集完整世界状态;读档先填充运行时缓存,再由场景消费
  • 双存储:localStorage(SQLite KV)为主 + JSON 文件备份
  • 运行时缓存:在内存中缓存玩家/进度数据,场景切换无需磁盘 I/O
  • 自动存档:周期性(60s)+ 触发式(升级、换装等)保存到当前活跃槽位

存档流程架构

存档高层流程

来源: Adventure-King/Classes/Scenes/GameScene.cpp L937-L973

Adventure-King/Classes/Scenes/GameScene.cpp L328-L389

Adventure-King/Classes/Save/SaveManager.cpp L325-L376


存档数据采集阶段

存档流程从 GameScene::fillProgressDataForSave()世界状态采集 开始:

数据来源方法输出结构
玩家位置_player->getPosition()progressData.playerPosX/Y
关卡清理状态_levelMap->isLevelCleared()progressData.isLevelCleared
敌人生成点_levelMap->exportEnemySpawnPointStates()vector<EnemySpawnPointState>
竞技场战斗状态_levelMap->exportArenaStates()vector<ArenaState>
存活怪物遍历 _gameLayer 子节点vector<MonsterState>(HP、MP、break meter、arenaID)

怪物快照逻辑Adventure-King/Classes/Scenes/GameScene.cpp L349-L388

  • 只保存仍存活的 MonsterBase 实例
  • 竞技场怪物会写入 arenaID,以避免读档后波次被重新刷出
  • Boss 的 break meter 通过 getBreakMeter() 持久化

来源: Adventure-King/Classes/Scenes/GameScene.cpp L328-L389

Adventure-King/Classes/Scenes/LevelMap.cpp L570-L646


玩家数据提取

SaveManager::extractPlayerData() 会查询 PlayerCharacter 的各组件:Adventure-King/Classes/Save/SaveManager.cpp L645-L736

组件导出方法:

  • AttributeComponent::exportToSaveData():基础属性 + 装备加成 + AI 祝福
  • InventoryComponent::exportToSaveData():已装备物品(按槽位)+ 背包列表
  • SkillComponent::exportToSaveData():已学习技能 + 主动/被动槽位 + 冷却

来源: Adventure-King/Classes/Save/SaveManager.cpp L645-L736


序列化与存储

数据采集完成后,SaveManager 会执行 双通道持久化Adventure-King/Classes/Save/SaveManager.cpp L358-L376

  1. 序列化为 JSONJsonSerializer::serialize(saveSlotData) → JSON 字符串
  2. 写入 localStoragelocalStorageSetItem(key, json)(SQLite KV)
  3. 写入 JSON 文件:原子写模式(.tmp → rename)保证崩溃安全
  4. 更新元信息:时间戳、游玩时长、游戏版本

两条存储路径的关键差异:

  • localStorage:随机访问快、跨平台(Android/iOS/桌面)
  • JSON 文件:可读性备份,当 localStorage 损坏时用于回退恢复

来源: Adventure-King/Classes/Save/SaveManager.cpp L325-L376

Adventure-King/Classes/Save/JsonSerializer.cpp L412-L604


读档流程架构

读档高层流程

来源: Adventure-King/Classes/Scenes/HelloWorldScene.cpp L586-L634

Adventure-King/Classes/Scenes/GameScene.cpp L94-L122

Adventure-King/Classes/Scenes/GameScene.cpp L198-L306


运行时缓存填充

读档会触发“运行时缓存填充”,而不是直接创建场景。这样可以让“新游戏”和“读档进入”复用同一套初始化管线

HelloWorldScene 加载回调(load callback) Adventure-King/Classes/Scenes/HelloWorldScene.cpp L604-L640

:

// Read save from disk
SaveSlotData saveData;
saveManager->loadGame(slotIndex, saveData);

// Populate runtime cache
saveManager->setRuntimePlayerData(saveData.playerData);
saveManager->setRuntimePlayerPosition(Vec2(saveData.progressData.playerPosX, ...));
saveManager->setRuntimeProgressData(saveData.progressData);

// Transition to LoadingScene → GameScene (will consume cache)
auto loadingScene = LoadingScene::createScene(targetSceneID);
Director::getInstance()->replaceScene(transition);

为什么使用运行时缓存,而不是直接传递 SaveSlotData?

  • 统一初始化入口:GameScene::initPlayer() 总是先检查 hasRuntimePlayerData()
  • 同时适用于读档与场景切换(HOME → Level → HOME)
  • 将存/读档逻辑与场景生命周期解耦

来源: Adventure-King/Classes/Scenes/HelloWorldScene.cpp L604-L640

Adventure-King/Classes/Save/SaveManager.cpp L99-L165


GameScene 中的运行时缓存消费

GameScene::initWithPhysicsConfig() 会按优先级顺序消费运行时缓存:Adventure-King/Classes/Scenes/GameScene.cpp L425-L509

1. 创建玩家阶段(读取缓存前):

// Default initialization
CharacterRole role = CharacterRole::WARRIOR;
bool hasRuntimeData = saveManager->hasRuntimePlayerData();

// Priority: runtimePlayerData > sessionSelectedRole > default
if (hasRuntimeData) {
role = static_cast<CharacterRole>(saveManager->getRuntimePlayerData().role);
} else if (saveManager->hasSessionSelectedRole()) {
role = saveManager->getSessionSelectedRole();
}

auto playerSprite = PlayerCharacter::create(role, spritePath);

2. 恢复玩家数据(创建后):

if (saveManager->hasRuntimePlayerData()) {
saveManager->applyPlayerData(_player, saveManager->getRuntimePlayerData());
}

3. 恢复世界状态(玩家生成后):Adventure-King/Classes/Scenes/GameScene.cpp L198-L306

if (saveManager->hasRuntimeProgressData()) {
GameProgressSaveData progress = saveManager->getRuntimeProgressData();
saveManager->clearRuntimeProgressData(); // Clear immediately

_levelMap->applyEnemySpawnPointStates(progress.enemySpawnPoints);
_levelMap->applyArenaStates(progress.arenas, ...);

// Restore alive monsters
for (const auto& m : progress.aliveMonsters) {
auto monster = createMonsterByType(m.monsterType);
monster->setPosition(Vec2(m.posX, m.posY));
monster->setCurrentHP(m.currentHP);
// ... restore break meter, arena registration
}

_levelMap->restoreLevelClearedForLoad(progress.isLevelCleared);
}

4. 恢复玩家位置(延迟 1 帧):Adventure-King/Classes/Scenes/GameScene.cpp L94-L122

void GameScene::onEnter() {
Scene::onEnter();

if (saveManager->hasRuntimePlayerPosition()) {
const Vec2 savedPos = saveManager->getRuntimePlayerPosition();
saveManager->clearRuntimePlayerPosition();

scheduleOnce([this, savedPos](float)
{
if (this->_player)
{
this->_player->setPosition(savedPos);
}
},
0.0f,
"ApplyRuntimePlayerPosition");
}
}

为什么要延迟 1 帧设置位置? 派生场景(例如 HomeScene)可能会在 init() 中对 _gameLayer 应用自定义缩放;因此必须在缩放完成之后再设置位置。

来源: Adventure-King/Classes/Scenes/GameScene.cpp L94-L122

Adventure-King/Classes/Scenes/GameScene.cpp L198-L306

Adventure-King/Classes/Scenes/GameScene.cpp L425-L509


组件数据应用

SaveManager::applyPlayerData() 会把存档状态写入 PlayerCharacter 的各组件:Adventure-King/Classes/Save/SaveManager.cpp L738-L863

组件方法行为
PlayerCharacter直接 settersetLevel(), setExperience(), setCurrentHP/MP(), set[Active/Passive/Attribute]Points()
AttributeComponentapplyFromSaveData()恢复基础属性 + AI 祝福加成 → recalculateFinalAttributes()
InventoryComponentapplyFromSaveData()清空槽位 → 重新装备全部物品 → 触发属性重算相关回调
SkillComponentapplyFromSaveData()清空已学 → 重新学习技能 → 分配主动/被动槽位 → 恢复冷却

关键顺序: 必须先应用装备、再应用技能,因为某些技能效果依赖当前装备的武器类型。

来源: Adventure-King/Classes/Save/SaveManager.cpp L738-L863


运行时缓存系统

运行时缓存(runtime cache) 会把临时的玩家/进度数据存放在 SaveManager 的内存中,用于实现零延迟场景切换(无需磁盘 I/O)。

运行时缓存生命周期

来源: Adventure-King/Classes/Save/SaveManager.cpp L84-L165


缓存填充场景

场景 1:场景切换(Level → Map → Level)

当玩家通过传送门离开关卡时:Adventure-King/Classes/Scenes/GameScene.cpp L745-L757

void GameScene::returnToMapScene() {
// Cache player state before scene destruction
if (_player) {
saveManager->cacheRuntimePlayerData(_player);
}

auto mapScene = MapScene::createScene();
Director::getInstance()->replaceScene(transition);
}

当玩家从地图重新进入关卡时,GameScene::initPlayer() 会消费缓存:Adventure-King/Classes/Scenes/GameScene.cpp L502-L509

场景 2:读档(Disk → Runtime Cache → Scene)

读档流程会同时填充玩家数据与进度数据:Adventure-King/Classes/Scenes/HelloWorldScene.cpp L612-L617

saveManager->setRuntimePlayerData(saveData.playerData);
saveManager->setRuntimePlayerPosition(Vec2(saveData.progressData.playerPosX, ...));
saveManager->setRuntimeProgressData(saveData.progressData);

场景 3:新游戏(带职业选择)

主菜单职业选择会清理陈旧缓存:Adventure-King/Classes/Save/SaveManager.cpp L133-L146

void SaveManager::setSessionSelectedRole(CharacterRole role) {
_sessionSelectedRole = role;
_hasSessionSelectedRole = true;

// Clear runtime data to prevent "changed role but kept old stats"
clearRuntimePlayerData();
clearRuntimePlayerPosition();
clearRuntimeProgressData();
}

来源: Adventure-King/Classes/Scenes/GameScene.cpp L745-L767

Adventure-King/Classes/Save/SaveManager.cpp L84-L165


缓存 vs. 活跃槽位

概念用途生命周期清理时机
运行时缓存零延迟场景切换单次场景切换被 GameScene 消费后
活跃存档槽位记录自动存档应写入哪个槽位整个游戏会话应用重启或显式清理
会话职业选择记录主菜单选中的职业直到第一个场景创建被 GameScene 消费后

开始游戏时,HelloWorldScene 会设置 活跃存档槽位Adventure-King/Classes/Scenes/HelloWorldScene.cpp L494-L501

saveMenu->setStartSlotCallback([this, startMenuItem](int /*slotIndex*/, bool hasSave, const SaveSlotData &saveData)
{
if (!hasSave)
{
// 新开局:进入职业选择(确认后进入 HOME)
this->showRoleSelectLayer(startMenuItem);
return;
}

// 读档:统一走 LoadingScene
const std::string &sceneName = saveData.progressData.currentSceneName;
auto registry = SceneRegistry::getInstance();
SceneID targetID = registry ? registry->getSceneIDByName(sceneName) : SceneID::NONE;
if (targetID == SceneID::NONE)
{
CCLOG("HelloWorld - 读档失败:注册表中不存在场景 [%s]", sceneName.c_str());
return;
}

auto saveManager = SaveManager::getInstance();
if (saveManager)
{
saveManager->setRuntimePlayerData(saveData.playerData);
saveManager->setRuntimePlayerPosition(Vec2(saveData.progressData.playerPosX, saveData.progressData.playerPosY));
saveManager->setRuntimeProgressData(saveData.progressData);
}

auto loadingScene = LoadingScene::createScene(targetID);
if (loadingScene)
{
auto transition = TransitionFade::create(GameSceneConfig::Scene::TRANSITION_DURATION, loadingScene, Color3B::BLACK);
Director::getInstance()->replaceScene(transition);
}
});

来源: Adventure-King/Classes/Save/SaveManager.cpp L168-L189

Adventure-King/Classes/Scenes/HelloWorldScene.cpp L494-L574


自动存档机制

自动存档系统由两类触发驱动:周期定时器(60s)与立即请求(状态变化触发)。

自动存档流程

来源: Adventure-King/Classes/Scenes/GameScene.cpp L864-L935

Adventure-King/Classes/Scenes/GameScene.cpp L975-L1011


触发机制

1. 立即存档请求(状态变化触发)

由关键状态变化触发:Adventure-King/Classes/Save/SaveManager.cpp L533-L547

  • Level up: PlayerCharacter::gainExperience()requestImmediateSave("升级")
  • 学习技能(Skill learned)SkillComponent::learnSkillFromData()requestImmediateSave("学习技能")
  • 装备变更(Equipment changed)InventoryComponent::equipItem()requestImmediateSave("装备变更")
  • 属性提升(Attribute upgraded)AttributeComponent::upgradeAttribute()requestImmediateSave("属性提升")

实现:

void SaveManager::requestImmediateSave(const std::string& reason) {
_immediateSaveRequested = true;
_immediateSaveReason = reason;
}

bool SaveManager::consumeImmediateSaveRequest(std::string& outReason) {
if (!_immediateSaveRequested) return false;

_immediateSaveRequested = false;
outReason = _immediateSaveReason;
_immediateSaveReason.clear();
return true;
}

2. 周期定时器(60 秒)

tickAutoSave() 控制:Adventure-King/Classes/Save/SaveManager.cpp L500-L524

bool SaveManager::tickAutoSave(float dt) {
if (!_autoSaveEnabled) return false;
if (!_hasActiveSaveSlot) return false;

_autoSaveTimer += dt;
if (_autoSaveTimer >= _autoSaveInterval) {
_autoSaveTimer = 0.0f;
return true; // Signal: should auto-save now
}
return false;
}

任意成功存档后会重置计时器,以避免短时间内重复存档:Adventure-King/Classes/Scenes/GameScene.cpp L966-L970

来源: Adventure-King/Classes/Save/SaveManager.cpp L500-L547

Adventure-King/Classes/Scenes/GameScene.cpp L975-L1011


GameScene 集成

GameScene::processSaveRequests() 负责编排自动存档的执行:Adventure-King/Classes/Scenes/GameScene.cpp L975-L1011

void GameScene::processSaveRequests(float dt) {
auto saveManager = SaveManager::getInstance();
if (!saveManager || !_player) return;
if (!saveManager->hasActiveSaveSlot()) return;

// Priority 1: Immediate requests (state changes)
std::string reason;
if (saveManager->consumeImmediateSaveRequest(reason)) {
std::string toast;
const bool ok = saveToActiveSlotInternal("自动保存", reason, toast);
if (_uiController && !toast.empty()) {
_uiController->showToast(toast, ok ? Color3B::GREEN : Color3B::RED);
}
return;
}

// Priority 2: Periodic timer (60s)
if (saveManager->tickAutoSave(dt)) {
std::string toast;
const bool ok = saveToActiveSlotInternal("自动保存", "", toast);
if (_uiController && !toast.empty()) {
_uiController->showToast(toast, ok ? Color3B::GREEN : Color3B::RED);
}
}
}

即使处于暂停状态也会调用,以支持背包/技能菜单中的操作触发自动存档:Adventure-King/Classes/Scenes/GameScene.cpp L883-L892

来源: Adventure-King/Classes/Scenes/GameScene.cpp L975-L1011

Adventure-King/Classes/Scenes/GameScene.cpp L864-L935


场景集成点

GameScene 存档入口

方法触发方式采集内容目标槽位
saveToActiveSlotInternal()手动存档(暂停菜单)完整世界状态_activeSaveSlot
processSaveRequests()自动存档(定时/状态变化)完整世界状态_activeSaveSlot
returnToMapScene()玩家通过传送门离开关卡仅玩家数据(缓存)无(运行时缓存)

来源: Adventure-King/Classes/Scenes/GameScene.cpp L937-L973

Adventure-King/Classes/Scenes/GameScene.cpp L975-L1011

Adventure-King/Classes/Scenes/GameScene.cpp L745-L767


GameUIController 存档回调

GameUIController 将保存回调传递给 PauseMenu Adventure-King/Classes/Scenes/GameScene.cpp L586-L646

:

_uiController->init(
this, _player, getLevelName(),
[this]() { returnToMapScene(); },
[this](bool paused) { setGamePaused(paused); },

// Manual save callback
[this](std::string &outMessage) -> bool
{
return this->saveToActiveSlotInternal("保存", "", outMessage);
},

[this]() {
return _levelMap && _player && _levelMap->isPointAtGate(_player->getPosition());
},

// Load game callback
[](const SaveSlotData& saveData) {
auto registry = SceneRegistry::getInstance();
SceneID targetID = registry->getSceneIDByName(saveData.progressData.currentSceneName);

// Populate runtime cache
auto saveManager = SaveManager::getInstance();
saveManager->setRuntimePlayerData(saveData.playerData);
saveManager->setRuntimePlayerPosition(Vec2(...));
saveManager->setRuntimeProgressData(saveData.progressData);

// Transition to LoadingScene
auto loadingScene = LoadingScene::createScene(targetID);
Director::getInstance()->replaceScene(transition);
}
);

注意: 该 load 回调与 HelloWorldScene 的读档流程完全一致,说明所有入口点都使用统一的读档架构。

来源: Adventure-King/Classes/Scenes/GameScene.cpp L586-L646


SaveMenuLayer 读档流程

SaveMenuLayer 会显示存档槽位并处理读档确认:Adventure-King/Classes/Scenes/Layers/SaveMenuLayer.cpp L494-L565

云同步集成: SaveMenuLayer 也会通过 CloudSyncService 处理云端上传/下载:Adventure-King/Classes/Scenes/Layers/SaveMenuLayer.cpp L633-L819

来源: Adventure-King/Classes/Scenes/Layers/SaveMenuLayer.cpp L494-L565

Adventure-King/Classes/Scenes/Layers/SaveMenuLayer.cpp L633-L819


错误处理与回退

存储回退链

当 localStorage 读取失败时,SaveManager 会回退到 JSON 文件:Adventure-King/Classes/Save/SaveManager.cpp L404-L444

1. Try readFromStorage(key) [localStorage]
↓ Failed?
2. Try readFromFile(filepath) [JSON backup]
↓ Failed?
3. Return false (no valid save)

读档校验

JsonSerializer::deserialize() 会执行结构校验(schema validation):Adventure-King/Classes/Save/JsonSerializer.cpp L606-L1034

  • 检查必需字段存在(slotIndexplayerDataprogressData
  • 校验枚举取值(role、equipment slot、attribute type)
  • 将数值 clamp 到合法范围(level ≥ 1,HP/MP ≥ 0)
  • 对缺失的可选字段填充默认值(例如旧存档缺少 isLevelCleared

向后兼容: 对缺少新字段(例如 breakDamageisLevelCleared)的旧存档,会在反序列化阶段自动完成升级/补全。

来源: Adventure-King/Classes/Save/SaveManager.cpp L404-L444

Adventure-King/Classes/Save/JsonSerializer.cpp L606-L1034


汇总表:存档 vs 读档路径

方面存档流程读档流程
入口点GameScene::saveToActiveSlotInternal()SaveMenuLayer → load 回调 → 运行时缓存
数据采集fillProgressDataForSave() + extractPlayerData()JsonSerializer::deserialize()
存储读写双写(localStorage + JSON 文件)双读(优先 localStorage,失败回退 JSON)
场景切换无(原地存档)运行时缓存 → LoadingScene → GameScene
状态恢复N/AapplyPlayerData() + 世界状态恢复
缓存使用无(直接写入磁盘)必需(场景创建前填充)
时序同步(阻塞到写入完成)异步(下一帧创建场景)

来源: Adventure-King/Classes/Scenes/GameScene.cpp L937-L973

Adventure-King/Classes/Scenes/HelloWorldScene.cpp L586-L634

Adventure-King/Classes/Scenes/GameScene.cpp L198-L306