Skip to content

Restoration

woctordho edited this page Feb 5, 2022 · 36 revisions

存档系统

设计目标

  • 保存和读取各种前端组件(图片、BGM等)的状态
  • 在对话中设置变量,用变量控制剧情分支,在读档时恢复变量
  • 在小游戏中设置变量,此时变量取决于玩家输入
  • 在章节选择界面、文本回顾界面、流程图界面中跳转到一条已经到达过的对话
  • 读档时恢复文本回顾界面的内容
  • 在文本回顾界面中回跳时,恢复更早的文本
  • TODO:根据变量改变文本
  • TODO:用随机数设置变量
  • 占用的硬盘空间足够小
  • 读写文件的速度足够快

序列化

  • 每个有状态的前端组件实现IRestorable,它的状态表示成IRestoreData
  • CheckpointSerializer.Write中,每个IRestoreDataBinaryFormatter序列化成二进制流,再用DeflateStream压缩,再写到文件
    • 存档的状态一般是很稀疏的,所以需要压缩
    • 我们没有提供存档加密功能,因为代码是开源的,如果需要加密请自行解决
    • TODO:BinaryFormatter已经被微软官方deprecate了,以后我们可能会换成JSON或者其他格式

保存游戏状态

  • 需要存档的状态(checkpoint)由全局变量(CheckpointManager.globalSave.data["global_variables"])、节点历史(GameState.nodeHistory)和对话序号(GameState.currentIndex)唯一确定
    • Checkpoint与分支选项无关,如果两个选项从同一个旧节点跳转到同一个新节点,它们不会造成不同的状态
  • 节点历史的内容:
    • 首先是每个节点的名称
    • 剧本可以有循环,为了方便在节点历史中指定一段,每个节点每次出现时要记录出现次数
    • 以及每个小游戏结束后变量的hash,由NodeHistory.interrupts记录
  • 每条对话都有一个GameStateRestoreEntry,保存这条对话的(default stage的)代码块运行之前所有前端组件的状态,记录在CheckpointManager.globalSave.reachedDialogues
    • 读档时,先恢复目标对话的状态,再运行目标对话的代码块
  • 为了支持持续动画(详见演出系统),并减少存档占用的空间,GameStateRestoreEntry分为两类:
    • GameStateCheckpoint完整记录所有前端组件的状态
    • GameStateSimpleEntry不记录这些状态,只记录上一个GameStateCheckpoint的相距步数
      • 读档时先读取上一个GameStateCheckpoint,再快进到GameStateSimpleEntry的位置
    • 我们避免使用full/raw/real checkpoint之类的说法
  • 快进时,如果之前到达过一条对话,无论现在的变量如何,这条对话都视为已读
    • CheckpointManager有方法IsReachedAnyVariables(string nodeName, int dialogueIndex)
  • 可能的节点历史的数量随着分支选项和小游戏结果的数量增加而指数增长,但是我们只会记录玩家遇到过的节点历史。在最坏情况下,checkpoint占用的内存和硬盘随着游玩时间而线性增长

书签

  • 存档/读档界面中的每个存档称为书签(Bookmark),记录节点历史的hash和对话序号
    • 读档时,提供这些信息就能恢复checkpoint
  • 自动存档、快速存档与手动存档的格式是一样的

文本回顾界面

  • Log界面中的每个LogParam记录一条对话的节点名称、出现次数和对话序号
    • 在log界面中回跳时,提供这些信息就能恢复checkpoint
    • Log界面中的每条对话的节点历史一定是当前的节点历史的前缀,因此每个LogParam不用记录完整的节点历史,只要记录一个节点的名称和出现次数
  • LogParam.displayData记录这条对话的文本,包括所有语言的翻译,以便于随时切换语言
    • 如果文本中有需要插值的变量,LogParam.displayData则会记录插值后的文本
Clone this wiki locally