任务架构
没有脚本,但有逻辑:Romergo中的节点和变量是如何构造的
当人们谈论游戏编辑器中的逻辑时,通常很快就会想到脚本。某些地方作者写JavaScript,有些地方写Python,而有些地方则研究引擎自己的语言。这几乎提供了无限的自由,但同时也带来了语法错误、死循环、不兼容的插件以及连作者本人在半年后都不敢打开的代码。
在 Romergo,我走了另一条路。任务中没有任意脚本。不能插入函数、发起网络请求或直接访问播放器。取而代之的是,故事由有限但足够有表现力的集合组成:逻辑节点、变量、条件和效果。
这并不意味着在 Romergo 中只能做简单的分支。可以记住玩家的选择、计算分数、显示和隐藏选项、在后续章节选择不同路线、构建随机事件以及创建互动界面。只是作者不是下达“执行这段代码”的命令,而是描述“在这种状态下应该发生这个”。
图表已经是一个程序
最基本的逻辑 Romergo 直接位于章节地图上。场景通过过渡连接,特殊节点决定游戏流程的下一步。
如果大幅简化,小任务看起来是这样的:
开始
↓
与守卫对话
↓
有通行证吗?
├─ 有 → 封闭档案
└─ 没有 → 绕行路径
这已经是可执行的逻辑。Player 进入起始节点,显示场景,读取历史状态并选择合适的转换。作者看到的是一个清晰的路线图,而不是一堆 if、goto 和服务标识符。

*在 Builder 中实际的 Flow 片段:最终指控(Final Accusation)场景将决定传递到 Switch,然后几个 If / Else 检查状态并将故事引向不同的结局。底部打开的 Logic 面板显示了所有可用的集合:Dead End、Checkpoint、If / Else、Switch、Random 和 Teleport。*
逻辑面板目前有六个主要节点:
- **游戏结束** 会停止当前路线。这可能是失败、糟糕的结局或者只是故意关闭的结果。
- **检查点**在此处保存状态并开始新的回溯历史。玩家可以返回到检查点,但不能回到它之前。
- **如果 / 否则**检查一个规则。为真的出口按照匹配的条件前进,为假的出口保留备用路线。
- **开关**适用于多于两个选项的情况。其分支从上到下检查:匹配第一个条件的分支会触发,没有条件的分支成为默认选项。
- **随机** 会选择已连接的出口之一。随机会在状态中保存种子和步进,因此保存点可以恢复,而不会将玩家混乱地传送到另一条路线上。
- **传送** 本身不做任何决定。它帮助打断长距离的视觉连接,并将大地图的远端部分巧妙地连接起来。
起点和终点框定章节,普通场景展示内容,而逻辑节点将它们变成路线。在这个层面上就可以构建线性故事、分支、多个结局、随机事件和安全返回点——无需编写一行脚本。
变量——是历史的记忆
如果故事需要记住之前发生的事情,单靠一个图是不够的。为此,任务中有三种简单类型的变量:
boolean存储事实:是否找到钥匙,英雄是否撒谎,警报是否启动;number存储数字:信任度、健康值、线索数量或声望点;string存储选定值:路线名称、派系或决策代码。
每个变量都有一个键、一个对作者清晰明了的标签和初始值。数值变量还可以额外描述最小值和最大值,而服务变量可以从玩家的状态面板中隐藏,或仅在满足条件时显示。
这里的关键在于变量属于整个任务的进程,而不是某一场景。如果在第一章玩家获得了通行证,在第四章可以检查同一个标志。通过终点进入下一章时,状态不会被重置。它会与选择的选项、应用的效果、检查点以及当前的随机步骤一起保存。
形成了一个简单的作者循环:
玩家的选择会记录一个值
↓
变量存储状态
↓
条件稍后会读取它
↓
图表、场景或界面会作出反应
例如,在第一场景中,选择“显示找到的照片”可能会设置 portrait_revealed = true。稍后,**如果/否则**节点会检查这个标志并打开一个新的对话场景。再之后,终端界面只会向那些查看过照片的玩家显示额外的记录。
一个变量连接任务的三个不同部分。为此无需手动在场景或章节之间传递值:它们读取一个共享的进度状态。

*变量 route 在一个卡片中设置了稳定的关键字、易懂的名称、值类型和状态可见性。与选择和分支的关联位于同一页面的下方。*
选择写入,分支读取
在普通场景中,更直观的方式来改变状态是给选项添加效果。在可视化编辑器中,这看起来像赋值:
“信任玛丽” → ally = "mary"
“独自离开” → ally = "none"
之后,Switch 可以根据 ally 的值将玩家引导到所需的场景。变量页面单独显示哪些选择会写入变量,以及哪些分支会读取变量。这是一个小细节,但在大型任务中,它可以替代在几十个场景中痛苦的搜索。

*Switch 分支是从上到下检查的。在此示例中,Laboratory 路线在 route = lab 时触发,而 Habitat 在 route = habitat 时触发。*
条件支持常规比较:
- 等于和不等于;
- 大于,大于或等于;
- 小于,小于或等于;
- 值存在或尚未创建。
在简单的情况下,作者可以直接在 If / Else 或 Switch 的设置中选择变量、运算符和值。对于更复杂的逻辑,通用的 runtime 理解**所有条件**、**任一条件**和否定的分组。
这个规则可以结构化地描述为:
{
"op": "all",
"conditions": [
{ "op": "gte", "key": "evidence", "value": 3 },
{ "op": "eq", "key": "alarm_active", "value": false }
]
}
这看起来像一个小脚本,但本质上不是。在这里不能调用函数或随意更改内容。Romergo 事先知道所有允许的操作,检查结构,并在 Builder 的预览和 Player 的发布中以相同方式执行。
效果:无需编程语言的小命令
条件读取状态,而效果改变状态。Runtime Romergo 支持多种原子操作:
set写入一个具体值;inc和dec增加或减少数字;toggle切换逻辑标志;clamp将数字保持在设定范围内。
在普通的选择检查器中,主要场景故意保持简单:选择变量并通过 set 记录新值。在高级界面逻辑中,可以将条件和效果设置为可检查的结构。例如,终端状态之间的转换可以同时记录决策、添加线索并触发警报:

*效果直接附加到答案选项:三个选项将值 lab、habitat 和 server 分别写入 route。变量字段和新值即使在狭窄的检查器中也保持分开。*
[
{ "op": "set", "key": "terminal_decision", "value": "cut-power" },
{ "op": "inc", "key": "evidence", "amount": 1 },
{ "op": "toggle", "key": "alarm_active" }
]
这种方法有一个重要的限制:它不是任意公式计算器。不能写 evidence * trust / 2、运行循环或创建自己的函数。复杂的行为是由多个明确步骤组合而成的,逻辑扩展是通过新的可验证操作 runtime 实现的,而不是通过执行未知代码。
对于作者来说,这稍微不那么无边无际,但故事仍然是可预测的。同样的效果可以在发布前进行检查、保存、在玩家之间同步,并在加载进度后重现。
为什么在逻辑节点内暂时还没有另一个 Flow
尽管当前方法是可预测的,但它有一个明显的缺点:对于没有开发经验的人来说,“变量——操作符——数值”的形式已经可能看起来像编程。而当旁边出现条件组和多个效果时,声明式结构对 Player 是安全的,但不一定对作者来说简单。
似乎很自然,最终会在逻辑内部采用可视化节点。与其使用条件列表,不如连接 **与**、**或**、**非** 块,对变量进行比较并改变其值。在纸面上,这看起来比文本或 JSON 更友好。
但随后我设想了一个真实的情景:作者在章节地图上打开一个逻辑节点,却在其中发现另一个 Flow,里面有自己的节点和连接。结果是图中套图。需要知道自己当前所在的位置,如何返回故事层级,以及在两个 Flow 中哪个地方查找错误。对于新手来说,这种嵌套可能比现有表单更可怕。
因此,目前采用的是折衷方案。故事路线保持可视化,而节点内部的条件和效果则以紧凑的显式字段显示。这是可行的,但我认为界面还没有最终定型。可能以后会出现更易理解的现成模板、逐步设置和可视化模块的组合——这样的组合真正减少复杂性,而不仅仅是将其转移到另一个编辑器中。
逻辑不仅存在于地图上
界面场景使用相同的状态。小部件、消息、回答选项、任务、通知或媒体流只有在 showWhen 匹配时才会出现。指标值可以关联到变量,而小部件状态之间的转换可以在延迟后、根据条件或在玩家操作后触发。
例如,飞船电脑屏幕可能会这样表现:
1. 一开始只能看到被锁定的终端。
2. 在玩家选择后,效果会设置 terminal_unlocked = true。
3. 条件会打开飞船布局和新的按钮。
4. 点击按钮会改变小部件的状态并增加 evidence。
5. 当达到 evidence >= 3 时,会出现隐藏的通知,并且新的任务分支将变得可用。
章节地图、普通选择和交互界面不会创建三个独立的系统。它们读取并修改同一个状态对象。因此,在对话中找到的线索可能会打开界面元素,而在界面内的操作会改变后续场景。
正是在这里,声明式逻辑开始感觉像真正的编程。只不过作者不是处理文件、函数和事件总线,而是处理历史中易于理解的实体:事实、选择、条件和过渡。
为什么我故意不添加任意 JavaScript
插入脚本的可能性似乎是通向任何新机制的最短路径。但一旦发布的任务获得执行任意代码的权限,整个产品模型就会改变。
需要决定哪些浏览器 API 对这段代码可用,如何限制网络和存储,如何处理无限循环,如何将任务离线移植,如何在多人游戏中同步,以及如何确保旧项目在更新 Player 后继续运行。
声明性规则比任意函数要无聊得多——也正因为如此,它更可靠。它可以在运行前进行验证。可以理解它读取和写入了哪些变量。可以安全地在链条中间保存进度。可以在浏览器、离线构建和内嵌 Player 中以相同方式执行任务。
有限的词汇在这里并不妨碍逻辑,而是设定了它的界限。如果出现真正有用的操作,最好将其添加到通用 runtime 中,并为所有作者提供相同的经过验证的行为,而不是让每个项目去发明自己的引擎。
作为甜点——自定义的 HTML 和 CSS
同时 Romergo 仍然为真正的代码留有空间,尤其是在最安全的地方:普通场景的布局中。
在项目中可以在 HTML 和 CSS 上创建自定义外观主题,并在不同章节中使用它。HTML 设置预设区域的位置——台词窗口、头像、角色名、文本、问题和选择按钮。CSS 可以显著改变这些区域:边框、背景、卡片形状、间距、排版以及对屏幕尺寸的响应。
现成的主题 **Standard**、**Minimalism**、**Brutalism**、**Romance** 和 **Neon** 显示已确认的范围,无需手动排版。创建自定义主题时,Romergo 会复制所选模板:之后可以编辑 HTML 和 CSS,并立即在真实预览 Player 中查看效果。

*这里没有概念艺术:左侧显示的是内置主题 Brutalism 的真实 CSS,右侧是同一代码在手机预览中的效果。桌面/移动切换开关可以立即检查响应规则,而自定义主题从所选模板的副本开始工作。*
但边界保持不变。在自定义 HTML 中没有 <script>、事件处理程序、表单、iframe 和外部资源。CSS 无法加载外部的 url() 或 @import。必需的场景区域必须保持原位;如果模板丢失了它们,Player 会恢复为安全的标准布局。
也就是说,HTML 和 CSS 决定了 **故事的外观**,而节点、变量、条件和效果则决定了 **故事的运行方式**。
我正是喜欢这样的划分。作者可以让任务在视觉上与其他任务不同,但它的流程仍然是整体可验证 runtime 的一部分。外层可以被彻底重新着色和重建,而不必将每个主题变成独立的程序。
没有代码的代码——并不意味着没有逻辑
Romergo 的目标不是用图片来替代编程语言。图形不适合复杂的数学,而一套原子效果也不会成为通用的自动化引擎。
然而,对于互动故事来说,通常需要其他东西:记住一个事实、修改计数器、开启选项、选择路径、显示界面状态、保存回溯点,并将所有这些决策传递到下一章。
为此,节点和变量并不是脚本的简化版本,而是故事本身更直接的语言。作者描述的不是给计算机的指令,而是世界的因果关系:玩家做出了选择,状态发生了变化,故事做出了反应。
如果想要一点真正的代码,HTML 和 CSS 仍然等着做甜点。