游戏
驱动运行中游戏的节点——开始与退出、推进对话、读写玩家偏好,以及管理存档与历史
Game 分类是蓝图与运行中剧情之间的接缝。这里的一切都在和活着的 NarraLeaf 运行时说话:游玩本身、屏幕上的对话、玩家的偏好、项目的本地存档,以及对话历史。标题画面、设置画面、存读档网格和回顾界面,都由本页的节点搭起来
本页每一个节点都需要运行中的游戏。编辑器画布没有游戏——放在那里的 Game 节点会以Host API unavailable (use Dev Mode)失败,读取它的 Blueprint Value 预览为空。这些行为一律在开发模式中验证
约定
- 基本上要么 pure 要么 latent。 四十一个取值节点是
Pure,可用于event、function、macro图,也可以支撑 Blueprint Value。四十七个是Latent,因此只能用于event和macro——latent 节点在function图里没有地方安放一个待定的结果。第八十九个是Clear Visited,也是唯一的怪例:有副作用但同步,所以它和 latent 节点一样只能用于event和macro,自己却不是 latent - 有 Host API 不等于有一局游戏。
List Saves、Get Save Metadata、Get Save Time、Get Save Playtime、Get Save Preview、Delete Save、List Auto Saves和Get Latest Auto Save读取项目的存档库,在两局游戏之间、没有游戏运行时也能工作。结局、走过的路线、DLC 与构建变体那几组读取节点同样如此——结局画廊本来就是从标题界面打开的,那时还没有任何游戏。凡是序列化、反序列化或指挥活动游戏的节点都需要一局游戏,没有就失败 - 状态取值节点会降级,偏好取值节点不会。 每一个纯状态读取——
Is In Game、Is Game Overlay、Get Nametag、Get Speaker Avatar、Get Speaker Color、Get Dialog Text、Is Narrator、Is Dialog Waiting、Get Choice Count、Get Notifications、Is NVL Mode、Is Text Read、Has Read Text、Can Undo History、Can Redo History、Get Track Volume、两个游玩时长读取,以及结局、走过的路线、DLC、构建变体与角色的那些读取——在什么都没有播放时都给出中性值:false、null、0、空数组,颜色给不透明白,音轨音量给 1。正因如此,同一个共享 Page 才能按它们分支,画廊的一行也才能把「锁住」的样子直接绑到引脚上。十六个偏好取值节点是例外:它们读的是活动GamePreference,没有游戏就失败。Is Dialog Shown也是其中之一——它读的是showDialog偏好,只是名字按问题起而不是按键名起 - 偏好初始化挂在
On Game Ready上,不要挂App Boot。App Boot可能早于LiveGame存在就运行,而没有活动游戏的偏好写入会失败。参见事件 - 存档 id 会被校验。 id 会被 trim,不能为空,不能含路径分隔符或控制字符。磁盘上的文件名由 id 的哈希派生;你选的 id 保存在记录的 metadata 里
游玩
Start Game和Quit Game是尾节点:它们把控制权交给运行时,没有执行出口,所以后续该发生的事属于剧情,或者属于它们打开的那个 Page
Start Game
blueprint.game.startStory · Latent
Start Game(开始游戏)启动节点卡片上选中的 Story 与 Scene,并切换到游戏舞台。游戏首帧就绪后,当前 Page 栈会被藏到游戏下面,因此之后调用Go Page是把目标叠加在舞台之上,而不是替换舞台
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
storyId | in · data | string | 可选。连线优先于卡片 |
sceneId | in · data | string | 可选。连线优先于卡片——Import Progress 的 Scene 就接到这里 |
startBlockId | in · data | string | 可选,卡片上标作 From Row,只能连线。从该行进入场景,而不是从场景开头 |
卡片字段
| 字段 | 含义 |
|---|---|
Story | 要运行的剧本。除非 storyId 已连线,否则必填 |
Scene | 要进入的场景。选项按选中的 Story 过滤。除非 sceneId 已连线,否则必填 |
十次里有九次作者要的是选择器:它列出工程自己的剧本,也拼不错。这几个引脚是为第十次准备的——写图的时候还不知道目标是哪一个,比如一个「继续」按钮要从玩家上次的位置接着走
Quit Game
blueprint.game.quit · Latent
Quit Game(退出游戏)结束当前游玩,并打开卡片上选中的 Page。一次调用做完全部事情:丢弃活动游戏与会话、取消仍在等待的启动请求、清空对话名牌、恢复普通 Page 显示,然后导航——不需要再接一个Go Page
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — |
卡片字段
| 字段 | 含义 |
|---|---|
Page | 游戏结束后打开的 Surface。必填 |
Is In Game
blueprint.game.isInGame · Pure
Is In Game(是否在游戏中):只要游戏舞台可见且存在会话就为 true——即使上面开着 Page 叠层也一样。普通 Page 预览、首次Start Game之前、Quit Game之后为 false
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
isInGame | out · data | boolean |
Is Game Overlay
blueprint.game.isGameOverlay · Pure
Is Game Overlay(是否为游戏覆盖层):当前 Surface 实例是否以游戏之上的 UI 叠层身份运行。Start Game或Load Save之后用Go Page打开的 Page 返回 true,内建的游戏 UI Surface(例如对话槽位)也返回 true;通过nl.frame嵌入的子 Page 继承父级的答案。普通应用 Page、Page 预览,以及Quit Game打开的那个 Page 返回 false
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
isGameOverlay | out · data | boolean |
答案在 Page 实例创建时就锁定,所以正在播放退出动画的暂停菜单仍返回 true,哪怕Quit Game已经开始拆掉会话。要问运行时是否还在,用Is In Game;要问一个共享 Page 该显示成什么样,用这个
推进剧情
Next
blueprint.game.next · Latent
Next(下一步)触发运行时的虚拟点击。有对话包装层暴露的点击目标时点它,把「打字中就先补完这句、已完成就进入下一步」的判断交还给引擎;没有对话目标时才退回去点播放器的主内容。它不会强制跳过
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | 推进请求完成后触发 |
没有活动游戏运行时会失败
Skip
blueprint.game.skip · Latent
Skip(跳过)跳过当前对话。它是作者显式提供的 Skip 动作;默认对话模板不会替你绑定
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
没有活动游戏运行时会失败
Select Choice
blueprint.game.choose · Latent
Select Choice(选择选项)按原始序号选择活动选择菜单中的选项。隐藏选项不会呈现给玩家,但仍然占据序号,所以菜单给出的序号不一定连续——请把选项自带的index接回来,而不是它在列表里的位置。宿主会重新校验目标的 hidden / disabled 状态,非法选择被静默拒绝
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
index | in · data | integer | 必须是非负整数。支持卡片字面量 |
没有打开的选择菜单时会失败
Get Choice Count
blueprint.game.getChoiceCount · Pure
Get Choice Count(获取选项数量):活动选择菜单展示了多少个选项。隐藏选项不计入。菜单关闭后回到 0
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
count | out · data | integer |
对话
对话文本、说话人或完成状态变化时,绑定到对话的元素都会被 flush,因此在On Flush里读取下列取值节点的控件会随剧情推进保持最新,而不需要重新挂载
Get Nametag
blueprint.game.getNametag · Pure
Get Nametag(获取名牌):当前说话人的名字。旁白、没有说话人、说话人名为空或全是空白时为 null
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
nametag | out · data | string | 没有说话人时为 null |
默认对话模板用普通nl.text做名牌,并在该控件自己的蓝图里驱动它——Init与On Flush接到Get Nametag,再用一次判空控制文本和不透明度。这里没有任何特殊的名牌控件
Get Speaker Avatar
blueprint.game.getSpeakerAvatar · Pure
Get Speaker Avatar(获取说话人头像):说话角色的对话头像。引擎是从该角色的活的立绘上解析出来的,所以它天然反映角色当前穿着的差分,并且在撤销、读档、跳过之后依然正确。项目无法映射回资源的头像 URL 会解析为 null,而不是猜一个
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
avatar | out · data | ImageAsset|null | 可接给图片节点或图片 Blueprint Value |
Get Dialog Text
blueprint.game.getDialogText · Pure
当前屏幕上那句话的文本。没有人在说话时为空字符串
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
text | out · data | string |
Get Speaker Color
blueprint.game.getSpeakerColor · Pure
说话角色的主题色,用来给名牌或姓名条上色
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
color | out · data | RGBAColor | 没有人说话时为不透明白。这个引脚是非空颜色,没有「缺省」可以承载 |
Is Narrator
blueprint.game.isNarrator · Pure
屏幕上这一句是旁白还是有角色在说。要为旁白整体换一套对话框外观,用它比对 Get Nametag 判空干净
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
isNarrator | out · data | boolean |
Is Dialog Waiting
blueprint.game.isDialogWaiting · Pure
这一句是否已经打完字、正等玩家推进。「点击继续」的指示器绑的就是这个引脚
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
isWaiting | out · data | boolean | 没有宿主、屏幕上也没有句子时为 false——指示器必须在任何游戏存在之前就能排好版 |
Get Character
blueprint.game.getCharacter · Pure
工程里某一个角色的记录,在卡片上选。和上面三个「说话人」读取不同,它问的不是谁在说话——它是人物档案、好感度界面或者角色画廊用来画一个不在台上的人的那条路
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
name | out · data | string | 没选或角色已不存在时为空 |
characterColor | out · data | RGBAColor | 没有记录时为默认色 |
characterAvatar | out · data | ImageAsset|null | |
found | out · data | boolean |
卡片字段
| 字段 | 含义 |
|---|---|
Character | 工程里的某个角色 |
Is NVL Mode
blueprint.game.isNvlMode · Pure
Is NVL Mode(是否为 NVL 模式):游戏当前是否处于 NVL 模式
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
isNvlMode | out · data | boolean |
Get Notifications
blueprint.game.getNotifications · Pure
Get Notifications(获取通知):运行时当前正在显示的通知,镜像自引擎的通知组件。没有通知时为空数组
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
notifications | out · data | array |
条目字段
| 字段 | 含义 |
|---|---|
id | 通知 id |
message | 通知文本 |
默认通知模板不使用该节点:它的列表项模板直接从项属性里读message
Show Dialog
blueprint.game.showDialog · Latent
Show Dialog(显示对话框)把showDialog偏好设为 true,让对话 UI 重新显示。它只改运行时偏好——不改 UI 文档,也不推进剧情
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
没有活动游戏运行时会失败
Hide Dialog
blueprint.game.hideDialog · Latent
Hide Dialog(隐藏对话框)把showDialog设为 false。隐藏期间对话包装层仍保留自己的交互语义,所以Next、Skip和之后的Show Dialog照常工作
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
没有活动游戏运行时会失败
Toggle Dialog Display
blueprint.game.toggleDialogDisplay · Latent
Toggle Dialog Display(切换对话框显示)读取showDialog并写入相反值——一个 UI 控件配一个节点。它不碰别的:不碰对话文本、不碰名牌、不碰文字速度
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
没有活动游戏运行时会失败
Is Dialog Shown
blueprint.game.isDialogShown · Pure
上面三个的读取那一半。它是顶着状态读取名字的偏好读取——它读的值就是 showDialog——所以没有活动游戏时它失败,而不是降级
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
isShown | out · data | boolean |
偏好
下面每一对都对应活动游戏上引擎GamePreference的一个字段。取值节点是 pure 的,可安全放进 Blueprint Value;设值节点是 latent 的,写入前先校验——值不合法时节点失败,而不是被夹到范围内。showDialog在这里没有配对节点,由Show Dialog、Hide Dialog、Toggle Dialog Display和Is Dialog Shown覆盖
本节的设值节点在没有活动游戏运行时都会失败,取值节点也一样,只有 Get Track Volume 例外——它读的是引擎的总线混音器而不是GamePreference。要为新一局游戏播种偏好,把设值节点接在On Game Ready上——它在LiveGame已存在之后、第一句剧情之前运行
Get Sentence Speed
blueprint.game.getCps · Pure
Get Sentence Speed(获取文字速度)读取cps,即一句话逐字显示的每秒字符数
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
cps | out · data | float |
Set Sentence Speed
blueprint.game.setSentenceSpeed · Latent
Set Sentence Speed(设置文字速度)写入cps。必须是大于 0 的有限数字
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
cps | in · data | float | 支持卡片字面量 |
Get Auto Forward
blueprint.game.getAutoForward · Pure
Get Auto Forward(获取自动前进)读取autoForward,即一句话播完后游戏是否自行推进
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
autoForward | out · data | boolean |
Set Auto Forward
blueprint.game.setAutoForward · Latent
Set Auto Forward(设置自动前进)写入autoForward。必须是布尔值
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
autoForward | in · data | boolean |
Get Auto Forward Delay
blueprint.game.getAutoForwardDelay · Pure
读取autoForwardDelay,即自动前进在一句话打完之后要等多久才推进
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
autoForwardDelay | out · data | float |
Set Auto Forward Delay
blueprint.game.setAutoForwardDelay · Latent
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
autoForwardDelay | in · data | float | 支持卡片字面量 |
Get Skip
blueprint.game.getSkip · Pure
Get Skip(获取跳过)读取skip,即是否允许跳过
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
skip | out · data | boolean |
Set Skip
blueprint.game.setSkip · Latent
Set Skip(设置跳过)写入skip。必须是布尔值
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
skip | in · data | boolean |
Get Skipping
blueprint.game.getSkipping · Pure
读取skipping,即此刻是不是正在跳过。上面的skip是「允许跳过」这个设置,这一个是状态
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
skipping | out · data | boolean |
Set Skipping
blueprint.game.setSkipping · Latent
从图里开关跳过——一个会自锁的跳过按钮,而不是按住才跳的 Skip 动作
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
skipping | in · data | boolean |
Get Skip Read Text
blueprint.game.getSkipReadText · Pure
读取skipReadText,即跳过是否会在玩家没见过的那一句停下来。这里说的「读过」就是本页「已读文本」一节背后的那份记录
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
skipReadText | out · data | boolean |
Set Skip Read Text
blueprint.game.setSkipReadText · Latent
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
skipReadText | in · data | boolean |
Get Skip Delay
blueprint.game.getSkipDelay · Pure
Get Skip Delay(获取跳过延迟)读取skipDelay,单位毫秒——玩家按住多久之后开始跳过
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
skipDelay | out · data | float |
Set Skip Delay
blueprint.game.setSkipDelay · Latent
Set Skip Delay(设置跳过延迟)写入skipDelay。必须是大于等于 0 的有限数字,单位毫秒
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
skipDelay | in · data | float | 支持卡片字面量 |
Get Skip Interval
blueprint.game.getSkipInterval · Pure
Get Skip Interval(获取跳过间隔)读取skipInterval,单位毫秒——跳过时两句之间的间隔
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
skipInterval | out · data | float |
Set Skip Interval
blueprint.game.setSkipInterval · Latent
Set Skip Interval(设置跳过间隔)写入skipInterval。必须是大于 0 的有限数字,单位毫秒
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
skipInterval | in · data | float | 支持卡片字面量 |
Get Game Speed
blueprint.game.getGameSpeed · Pure
Get Game Speed(获取游戏速度)读取gameSpeed,即全局速度倍率
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
gameSpeed | out · data | float |
Set Game Speed
blueprint.game.setGameSpeed · Latent
Set Game Speed(设置游戏速度)写入gameSpeed。必须是大于 0 的有限数字
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
gameSpeed | in · data | float | 支持卡片字面量 |
Get Global Volume
blueprint.game.getGlobalVolume · Pure
Get Global Volume(获取全局音量)读取globalVolume,即其它所有声道都要乘上的总音量
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
globalVolume | out · data | float |
Set Global Volume
blueprint.game.setGlobalVolume · Latent
Set Global Volume(设置全局音量)写入globalVolume。必须是大于等于 0 的有限数字
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
globalVolume | in · data | float | 支持卡片字面量 |
Get BGM Volume
blueprint.game.getBgmVolume · Pure
Get BGM Volume(获取背景音乐音量)读取bgmVolume,即背景音乐声道
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
bgmVolume | out · data | float |
Set BGM Volume
blueprint.game.setBgmVolume · Latent
Set BGM Volume(设置背景音乐音量)写入bgmVolume。必须是大于等于 0 的有限数字
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
bgmVolume | in · data | float | 支持卡片字面量 |
Get SFX Volume
blueprint.game.getSoundVolume · Pure
Get SFX Volume(获取音效音量)读取soundVolume,即音效声道
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
soundVolume | out · data | float |
Set SFX Volume
blueprint.game.setSoundVolume · Latent
Set SFX Volume(设置音效音量)写入soundVolume。必须是大于等于 0 的有限数字
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
soundVolume | in · data | float | 支持卡片字面量 |
Get Voice Volume
blueprint.game.getVoiceVolume · Pure
Get Voice Volume(获取语音音量)读取voiceVolume,即语音声道
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
voiceVolume | out · data | float |
Set Voice Volume
blueprint.game.setVoiceVolume · Latent
Set Voice Volume(设置语音音量)写入voiceVolume。必须是大于等于 0 的有限数字
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
voiceVolume | in · data | float | 支持卡片字面量 |
Get Voice Fade Duration
blueprint.game.getVoiceFadeDuration · Pure
Get Voice Fade Duration(获取语音淡出时长)读取voiceFadeDuration,单位毫秒——一句话结束时语音淡出所需的时间
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
voiceFadeDuration | out · data | float | 卡片上标为 Voice Fade |
Set Voice Fade Duration
blueprint.game.setVoiceFadeDuration · Latent
Set Voice Fade Duration(设置语音淡出时长)写入voiceFadeDuration。必须是大于等于 0 的有限数字,单位毫秒
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
voiceFadeDuration | in · data | float | 卡片上标为 Voice Fade。支持卡片字面量 |
Get Voice End Mode
blueprint.game.getVoiceEndMode · Pure
Get Voice End Mode(获取语音结束模式)读取voiceEndMode:一句话结束时仍在播放的语音该如何处理
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
voiceEndMode | out · data | string | "fade"、"stop"、"none" 之一 |
Set Voice End Mode
blueprint.game.setVoiceEndMode · Latent
Set Voice End Mode(设置语音结束模式)写入voiceEndMode。必须恰好是 "fade"、"stop" 或 "none"
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
voiceEndMode | in · data | string | 支持卡片字面量 |
Get Track Volume
blueprint.game.getTrackVolume · Pure
玩家给工程某一条音轨设的音量。混音器是工程自己声明的一棵音轨树——需要的话可以给每个角色一条总线——而这一对正是「一条音轨一行」的设置界面的搭法
它是本节里的异类:值来自引擎的总线混音器而不是GamePreference,所以没有活动游戏它也不失败。没选音轨、或者没有东西可问时读作 1——绑在它上面的滑块会停在顶端,而不是在页面还没连完的时候塌到零
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
audioTrackId | in · data | string | 可选。连线优先于卡片 |
volume | out · data | float | 没有指名音轨时为 1 |
卡片字段
| 字段 | 含义 |
|---|---|
Track | 工程的某一条音轨 |
Set Track Volume
blueprint.game.setTrackVolume · Latent
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
audioTrackId | in · data | string | 可选。连线优先于卡片 |
volume | in · data | float | 支持卡片字面量 |
卡片字段
| 字段 | 含义 |
|---|---|
Track | 工程的某一条音轨 |
存档
存档是项目本地的,以你自己选的 id 为键。同一个 id 写第二次就是覆盖
List Saves只返回玩家存档。自动存档住在保留 id 命名空间里——@autosave.<n>——由List Auto Saves列出;两个集合永不重叠。这是刻意的:基于List Saves做的存读档网格从不需要过滤掉 Studio 自己的簿记条目,自动存档界面也从不需要藏起玩家的存档位。在别的地方,自动存档 id 就是一个普通存档 id,可以原样喂给Load Save、Get Save Preview、Get Save Metadata和Delete Save
Save fields
一份存档携带两类数据。一类由引擎在每次存档时写入:存档将从哪一句继续、那句话由谁说出、以及时间戳。另一类由你决定,存档字段就是声明它的地方
从 Save Game 或 Get Save Metadata 卡片底部的按钮打开。每个字段有名称、类型和默认值
声明过的字段会成为具名引脚:在 Save Game 上是输入,在 Get Save Metadata 上是输出。这份列表属于工程,不属于打开它的那个节点,因此工程内所有存档节点显示的字段完全一致,一个字段只需声明一次
重命名字段只更换引脚标签,已有的连线全部保留。改变类型会重置它的默认值
| 行为 | 结果 |
|---|---|
| 从声明该字段之前写下的存档中读取它 | 该字段的默认值 |
| 删除字段 | 已写入的值仍留在持有它们的存档文件中 |
会执行到的 Save Game 上,声明过的字段留空 | 工程 lint 以 error 报告 blueprint/save-field-empty |
两个节点上的 metadata 引脚都保留。在 Save Game 上,声明字段会覆盖写在它之上,因此同名的键以声明字段为准;在 Get Save Metadata 上,它仍然返回完整的存储对象
Save Game
blueprint.game.save.write · Latent
Save Game(保存游戏)把活动游戏序列化进项目的本地存档库。Capture为 true 时还会截一张舞台 PNG 存为该存档的预览图;截图失败会被上报,但不会连累存档本身
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
id | in · data | string | 支持卡片字面量 |
metadata | in · data | json | 可选。任意 JSON——对象、数组、字符串、数字、布尔或 null。未接线时写入 null |
screenshot | in · data | boolean | 可选。卡片上标为 Capture。支持卡片字面量 |
| 每个声明字段一个 | in · data | 该字段的类型 | 以字段名标注。类型支持时可用卡片字面量 |
没有活动游戏会话时会失败
Load Save
blueprint.game.save.load · Latent
Load Save(读取存档)读入一份存档并为它放弃当前进度:清空 router 及其历史,创建新的游戏实例并反序列化;等 router 退出后,Page 栈重新被藏到舞台之下——所以这之后的Go Page打开的是游戏之上的叠层
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
id | in · data | string | 支持卡片字面量 |
failed | out · exec | — | 存档缺失、损坏,或没有活动游戏运行时走这一支 |
没有 next:读档成功就把正在跑的游戏整个换掉了,这张图也就没什么好接着跑的。failed 是它唯一的出口,也是游戏把这件事告诉玩家的那一支
Delete Save
blueprint.game.save.delete · Latent
Delete Save(删除存档)删掉一份存档。删一个本来就不存在的 id 也算成功,所以删除按钮不必先检查。它只需要项目的存档命名空间——不需要运行中的游戏
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
id | in · data | string | 支持卡片字面量 |
List Saves
blueprint.game.save.listIds · Latent
List Saves(列出存档)列出项目里玩家存档的 id。顺序不保证。损坏的、或 metadata 不是玩家存档的文件会被跳过,而不是报错
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
ids | out · data | array | string 数组 |
Get Save Metadata
blueprint.game.save.getMetadata · Latent
Get Save Metadata(获取存档元数据)读取一份存档的作者数据。每个声明字段各占一个引脚;metadata 引脚返回完整的存储对象,包含没有任何字段声明过的键。系统字段不属于两者:id、记录类型、createdAt、updatedAt 与预览图分别由Get Save Time和Get Save Preview读取
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
id | in · data | string | 支持卡片字面量 |
metadata | out · data | json | 存档不存在或没有用户 metadata 时为 null |
| 每个声明字段一个 | out · data | 该字段的类型 | 以字段名标注。存档中没有该字段时给出它的默认值 |
Get Save Time
blueprint.game.save.getTime · Latent
Get Save Time(获取存档时间)读取一个槽位的写入时间。存档库会给每条写入的记录盖时间戳,该节点把它们读回来。存档界面无需再把时间写进自己的 metadata 并与之保持同步
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
id | in · data | string | 支持卡片字面量 |
savedAt | out · data | float | 槽位最后一次写入的时间。存档不存在时为 0 |
createdAt | out · data | float | 槽位首次写入的时间。存档不存在时为 0 |
exists | out · data | boolean |
两个时间都是 1970 年 1 月 1 日 UTC 以来的毫秒数,与 List Auto Saves 报告的单位相同,Time 分类的每个节点都接受它
空槽位读 exists 判断。存档不存在时 savedAt 为 0,而 0 同时也是 1970 年的一个有效时刻
Get Save Line
blueprint.game.save.getLine · Latent
Get Save Line(获取存档语句)读取一份存档将从哪一句继续,以及那句话由谁说出。引擎每次序列化都会记下这两项,该节点把它们读回来。存档界面不必在存档时去读 backlog,再把最后一条写进自己的字段
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
id | in · data | string | 支持卡片字面量 |
line | out · data | string | 存档不存在,或存档写于任何对话播放之前时为空串 |
speaker | out · data | string | 旁白,以及尚无对话的存档为空串 |
exists | out · data | boolean |
它不是 backlog 的最后一条。backlog 记的是最后显示过的那一句,这两个字段记的是存档将从哪一句继续。从叠层界面存档,或在最后一句之后还执行了非对话动作时,两者并不相同
Get Save Playtime
blueprint.game.save.getPlaytime · Latent
某个存档槽写下时,那一局已经玩了多久——存读档网格在缩略图下面写的那个「12 小时 34 分」
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
id | in · data | string | 支持卡片字面量 |
playtimeSeconds | out · data | float | |
playtimeMilliseconds | out · data | float | |
recorded | out · data | boolean | 在开始记录游玩时长之前写下的存档为 false——槽在,数字不在 |
exists | out · data | boolean |
排版用 Time 里的 Format Duration
Get Save Preview
blueprint.game.save.getPreview · Latent
Get Save Preview(获取存档预览)把存档里的截图变成当前会话内可用的ImageAsset。这张图不会导入项目资源库;直接接给Set Image Asset或图片 Blueprint Value 即可
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
id | in · data | string | 支持卡片字面量 |
preview | out · data | ImageAsset|null | 存档不存在或没有截图时为 null |
自动存档
自动存档按项目配置,位置在项目 → 游戏:开关、间隔,以及环保留多少个槽位。它随项目一起出货。图里不排任何日程——调度器自己在跑,并且只在剧情自上次写入以来推进过时才写,所以间隔再短,闲置的游戏也不产生开销。写入沿环轮转,最旧的先被覆盖,因此一直自动存档也只占用有界的磁盘
Auto Save
blueprint.game.autoSave.write · Latent
Auto Save(自动保存)立刻写一次自动存档,落在调度器所用的同一个环的下一个槽位,并附带截图。它同时忽略开关设置和「剧情已推进」这道门——这一次是作者显式要求的——写完之后与定时产生的自动存档没有任何区别。执行继续:自动存档不会打断正在进行的游玩
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
没有正在运行的游戏时会失败,与其它存档写入节点一致
List Auto Saves
blueprint.game.autoSave.list · Latent
List Auto Saves(列出自动存档)列出保留命名空间里的全部自动存档,从新到旧。调小环容量后遗留的槽位仍会被列出——改配置不该让玩家的存档凭空消失——只是不会再被轮转覆盖
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
entries | out · data | array | 对象数组,字段见下 |
count | out · data | integer |
条目字段
| 字段 | 含义 |
|---|---|
id | 存档 id。可直接喂给Load Save、Get Save Preview、Get Save Metadata和Delete Save |
slot | 在环内的槽位序号 |
timestamp | 最后写入时间,Unix 毫秒 |
createdAt | 首次写入时间,Unix 毫秒 |
metadata | 写入方附带的内容,没有时为 null |
条目带的是 id,而不是序列化后的存档:SavedGameData 那样的数据块在图里毫无用处,而 id 能直接接进既有的存档节点
Get Latest Auto Save
blueprint.game.autoSave.latest · Latent
Get Latest Auto Save(获取最新自动存档)取最新的一条自动存档。这是「继续游戏」按钮的专用节点——等价于List Auto Saves的第一条,省掉取下标和取字段那几个节点
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
id | out · data | string | 没有自动存档时为空字符串 |
hasAutoSave | out · data | boolean | 接上它来禁用「继续」 |
timestamp | out · data | float | 最后写入时间,Unix 毫秒。没有自动存档时为 0 |
历史
对话历史——也就是回顾记录——是引擎自己保存的、已经过去的台词与选择菜单,并且可以回放到其中任意一点
它是一条带播放头的时间线,这七个节点就是它的两侧。Get History 是播放头之前的一切:已经读过的台词,按顺序。往回退会移动播放头,被它退过去的那些台词就成了未来,Get Future History 返回它们,Redo Next History Entry 再走回去。Can Undo History 与 Can Redo History 则是一对回顾按钮据以禁用自己的东西
Get History
blueprint.game.history.get · Latent
Get History(获取历史)把回顾记录作为一个扁平数组给出,从旧到新,可以直接绑到列表控件上
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
entries | out · data | array | 对象数组,字段见下 |
count | out · data | integer |
条目字段
| 字段 | 含义 |
|---|---|
id | 历史 token。喂给Restore From History |
type | 台词为 "say",已确定的选择为 "menu" |
text | 句子文本,或菜单的提示语。源里没有时为空字符串 |
character | say 条目的说话人名牌;菜单与旁白为 null |
voice | say 条目的语音 id;没有时为 null |
selected | menu 条目里被选中的选项文本;say 条目与未确定的菜单为 null |
isPending | 当前还在屏幕上、尚未提交的那一条为 true |
没有游戏运行时为空数组
Restore From History
blueprint.game.history.restore · Latent
Restore From History(从历史恢复)把游戏跳回某一条回顾记录,丢弃它之后的一切。引擎允许时按该条目的快照恢复,否则退回到 undo——正因如此,它在刚刚读档、undo 栈是空的游戏上依然有效
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
id | in · data | string | 卡片上标为 Entry Id。必填;空 id 会让节点失败。支持卡片字面量 |
没有活动游戏运行时会失败
Undo Last History Entry
blueprint.game.history.undoLast · Latent
Undo Last History Entry(撤销上一条历史)回退一条——即不带目标的Restore From History。它就是回顾界面上的「返回」按钮
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
没有活动游戏运行时会失败
Redo Next History Entry
blueprint.game.history.redoNext · Latent
走进那些撤销造出来的未来——「返回」旁边的那个「前进」。用别的方式推进剧情会把那个未来丢掉,这正是它是 redo 而不是第二个 Next 的原因
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
没有活动游戏运行时会失败
Get Future History
blueprint.game.history.getFuture · Latent
播放头之前方的那些条目,形状与 Get History 给出的相同。想让玩家看见自己退回来之前走到哪的回顾界面,把它绑到第二个列表上
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
entries | out · data | array | 条目字段与 Get History 相同 |
count | out · data | integer |
没有游戏在跑时、以及什么都没退过时,都是空数组
Can Undo History
blueprint.game.history.canUndo · Pure
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
canUndo | out · data | boolean | 没有游戏在跑时为 false |
它是 pure 的,所以能直接支撑 Blueprint Value:把返回按钮的 enabled 绑上去,它就会在记录开头自己变灰,而不必每帧跑一张图
Can Redo History
blueprint.game.history.canRedo · Pure
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
canRedo | out · data | boolean | 没有游戏在跑时为 false |
已读文本
一句话在文字显示完毕的那一刻被标为已读——ADV 下是打字结束,NVL 下是等待推进。记录以消息稳定的文本 id 为键,因此能挺过剧本改动,并且跨越项目里的每一个 Story;它按项目级持久化,与任何存档文件无关。「只跳过已读文本」这类选项就建立在它之上
Is Text Read
blueprint.game.isTextRead · Pure
Is Text Read(文本是否已读):当且仅当屏幕上有一句对话并且它的消息被标为已读时为 true——要么早先的游玩里见过,要么刚刚显示完毕。没有台词在显示时为 false
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
isRead | out · data | boolean |
Clear Text Read
blueprint.game.clearTextRead · Latent
Clear Text Read(清除已读记录)清空已读记录,内存与磁盘一起清,覆盖项目里的每一个 Story。仍在屏幕上、且已经显示完毕的那一句会立刻重新标为已读——玩家正看着它
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
Has Read Text
blueprint.game.isTextReadById · Pure
指定的某一句是否被读过,按它的文本 id 来问,而不是问屏幕上那一句。上面的 Is Text Read 回答的是当前这句;这一个回答的是玩家此刻并没有在看的某一句,那正是额外内容界面或语音画廊需要的
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
textId | in · data | string | 支持卡片字面量 |
isRead | out · data | boolean | id 为空时为 false,于是连了一半的画廊行只是保持锁住,而不是把整页带垮 |
游玩时长
Get Playtime
blueprint.game.getPlaytime · Pure
当前这一局已经玩了多久。两个引脚是同一个数字的两种单位,于是要排版小时的界面和要按毫秒算数的界面各取所需
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
playtimeSeconds | out · data | float | |
playtimeMilliseconds | out · data | float |
没有东西在计时时为 0——在任何一局开始之前,标题界面问「这一局玩了多久」是一个有真实答案的正当问题,而故事预览根本就没有秒表
Get Total Playtime
blueprint.game.getTotalPlaytime · Pure
在这个工程上一共花过多少秒,跨越每一次游玩。通关界面写的是这个数,暂停菜单写的是 Get Playtime
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
totalPlaytimeSeconds | out · data | float | |
totalPlaytimeMilliseconds | out · data | float |
路线进度
Is Scene Visited 与 Is Option Picked 读的是剧情编译器随玩家推进写下的那份记录。它存在存档里,因此会随读档一起回退——它回答的是「在这一局里我走过这条路线没有」,一次性选项和剧情中途的解锁要的就是这个。下面的结局记录恰好相反,存在工程持久化里
两个读取节点都是 pure 的,所以画廊的一行可以把「锁住」的样子直接绑到引脚上,而不必每行跑一张图;id 为空时回答「没有」,而不是抛错
它不是 Has Read Text。已读记录是在一句话被显示时写下的,所以玩家只是打开过的菜单里的每个选项都算读过。Is Option Picked 是在选中的那一刻写下的
Is Scene Visited
blueprint.game.isSceneVisited · Pure
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
storyId | in · data | string | 可选。连线优先于卡片 |
sceneId | in · data | string | 可选。连线优先于卡片 |
isVisited | out · data | boolean |
卡片字段
| 字段 | 含义 |
|---|---|
Story | 要问的剧本 |
Scene | 场景,按该剧本过滤 |
十次里有九次用选择器就够了。这两个引脚是为第十次准备的:一张画廊卡放一次、从自己的参数里读出自己是哪个场景,而不是一个场景复制一张卡
Is Option Picked
blueprint.game.isOptionPicked · Pure
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
storyId | in · data | string | 可选。连线优先于卡片 |
optionId | in · data | string | 可选。连线优先于卡片 |
isPicked | out · data | boolean |
卡片字段
| 字段 | 含义 |
|---|---|
Story | 要问的剧本 |
Option | 一个选项,按该剧本过滤 |
Clear Visited
blueprint.game.clearVisited
把两份记录都清空。它有副作用但不是 latent——它在这一 tick 内清掉活动 Storable 里的两个键,不像 Clear Text Read 要写宿主持久化、必须被等待
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
结局
剧本里的一行 /ending 就是完整的声明,它的 block id 就是这里一切据以索引的身份。这四个节点是结局界面的搭法:玩家找到了哪些,以及全集是什么、好把它们摆开来对照
结局记录存在工程持久化里而不是存档里,没有任何东西会让它回退。结局界面问的是这个玩家曾经见过什么,所以建在存档记录上的画廊会在有人读了旧档之后当着他的面重新锁上条目,「8 个里的 5 个」会变少。这也意味着这四个节点都不需要正在运行的剧情——结局画廊本来就是从标题界面打开的
Is Ending Reached
blueprint.game.isEndingReached · Pure
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
isReached | out · data | boolean | 没选结局时为 false,于是连了一半的行保持锁住 |
卡片字段
| 字段 | 含义 |
|---|---|
Story | 要问的剧本 |
Ending | 该剧本里声明的一个结局 |
Get Endings
blueprint.game.getEndings · Pure
该剧本声明的全部结局,不论是否已达成——画廊摆开、并据以计数的那个全集
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
endings | out · data | array | 没选剧本时为空 |
卡片字段
| 字段 | 含义 |
|---|---|
Story | 要列出结局的剧本 |
Clear Ending State
blueprint.game.clearEndingState · Latent
把一个结局重新锁上
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
卡片字段
| 字段 | 含义 |
|---|---|
Story | 要问的剧本 |
Ending | 要忘掉的那个结局 |
Clear Endings
blueprint.game.clearEndings · Latent
把它们全部重新锁上。两个清除节点都要写宿主持久化,所以都是 latent
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — |
版本与额外内容
Get Build Variant
blueprint.game.getAppTag · Pure
这个包是工程的哪一个变体。
它是整份目录里唯一一个在游戏存在之前就已经定下值的节点。一次构建产出一个变体,它的名字在这里被代入,消费它的那次比较被折叠,这一版不走的那条分支被删除——于是变体判断背后的内容是从包里不存在,而不只是在包里到不了。如果一张图里的 Get Build Variant 最终没有决定任何分支,它会在构建闸门上被拒绝,每一种构建包括正式版都是如此:这里没有可以退回去的运行期值,所以这样的图不是一个可以容忍的泄漏,而是根本编译不出来
这也是它不收任何输入、也没有卡片字段的原因。能给它的任何东西,都是给一个已经只有一个答案的问题再加一个输入
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
appTag | out · data | string | 变体的名字。完整正式版是 main |
Is DLC Installed
blueprint.game.isDlcInstalled · Pure
某一份额外内容此刻是不是就在运行中的游戏旁边。
这个问题问的刻意是在不在,而不是有没有买。DLC 是作为一个文件出现在游戏旁边的,是玩家从哪里买的、哪里就把它放过来;它在不在,是这个包看得见的事实。玩家是否拥有它,是商店的事实、是商店插件的节点去回答的——而拿它来挡内容,会在玩家离线时把他买过的章节夺走,问「在不在」永远不会
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
isInstalled | out · data | boolean | 没选 DLC 时为 false,于是连了一半的菜单行保持隐藏 |
卡片字段
| 字段 | 含义 |
|---|---|
DLC | 工程声明的某一个 DLC |
在版本之间携带进度
试玩版和完整版是两个独立的包、两个独立的 app id,因此各有各的用户数据目录,资源保护密钥也刻意不同:正式版读不了试玩版的存档文件,也不该去读。这两个节点就是那条确实跨过去的通道,而且它刻意不是一个存档文件——它是每个作品一份的普通 JSON 文档,装着工程级变量、玩家走到哪里,以及他见过哪些场景
那份文档放在哪里由执行这件事的宿主决定,绝不由图决定:桌面宿主把这件事交给自己的主进程,Web 导出则拒绝,因为一个网页没有共享文件可写。所以两个节点都带一条失败分支和一个原因,图必须听得懂「不行」
Export Progress
blueprint.game.progress.export · Latent
把这一局持有的一切写进该作品的进度文档。它不收任何输入:跨过去的是工程声明的那一整套,而不是某一张图替另一个版本必须能读的文档挑出来的一个子集
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
next | out · exec | — | |
failed | out · exec | — | 写不了的宿主、没有进度键的构建,或者磁盘拒绝了 |
error | out · data | string | 是其中哪一种 |
Import Progress
blueprint.game.progress.import · Latent
读那份文档,并把它持有的内容应用到运行中的游戏上。
它不会跳转。 Scene 只是作为数据交出来,节点就停在这里。启动剧情的是 Start Game,而标题界面通常还有别的事要先做——一次淡出、一次确认、一个章节选择。一个自己就跳过去的节点,会成为绕开这一切的第二种开始剧情的方式
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
found | out · exec | — | |
missing | out · exec | — | 没有人导出过 |
failed | out · exec | — | 读不出来的文档 |
sceneId | out · data | string | 玩家走到了哪里。文档没有锚点时为空。接给 Start Game |
error | out · data | string |
三条出路,因为作者对三者的回答不一样。Missing 是所有从没导出过的人的常态——全新安装、直接买完整版的玩家——它通向「开始新游戏」,而不是一句道歉;把它并进 Failed 会让每一个第一次玩的人先看到一个错误。Failed 是读不出来的文档:更新的构建写的、属于别的作品的,或者已经损坏
存储
Check Storage Durability
blueprint.game.storageDurability · Latent
这个构建写下去的东西留不留得住——关于玩家存档,唯一一件宿主知道而游戏自己推不出来的事。
打包的桌面游戏把文件放在没人会去回收的用户数据目录里。Web 导出则是浏览器的客人:一个没有拿到持久化存储授权的站点,可能在存储压力下被整体清除,存档、持久变量和已读记录一起没。页面在加载时申请那份授权,而这里给出的就是它得到的答复
| 引脚 | 方向 | 类型 | 说明 |
|---|---|---|---|
in | in · exec | — | |
durable | out · exec | — | |
evictable | out · exec | — | |
unknown | out · exec | — |
三条分支,因为它们通向三种不同的措辞。Evictable 是「这个浏览器可能会删掉存档」;Unknown 是浏览器不肯说,而在真相是后者时告诉玩家前者,是一个没人许下过的承诺。这里不替你做任何决定——存储可能被回收的页面,仍然是一个能把游戏打通的页面,而玩家要不要听说这件事,属于作品本身