高级指令:全屏文本信号与成就
掌握 screentext 全屏叙事 signal waitsignal 与 Godot 侧联动以及成就指令与 end 的回退语义
几何构成(speed-rouge)的 Boss 战剧情有三个需求:开打前打出一块全屏的章节标题;剧本暂停、等 Boss 登场动画播完再继续(演出由 GDScript 驱动);击败 Boss 后解锁一个成就。对话框与选项解决不了这些——你需要 screentext、signal/waitsignal 与 achievement 这组进阶指令。在上一篇里,你已经能用变量、条件分支与选项让剧情”动”起来;本篇把这些进阶指令一次讲透,并补上一块容易被忽视的内容:剧本导出时的加密保护。
这一组指令有一个共同的背景主题:回退(Rollback)。Konado 允许玩家退回上一句对话重新阅读(详见存档与回退一篇),那么当玩家回退跨越了 signal、achievement 这类”副作用指令”时会发生什么?本篇会逐一给出答案。理解这些语义,能帮你避免”成就被反复触发""音效重复播放”这类隐蔽 bug。
screentext:全屏叙事文本
当你需要展示章节标题、时间地点交代、大段旁白独白时,普通对话框显得太小、太吵。screentext 指令提供一种块形式的语法,把多行文本以 NVL(Novel-style,覆盖式全屏文本)的方式呈现在屏幕中央:
screentext {
"第三章 雨夜独行"
"她离开后的第七天,城市下起了入秋以来最大的一场雨。"
}它的交互流程是:
- 覆盖层以全屏形式居中展示文本;
- 文本逐行淡入,每行末尾带一个闪烁的三角指示器,提示玩家”还有下一行”;
- 玩家点击后展示下一行;
- 最后一行被确认后,覆盖层整体淡出,对话流程继续向下执行。
使用建议:章节标题、序章字幕、回忆片段的引言都很适合 screentext。它比一句句普通对话更有”翻页”的仪式感,也比频繁切换背景更省事。
顺带回顾一句与它互补的指令:对话框的显示与隐藏由 showtextbox 与 hidetextbox 负责(带可选的 duration 淡入淡出时长,hidetextbox 完成后还会清除角色名与文本),这部分语法细节在对话框指令一篇已经讲过,本篇不再重复。
signal:把剧情事件交给 GDScript
剧本语言擅长讲故事,但不擅长”做事”——播放一段自定义音效、给玩家加金币、触发一个过场动画,这些都属于程序逻辑。signal 指令就是剧本与程序之间的桥:
signal 好感度上升
signal 播放音效 thunder
signal 设置金币 100signal 后面可以跟任意文本内容。剧本执行到这一行时,KonadoDialogueManager 会发射它的 custom_signal(content) 信号,content 就是你在剧本里写的原文。外部 GDScript 连接这个信号即可处理:
extends Control
@export var dialogue_manager: KonadoDialogueManager
func _ready() -> void:
if dialogue_manager:
dialogue_manager.custom_signal.connect(_on_konado_dialogue_manager_play_sfx)
func _on_konado_dialogue_manager_play_sfx(content: String) -> void:
if content == "播放音效 thunder":
# 在这里接入你自己的音效播放逻辑
print("收到剧本信号:", content)约定一个简单的内容协议(比如”动作名 + 空格 + 参数”)就能让编剧在剧本里自由触发程序行为,而不必学习任何 GDScript。这也是官方示例 demo.gd 采用的模式:剧本 signal,代码侧连接 custom_signal 播放音效。
signal 的回退语义:可重放的一次性副作用
signal 是一种”可重放”的一次性副作用(Side Effect)。具体规则是:
- 玩家回退时可以跨越 signal 指令所在的位置;
- 重放到该位置时,signal 会重新发射一次,custom_signal 会被再次调用。
由此推出一条重要的实践准则:如果你的处理函数只修改”快照内”的状态(比如 KonadoVariableStore 里的变量、界面显示等会随存档与回退一起被记录的状态),那么回退加重放之后,结果与不曾回退完全一致;但如果处理函数修改的是”快照外”的状态(写文件、请求网络、调用第三方平台的接口),重复发射就会造成重复写入,你必须自己保证处理逻辑幂等(Idempotent,即重复执行结果不变),例如先检查再写入。
waitsignal:暂停等待外部事件
signal 是”剧本通知程序”,waitsignal 则反过来,是”剧本等待程序”。它的语法是:
waitsignal lightning_done执行到这一行,对话流程暂停,直到外部代码调用 KonadoDialogueManager 的 emit_wait_signal 方法并传入同名信号名才继续:
func _play_storm_animation() -> void:
# 播放一段过场动画,播完后恢复对话
animation_player.play("storm")
await animation_player.animation_finished
dialogue_manager.emit_wait_signal("storm_done")signal_name 既可以是字符串字面量(如上),也可以是标识符。典型用途包括:
- 等一段过场动画播完再继续剧情;
- 等一个小游戏(比如 QTE、拼图)结束后再回到对话;
- 等一个异步加载或网络请求完成。
设计剧本时,把 waitsignal 与 signal 配对使用,就能实现”剧本暂停、交给程序演出、演出完毕、剧本继续”的完整回合制协作。
end:终止指令与回退边界
end 是对话的终止指令,有两条铁律:
- 它会立即终结当前对话流程,位于它之后的所有内容都不会执行;
- 推荐把它写在对话结尾与 branch 分支的收尾处,让每个分支都有明确的”终点”。
branch bad_ending
kona "也许我们不会再见了。"
achievement unlock "bad_ending_reached"
end除了收尾,end 还有一个容易忽略的身份:回退边界(Rollback Boundary)。Konado 的回退机制不能跨越一条已经执行过的 end——已结束的剧本对回退来说是”过去的一章”,玩家无法通过”上一句”按钮退回上一个剧本文件里去。这保证了一个剧本播放完毕、进入下一个剧本后,回退行为不会把剧情搅乱。更系统的回退语义(快照、边界、副作用重放)会在存档与回退一篇完整展开。
成就指令:跨过它,但不会重放它
Konado 内置了三条成就指令,直接在剧本里解锁或推进成就进度:
achievement unlock "first_blood"
achievement increment "explorer" 1
achievement set_flag "secret_ending_found" true- achievement unlock:解锁一个成就;
- achievement increment:把某个计数型成就的进度加一个数值;
- achievement set_flag:设置某个标志位的值(标志型成就条件)。
这三条指令在回退体系里被特殊对待:它们是回退屏障(Rollback Barrier)。回退可以跨越成就指令,但跨越之后重新播放时,成就指令不会再次执行。换句话说,成就只增不减、只解锁不撤销——玩家解锁过”初次见面”,回退到见面之前再重新走一遍,也不会重复解锁或消失。这个设计符合玩家对成就系统的直觉:成就是对”曾经达成”的记录,而不是随剧情摇摆的状态变量。
成就指令的接收端是 KonadoAchievements 扩展组件(含 JSON 配置、条件类型、弹窗与面板层级),它的配置方法与 API 在本地化与扩展组件一篇专门讲解。
剧本导出保护:加密你的剧情数据
视觉小说的核心资产就是剧本。Konado 在导出环节提供了剧情数据加密:
- 在 Godot 的导出预设(Export Preset)中找到 Konado 分类,设置 Script Encryption Key;
- 导出时,编译后的剧情数据会被自动加密,防止通用工具直接从 PCK 包里读到明文剧本;
- 如果密钥为空或格式错误,Konado 会随机生成一个 256 位密钥,并把实际使用的密钥保存在 .godot/konado_export_credentials.cfg 中;
- 后续发布补丁或热更新时,必须使用与主包相同的导出预设与相同密钥,否则客户端无法解密新剧情。
两点务实提醒:其一,这套机制防的是”通用工具直读 PCK 明文”这种低成本窥探,不能替代专业的 DRM(数字版权管理)方案,商业项目若有强保护需求仍需另行评估;其二,务必把密钥文件与导出配置纳入你的发布流程管理,密钥一旦丢失,旧补丁与新包之间的兼容就断了。
综合示例:一段完整的雨夜过场
把本篇所有指令串起来,看一段”章节标题、触发音效、等待演出、解锁成就”的完整剧本:
screentext {
"第二章 雨夜"
"她推开了那扇门。"
}
actor show kona normal at 2
background night_street fade
signal 播放音效 thunder
play bgm rainy_theme
kona "这么晚了你还在这里。"
waitsignal storm_done
kona "刚才那道闪电,照亮了你的表情。"
branch ask_name
kona "我还不知道你的名字。"
choice "告诉她名字" -> tell_name
choice "保持沉默" -> keep_silent
end
branch tell_name
achievement unlock "first_meeting"
kona "我叫可娜多,请多指教。"
end
branch keep_silent
signal 好感度变化 1
kona "(她没有勉强你。)"
end配套的 GDScript 侧只需要两件事:连接 custom_signal 处理”播放音效”与”好感度变化”,并在雷暴动画结束时调用 emit_wait_signal(“storm_done”)。一个十几行的剧本,就完成了叙事、演出协作与成就记录的闭环。
坑点与自检
- signal 的处理函数里写文件、发网络请求,回退重放后同一请求发了两次——signal 是可重放副作用,快照外状态必须自己保证幂等(先检查再写入);
- 剧本卡在 waitsignal 不动——外部代码从未调用 emit_wait_signal(同名信号),先确认信号名逐字符一致、调用路径真的会执行;
- 玩家回退后成就消失或重复解锁——不会发生:成就指令是回退屏障,可跨越、不重放;若你的 UI 出现重复弹窗,检查的是接收端而不是剧本;
- branch 结尾忘写 end,回退边界混乱或后续剧本内容被意外执行——每个分支与每个剧本文件都以 end 收束;
- 补丁包换了导出预设,客户端解不开新剧情——补丁与主包必须同预设同密钥;密钥丢失等于断掉热更链路;
- 自检问题一:signal 与 waitsignal 的方向分别是什么?(signal 剧本通知程序;waitsignal 剧本暂停等程序,由 emit_wait_signal 解除)
- 自检问题二:end 的两个身份是什么?(终止当前对话流程;回退边界——回退不能跨越已执行过的 end)
- 自检问题三:导出加密防的是什么?(通用工具直读 PCK 明文剧本,不替代专业 DRM)
练习
- 跑通综合示例的雨夜过场:screentext 开场、signal 播雷声、waitsignal 等闪电动画、两条分支各自成就与好感度,GDScript 侧补齐连接与 emit_wait_signal;
- 制造一次”重复发射”事故:signal 处理函数里往文件追加一行,推进-回退-重放,观察文件里出现两行;再改成幂等写法修复;
- 配置一次带 Script Encryption Key 的导出,故意用另一个预设打补丁验证失败,再换回同密钥验证恢复,把结论写进你的发布清单。