跳到主要内容

存档数据结构

相关源文件

本页记录 Adventure-King 用于游戏持久化的各类数据结构。这些结构定义了可被序列化为 JSON 的完整状态快照,并可存储到本地数据库或同步到云端。

关于负责编排存/读档操作的 SaveManager 单例,请参见 SaveManager。关于存储层实现细节,请参见 Storage Layer。关于存/读档流程与运行时缓存机制,请参见 Save and Load Flow

概览

存档系统采用层级结构,其中 SaveSlotData 是根容器。所有数据结构都是用于 RapidJSON 序列化的纯 C++ struct:只包含 POD 类型、STL 容器与嵌套的存档结构——不包含指向游戏对象的指针,也不包含 Cocos2d 类型(唯一例外是 Vec2,它作为可序列化的坐标对使用)。

核心设计原则是 持久化与运行时分离:存档数据结构完全独立于 PlayerCharacterMonsterBaseLevelMap 等玩法类。运行时对象与存档数据之间的转换发生在明确边界:SaveManager::extractPlayerData()SaveManager::applyPlayerData()

核心存档层级结构

SaveSlotData 结构

SaveSlotData 是完整游戏状态快照的顶层容器。它按槽位(0-4)存储,包含元信息、玩家状态与世界进度。

来源Adventure-King/Classes/Save/SaveData.h L171-L186

字段说明

字段类型说明
slotIndexint槽位编号(0-4)
saveTimestampint64_tUnix 时间戳(毫秒)
gameVersionstring用于兼容性检查的版本字符串
playerDataPlayerSaveData玩家角色完整状态
progressDataGameProgressSaveData世界状态与关卡进度

saveTimestamp 会在存档时通过 std::chrono::system_clock::now() 生成;在云存档冲突场景中,客户端会用它做“最后写入优先(last-write-wins)”的选择依据。gameVersion 字段用于后续存档格式变更时的迁移逻辑预留。

来源: Adventure-King/Classes/Save/SaveData.h L171-L186

Adventure-King/Classes/Save/SaveManager.cpp L342-L344


GameProgressSaveData 结构

GameProgressSaveData 捕获游戏世界状态:包括玩家位置、关卡完成状态、生成点状态、竞技场进度,以及所有存活怪物的快照。

来源Adventure-King/Classes/Save/SaveData.h L109-L168

位置与关卡状态

字段类型说明
currentSceneNamestring场景标识(例如 "ORIGIN_MUSHROOM"),由 SceneRegistry 使用
playerPosX, playerPosYfloat读档时玩家出生点的世界坐标
isLevelClearedbool当前关卡胜利条件是否已满足(传送门是否解锁)
unlockedLevelsvector<string>在地图选择中可访问的场景名列表
playTimeSecondsint64_t本次会话时长(秒)(从 _sessionStartTime 计算到存档时刻)

isLevelCleared 用于避免一个问题:玩家胜利后存档,再读档时传送门会被重新锁住。对于旧存档如果缺少该字段,加载时会根据生成点/竞技场状态进行推断。

来源Adventure-King/Classes/Save/SaveData.h L112-L164

Adventure-King/Classes/Scenes/LevelMap.cpp L435-L447

EnemySpawnPointState(敌人生成点状态)

用于追踪 TMX 对象层 enemy_g 中定义的敌人生成点的激活状态。每个生成点包含:

字段类型说明
monsterTypestring怪物类型标识(例如 "goblin", "goblu"),由 createMonsterByType() 使用
posX, posYfloat生成点的世界坐标
countint该点要生成的怪物数量
hasSpawnedbool该生成点是否已经触发过

hasSpawnedtrue 时,LevelMap::updateEnemySpawns() 在读档后会跳过该点,避免重复生成。生成点通过 (monsterType, posX, posY, count) 的组合键进行唯一识别。

来源Adventure-King/Classes/Save/SaveData.h L126-L133

Adventure-King/Classes/Scenes/LevelMap.cpp L709-L735

ArenaState(竞技场状态)

表示单个竞技场遭遇(按波次的锁房间战斗)的进度:

字段类型说明
arenaIDstring来自 TMX 的唯一标识(例如 "arena_01"
currentWaveIndexint当前/下一波将要生成的波次索引(从 0 开始)
isActivatedbool玩家是否进入过竞技场触发区
isFinishedbool是否已完成全部波次

isActivatedtrueisFinishedfalse 时,竞技场门会关闭。系统使用 currentWaveIndex 决定下一波生成哪一波。如果当前波仍有怪存活,它们会通过 MonsterState 被恢复(见下文)。

来源Adventure-King/Classes/Save/SaveData.h L136-L143

Adventure-King/Classes/Scenes/LevelMap.cpp L773-L863

MonsterState(怪物状态)

保存时刻所有存活怪物的快照:包含 HP、位置与竞技场归属关系:

字段类型说明
monsterTypestring类型标识(小写:"goblin", "goblu", "obscur"
arenaIDstring若为竞技场怪物则为 arenaID;世界怪物则为空字符串
posX, posYfloat世界坐标
currentHP, currentMPfloat当前生命与法力
breakMeterintBoss 破韧条进度(非 Boss 为 0)

arenaID 用于把怪物关联到对应竞技场:当非空时,会通过 LevelMap::registerRestoredArenaMonster() 恢复,并挂接死亡回调以推进波次;世界怪物 arenaID 为空,直接恢复到场景中。

来源Adventure-King/Classes/Save/SaveData.h L148-L158

Adventure-King/Classes/Scenes/LevelMap.cpp L866-L923

嵌套数据结构

AttributesSaveData(属性存档数据)

用于存储属性值的简单键值映射:

AttributeType 枚举会被 cast 成 int 以便 JSON 序列化;反序列化时会把整数 key 解析回 AttributeType。空 map 表示默认/零值。

来源Adventure-King/Classes/Save/SaveData.h L11-L17

Adventure-King/Classes/Save/JsonSerializer.cpp L11-L46

EquipmentSaveData(装备存档数据)

保存一个 EquipmentWeapon 对象的全部属性:

字段类型说明
idint来自 GameConfig::Equipment 的装备 ID
name, descriptionstring展示文本
slotintEquipmentSlot 枚举 cast 成 int
levelint装备等级(独立于玩家等级)
attributeBonusAttributesSaveData提供的属性加成
spritePathstring图标 sprite 路径
isWeaponbool用于区分武器字段的判别标记

武器特有字段(当 isWeapon == true):

字段类型说明
weaponTypeintWeaponType 枚举 cast 成 int
attackDamagefloat基础武器伤害
attackRangefloat攻击距离
attackSpeedfloat攻速倍率
attackAnimationPrefixstring攻击动画名前缀(例如 "spr_man_attack_sword"
attackFrameCountint攻击动画帧数

isWeapon 决定序列化时是否填充武器字段,以及反序列化时是否将装备 cast 为 Weapon

来源Adventure-King/Classes/Save/SaveData.h L20-L41

Adventure-King/Classes/Save/SaveManager.cpp L819-L855

Adventure-King/Classes/Save/JsonSerializer.cpp L50-L125

SkillSaveData(技能存档数据)

捕获被动与主动技能状态:

字段类型说明
idint来自 GameConfig::Skill 的技能 ID
name, descriptionstring展示文本
isPassivebool用于区分技能类型的判别标记

主动技能字段:

字段类型说明
cooldownfloat基础冷却(秒)
manaCostfloat每次施放的 MP 消耗
breakDamageint对 Boss 破韧条的伤害(未知/旧存档为 -1)
currentCooldownfloat存档时剩余冷却时间

被动技能字段:

字段类型说明
attributeBonusAttributesSaveData装备时提供的属性加成

breakDamage 使用 -1 作为旧存档缺字段的哨兵值;加载时会从 GameConfig::Skill::getBreakDamageForSkillId() 回填缺失值。

来源Adventure-King/Classes/Save/SaveData.h L44-L65

Adventure-King/Classes/Save/JsonSerializer.cpp L128-L187

SettingsSaveData(设置数据)

该结构与槽位存档分离,用于保存用户偏好:

字段类型默认值说明
musicVolumefloat1.0f背景音乐音量(0.0-1.0)
sfxVolumefloat1.0f音效音量(0.0-1.0)
musicEnabledbooltrue音乐总开关
sfxEnabledbooltrue音效总开关

设置项独立于槽位存档存储(key 为 ak_settings),对所有存档全局生效:启动时读取,设置菜单修改时保存。

来源Adventure-King/Classes/Save/SaveData.h L189-L198

Adventure-King/Classes/Save/SaveManager.cpp L587-L648

Adventure-King/Classes/Save/JsonSerializer.cpp L358-L772

Serialize(序列化)函数

JsonSerializer::serialize(const SaveSlotData &data) 会构建一个嵌套的 RapidJSON 文档结构:

  1. 元信息块meta):槽位、时间戳、版本
  2. 玩家块player):递归序列化全部 PlayerSaveData 字段
  3. 进度块progress):场景名、坐标、生成点/竞技场/怪物数组

每个嵌套结构(AttributesSaveDataEquipmentSaveDataSkillSaveData)都有对应 helper:

  • serializeAttributes():把 map<int, float> 转为以字符串为 key 的 JSON object
  • serializeEquipment():处理装备字段与武器判别字段
  • serializeSkill():处理技能字段与主动/被动判别字段

来源Adventure-King/Classes/Save/JsonSerializer.cpp L190-L336

Deserialize(反序列化)函数

JsonSerializer::deserialize(const std::string &json, SaveSlotData &outData) 解析 JSON,并用 lambda helper 安全提取字段(带默认值):

auto getInt = [&](const rapidjson::Value &obj, const char *key, int def) -> int {
return (obj.HasMember(key) && obj[key].IsInt()) ? obj[key].GetInt() : def;
};

该模式保障向后兼容:若字段缺失(旧格式存档),则使用默认值。诸如 breakDamageisLevelCleared 这类新字段会使用哨兵默认值(-1false),并在加载流程中触发回填逻辑。

来源Adventure-King/Classes/Save/JsonSerializer.cpp L358-L772

向后兼容策略

缺失字段默认值回填策略
SkillSaveData 中的 breakDamage-1读档时从 GameConfig::Skill 查询回填(SaveManager.cpp L919-L936
isLevelClearedfalseenemySpawnPoints.empty() && arenas.empty() 或显式检测推断(LevelMap.cpp L71-L82
aiBlessingBonus空 map视为“没有祝福生效”
passiveSlotSkillIds空 vector旧存档的 -1 占位会被过滤

来源Adventure-King/Classes/Save/JsonSerializer.cpp L180

Adventure-King/Classes/Save/SaveManager.cpp L919-L936

存档数据与运行时对象的映射

下图展示存档数据结构如何映射到对应的运行时游戏对象:

来源Adventure-King/Classes/Save/SaveManager.cpp L772-L1129

Adventure-King/Classes/Scenes/GameScene.cpp L945-L1093

提取(运行时 → 存档数据)

SaveManager::extractPlayerData(PlayerCharacter *player) 会把玩家状态深拷贝到 PlayerSaveData

  1. 基础字段:直接复制 levelexperience、技能点等
  2. 当前状态:复制 currentHPcurrentMP、伤害倍率等
  3. 属性:调用 AttributeComponent::getBaseAttributes() 并序列化为 AttributesSaveData
  4. 装备:遍历 getEquippedItems()getInventoryItems(),把每个 Equipment 转成 EquipmentSaveData
  5. 技能:遍历 getLearnedSkills(),把每个 Skill 转成 SkillSaveData,并复制技能槽位数组

提取过程只做数据复制,不会保存任何运行时对象的指针或引用。

来源Adventure-King/Classes/Save/SaveManager.cpp L772-L1007

应用(存档数据 → 运行时)

SaveManager::applyPlayerData(PlayerCharacter *player, const PlayerSaveData &data) 会从存档数据重建玩家状态:

  1. 基础字段:通过 setter 设置等级、经验与技能点
  2. 当前状态:直接设置 HP/MP
  3. 属性:对 map 中每条属性调用 AttributeComponent::setBaseAttribute()
  4. 装备:用工厂函数把 EquipmentSaveData 转为 Equipment/Weapon,并调用 equipItem()addItemToInventory()
  5. 技能:用工厂函数把 SkillSaveData 转为 ActiveSkill/PassiveSkill,调用 learnSkill(),再恢复槽位分配

应用过程使用与玩法代码一致的 public API 来修改玩家,从而保证一致性与正确触发回调。

来源Adventure-King/Classes/Save/SaveManager.cpp L1009-L1129

世界状态提取

GameScene::fillProgressDataForSave(GameProgressSaveData &outData) 捕获世界状态:

  1. 位置:从 SceneRegistry 设置 currentSceneName,从玩家位置设置 playerPosX/Y
  2. 关卡清理:从 LevelMap::isLevelCleared() 设置 isLevelCleared
  3. 生成点:调用 LevelMap::exportEnemySpawnPointStates() 获取生成状态数组
  4. 竞技场:调用 LevelMap::exportArenaStates() 获取竞技场进度数组
  5. 存活怪物:遍历所有 MonsterBase 子节点,把类型/HP/位置/arenaID 写入 MonsterState

该函数在存档时(手动与自动)于暂停状态下调用,确保快照一致。

来源Adventure-King/Classes/Scenes/GameScene.cpp L945-L1093

世界状态恢复

读档时,GameScene 会按顺序调用:

  1. LevelMap::applyEnemySpawnPointStates():标记生成点为已触发,防止重复生成
  2. LevelMap::applyArenaStates():恢复竞技场波次索引与门禁状态
  3. 怪物重建:对每个 MonsterState 创建 MonsterBase 并设置 HP/位置
  4. LevelMap::registerRestoredArenaMonster():把竞技场怪物挂接到推进波次的死亡回调
  5. LevelMap::resumeActiveArenasIfNeeded():若竞技场处于激活且怪物数为 0,则生成当前波次

该多阶段恢复能确保竞技场不会重复生成已完成波次,并正确重建未完成波次。

来源Adventure-King/Classes/Scenes/LevelMap.cpp L709-L923

Adventure-King/Classes/Scenes/GameScene.cpp L666-L778

存储 Key 生成

存档数据结构会存到 SQLite(通过 cocos2d::localStorage)与旧版 JSON 文件中。Key 生成统一在 SaveManager 中实现:

ak_ 前缀命名空间用于避免与其它系统在同一数据库中产生 key 冲突。JSON 文件路径遵循相同模式:saves/save_0.jsonsaves/save_1.json 等。

来源Adventure-King/Classes/Save/SaveManager.cpp L16-L25

Adventure-King/Classes/Save/SaveManager.cpp L193-L201

Adventure-King/Classes/Save/SaveManager.cpp L249-L257

数据结构保证

所有存档数据结构遵循这些设计规则:

  1. 无指针:只使用值类型、字符串与容器
  2. 可默认构造:所有 struct 都有 = default 构造,便于初始化
  3. 仅使用 STL 容器vectormapstring 以保证广泛兼容
  4. 枚举转 int:所有枚举都 cast 成 int 以便 JSON 序列化
  5. 哨兵默认值:缺失字段使用哨兵值(-1、空字符串等)触发回填逻辑

这些保证使得:

  • 线程安全序列化:不存在共享状态或指针
  • 深拷贝简单:整个存档状态可轻易复制
  • 版本迁移友好:缺字段用默认值而不是报错
  • 云同步容易:JSON 可以直接通过 HTTP 传输

来源Adventure-King/Classes/Save/SaveData.h L1-L199