该篇简单分析 Fungus 是如何处理存档数据,并提供自行实现存档功能的思路。
如果想看自行实现存档功能的思路,可以直接看最后一节。
存档内容分析
以下取示例场景中第二个 Block 的 Save Point 内容。
为了方便查看,对内容有所修改,包括符号、空格、换行。
"{\"savePointKey\": \"Queens Chamber\",
\"savePointDescription\": \"13:03 30 五月, 2022\",
\"sceneName\": \"SaveGame\",
\"saveDataItems\": [ {
\"dataType\": \"FlowchartData\",
\"data\": \"{
\\\"flowchartName\\\":\\\"Flowchart\\\",
\\\"stringVars\\\":[],
\\\"intVars\\\":[],
\\\"floatVars\\\":[],
\\\"boolVars\\\":[
{\\\"key\\\":\\\"has_umbrella\\\",
\\\"value\\\":true},
{\\\"key\\\":\\\"burnt_bottom\\\",
\\\"value\\\":false} ]
}\"
},
{
\"dataType\": \"NarrativeLogData\",
\"data\": \"{
\\\"entries\\\": [
{\\\"name\\\": \\\"John\\\",
\\\"text\\\": \\\"Phew, I can't believe I managed to escape from that horde of man-eating robotic ants.\\\"},
{\\\"name\\\": \\\"John\\\",
\\\"text\\\": \\\"Good thing I brought my trusty umbrella or those fiends would have been the end of me!\\\"},
{\\n
\\\"name\\\": \\\"John\\\",
\\\"text\\\": \\\"Now where am I?\\\"} ]
}\"
} ]
}"
首先是 SavePoint 类提供的描述信息,之后是 SaveData 编码生成的 SaveDataItem 内容。
一般都是两项:"FlowchartData" 和 "NarrativeLogData".
前者记录 Flowchart 的数据,包括名称和变量集。
后者记录剧情日志。
生成 Json 的过程
以下文字描述可能不清晰,可以直接看结论。
- 数据一步步传递,直到 SavePointData 调用 SaveData.
- SavePointData 会传入一个 SaveDataItem 列表给 SaveData.
- SaveData 会从持有的 Flowchart 获取其 FlowchartData, 及名称和变量集。
- SaveData 会调用 JsonUtility 将 FlowchartData 生成 Json 并存入 SaveDataItem 列表。
- 然后从 FungusManager 获得 NarrativeLog, 该类提供自身的 Json 内容。
- 现在 SaveData 就给 SaveDataItem 列表就存入了两项内容,"FlowchartData" 和 "NarrativeLogData".
- SavePointData 现在持有 标识符,描述,场景名,以及装载过的数据列表。
- 于是 SavePointData 类调用 JsonUtility 将自己持有的数据全部生成 Json.
- 最终这条 Json 数据会传递给 SaveHistory 持有。
关键方法是 SaveData.Encode
.
这对我们的意义在于,当我们希望另外实现存档功能时,可以自行获取 FlowchartData 进行 Json 化处理,即我们不用额外去获取 Fungus 变量了。
一般的存档系统,并不需要存储一个存档点列表,很多时候也不需要叙事日志。当然即便需要也可以使用 NarrativeLog 类。
我们能够跳脱出 Fungus 本身的调用路径,去自行实现额外的存储逻辑。当然,Fungus 本身的事件也就不太好触发了。
不过,还有问题没有解决,就是如何恢复执行,这就需要我们去查看加载过程了。
加载 Json 并恢复运行的过程
以下文字描述可能不清晰,可以直接看结论。
- SaveManager 读取文件,获得 Json 字符串,然后使用 JsonUtility 即可解码并赋值给 SaveHistory 对象。
- SaveHistory 加载最新的 SavePoint. 该过程首先获得相应的 Json字符串。(SaveHistory 本身就是持有 Json 字符串列表,所以这些 Json 当然不会被解码)
- 将 Json 交由 SavePointData 进行解码。
- SavePointData 会解码出一个自己的对象,包括 标识符、描述、场景名、SaveDataItem 列表(Json)。
- 将 SaveDataItem 列表交由 SaveData 解码,该过程会将 Json 进一步解析为 FlowchartData 和 NarrativeLog.
- 其中 FlowchartData 会交由其类本身提供的解码函数,该函数会将所有变量重新装填进去。
- 至此,数据内容都已经恢复。
- 在 SavePointData 中,恢复了数据后就会配置事件,加载场景后即可恢复 Block 执行。
重点方法是 SavePointData.Decode
和 SaveManager.ExecuteBlock
, 下面还是结合代码分析。
// in SavePointData
public static void Decode(string saveDataJSON)
{
// 解析出一个自己的对象
var savePointData = JsonUtility.FromJson<SavePointData>(saveDataJSON);
UnityAction<Scene, LoadSceneMode> onSceneLoadedAction = null;
// 配置场景加载完成后的事件
onSceneLoadedAction = (scene, mode) => {
if (mode == LoadSceneMode.Additive ||
scene.name != savePointData.SceneName) {
return;
}
SceneManager.sceneLoaded -= onSceneLoadedAction;
// 从场景中获取 SaveData, 目的是获得 Flowchart 对象以恢复变量数据
var saveData = GameObject.FindObjectOfType<SaveData>();
if (saveData != null) {
// SaveData 会负责解析 SaveDataItem 内容,并配置变量和日志
saveData.Decode(savePointData.SaveDataItems);
}
// 实际上执行的就是 SaveManager 中的 ExecuteBlock 方法
SaveManagerSignals.DoSavePointLoaded(savePointData.savePointKey);
};
// 添加事件加载完成后的事件,注意,此时还未执行!
SceneManager.sceneLoaded += onSceneLoadedAction;
// 通过场景名称,加载场景
SceneManager.LoadScene(savePointData.SceneName);
// 场景加载完成后,才开始恢复数据和 Block
}
这个方法对于我们的意义在于,如何恢复 Flowchart 数据。
如果是自行实现存档系统,那么只需要读取持久化的 FlowchartData 内容,将其 Json 传递给 SaveData 对象的 Decode 方法,就会完成解码和装配的工作。
注意,这些过程应该在场景加载之后进行!
// in SaveManager
protected virtual void ExecuteBlocks(string savePointKey)
{
// 执行特定 Block
SavePointLoaded.NotifyEventHandlers(savePointKey);
// 获取 Save Point 对象,就是各个 Save Point Command
var savePoints = UnityEngine.Object.FindObjectsOfType<SavePoint>();
for (int i = 0; i < savePoints.Length; i++)
{
var savePoint = savePoints[i];
// 比对 Save Point 标识符以确定是哪条命令
if (savePoint.ResumeOnLoad &&
string.Compare(savePoint.SavePointKey, savePointKey, true) == 0)
{
// 配置需要执行的 Block 及 Command 索引
int index = savePoint.CommandIndex;
var block = savePoint.ParentBlock;
var flowchart = savePoint.GetFlowchart();
// 执行 Block
flowchart.ExecuteBlock(block, index + 1);
break;
}
}
}
这个方法对于我们的意义在于,如何恢复 Block 执行。
该方法会获取场景中的所有 Save Point Command, 然后比对标识符。
比对成功后,配置好要执行的 Block 信息,开始执行。
自行实现存档功能的思路
不考虑存档点列表和叙事日志,以及事件触发,仅考虑恢复 Flowchart 的运行。
注意:暂未经过具体测试,以下仅为理论内容。
存储过程
我们需要存储的内容有三
- 场景标识。
- 用于恢复 Flowchart 的 FlowchartData.
- 用于恢复游戏运行的 Command 标识。
因此,我们需要一个类包含这些内容。
场景标识通过 SceneManager 可以获取。
FlowchartData 可以直接通过 FlowchartData.Encode
获取。
Command 标识根据需求会有所不同,一般来说可以记录当前运行到的 Command. 如果不在 Block 运行中,Fungus 通常是依靠变量或点击恢复到运行中,所以也不需要特殊处理。
然后通过 JsonUtility.ToJson
将该存档对象 Json 化,写入文件即可。
加载过程
- 读取文件,通过
JsonUtility.FromJson
实例化一个存档对象。 - 通过场景标识,加载场景。
- 从存档对象获取 FlowchartData 对象,可以直接通过
FlowchartData.Decode
装配所有变量。 - 如果需要,获取 Command 标识,执行
flowchart.ExecuteBlock
方法即可恢复执行。