Skip to content

场景、对话与结局

场景

在文字冒险游戏中,故事总是发生在一个又一个的场景里。场景就像舞台,定义了玩家当前所处的环境,并为后续的剧情发展提供背景。

建议你在 script 文件夹中新建一个专门用来存放场景的文件,例如 scene.ts。当然,你也可以把场景、对话等内容写在同一个文件里,这完全取决于你的组织习惯。

创建一个场景的代码如下:

typescript
Adv.appendScene('main', {
    name: '家中',
    next: 'main-dialog',
});

让我们来逐行理解这段代码:

  • Adv.appendScene 这个函数用来向游戏中注册一个新场景。事实上,本项目的所有 API 都挂载在 ADVMaker 对象上,你将在后续的创作中频繁与它打交道。
  • 第一个参数 'main' 是场景的唯一标识符(ID)。这个 ID 非常重要,你在任何地方引用场景时都要用到它。请务必保证每个场景的 ID 是独一无二的,不能与任何其他场景或对话的 ID 重复,否则程序会报错。 ID 不应该以 __ 开头。
  • 第二个参数 是一个配置对象,里面包含了场景的各种属性:
    • name:场景显示给玩家的名称,例如 '家中''阴暗的森林'。它会出现在界面上方的标题栏,帮助玩家理解自己所处的位置。
    • next:进入该场景后立即执行动作。简单来说,就是告诉游戏“接下来该干什么”。如果不填,则默认为 null,游戏流程会暂时停止,等待你手动调用其他指令。

除了上面两个必填属性外,场景还支持几个非常实用的可选属性:

  • onEnter:一个回调函数,类型为 () => void。这个函数会在玩家进入场景时被触发。你可以用它来记录日志、播放背景音乐、改变游戏状态,或者执行任何你想一次性完成的操作。
  • onLeave:一个回调函数,类型为 () => void。这个函数会在玩家离开场景时被触发

例如:

typescript
Adv.appendScene('forest', {
    name: '幽暗森林',
    next: 'forest-dialog',
    onEnter: () => {
        // 当玩家进入森林时,在控制台输出一条信息
        console.log('玩家已进入森林区域');
    }
});

你可以使用链式调用方法。

typescript
Adv.appendScene('forest', {
    name: '幽暗森林',
    onEnter: () => {
        // 当玩家进入森林时,在控制台输出一条信息
        console.log('玩家已进入森林区域');
    }
})
    .next('forest-dialog')
    .build(); // 这个可选函数会返回整个场景对象,永远可以加在最后面

注意:链式调用设置的 next 会覆盖参数中设置的 next

对话

对话是叙事的主要手段。一段对话可以包含多句台词,并支持丰富的文本样式,让你的故事更加生动。

同样地,建议在 script 文件夹中创建一个 dialog.ts 文件来存放对话。

下面是创建一个对话的示例:

typescript
Adv.appendDialog('main-dialog', {
    script: [
        '你好!',
        '我是你的向导。',
        '接下来,让我们一同<b>冒险</b>吧',
        "Let's go!"
    ],
    next: "End"
});
  • Adv.appendDialog 用于注册一个新对话。
  • 第一个参数 'main-dialog' 是对话的唯一标识符(ID),同样需要遵守“全局唯一”的原则。
  • 第二个参数 配置对象包含:
    • script:台词内容。
      • 它可以是一个字符串数组,程序会先显示第一句,玩家每次点击屏幕后,依次显示下一句,直到全部播完。这种方式非常适合表现角色的内心独白或多轮叙述。
        • 每句台词不仅可以是纯文本,还可以是一段HTML代码。例如,你可以让文字变色或加粗:'这里有一只<span style="color:red">红色的钥匙</span>'
        • 如果你熟悉 Vue ,甚至可以直接传入 VNode 对象来实现复杂的交互组件。
      • 也可以是一个异步函数,此函数运行完后不会等待用户点击屏幕,会直接打印下一条。
      • 或者是一个选项数组(ADVChoice[])。使用 new ADVChoice(obj: ADVUserChoice) 来包装。
      • null,什么也不会干,但会阻塞直到玩家点击屏幕。
    • next:台词全部播放完毕后执行的动作
    • check:检定的配置,检定会在 next 生效之前发生。
    • in:填写一个场景的 id,表示该对话发生在的场景。若对话开始时不在该场景,则会自动移动到该场景中。若不填写则不会移动。

对话同样支持几个可选的回调:

  • onStart:在对话开始播放之前执行,类型为 () => void。你可以在这里调整角色的属性值、显示提示信息等。
  • onFinish:一个回调函数,类型为 () => void。这个函数会在玩家结束对话时被触发

此外,如果你不想触发完整的对话,只是想在游戏过程中临时插入一条消息,可以使用更轻量级的函数:

typescript
Adv.print('你注意到墙上有道裂缝...');

这种方式下,消息会直接出现在聊天记录中,且不会打断当前的游戏流程。

如果你这么写,可能会有警告:从 print 返回的 Promise 被忽略。

这个警告不会影响程序运行,但你可以通过增加 void 标识符来取消警告。

typescript
void Adv.print('你注意到墙上有道裂缝...');

以下异步方式,这种方式下,消息会直接出现在聊天记录中,并会阻塞当前程序直到用户点击屏幕。(如果是异步函数,无论如何都不会阻塞)

typescript
await Adv.print('你注意到墙上有道裂缝...');

请注意,只有 event(以 on 开头的字段)支持异步操作,且除了 event 之外的任何函数都不保证只执行1次,不应该有副作用

同样,对话支持链式调用:

typescript
Adv.appendDialog('main-dialog')
    .say('你好!') // 会加在 script 数组的末尾
    .say('我是你的向导。')
    .say('接下来,让我们一同<b>冒险</b>吧')
    .say("Let's go!")
    .next('End')
    .build(); // 这个可选函数会返回整个对话对象,永远可以加在最后面

链式调用会覆盖参数中配置的 next,但不会覆盖 script,此外,在 next 函数使用后,就不能再连接 say 了。

游戏的结局

每个故事都需要一个终点。ADVMaker 提供了一种优雅的方式来结束游戏:

typescript
Adv.end("故事结束")

end 函数接受一个字符串参数,这个字符串会作为“死亡原因”或“结局描述”显示在最终的 Game Over 画面上。调用该函数后,它会返回一个特殊的动作,你可以直接把这个返回值赋给任意 next 属性。例如: