Skip to content

异步轨道 ​

当 block 设置了 props.isAsync = true 时,engine 会创建一个独立于主流程运行的并行轨道。

轨道的创建方式 ​

在端口解析过程中,如果存在多个输出连接:

  • 第一个非 async 连接成为当前流程的延续
  • 其他连接(指向具有 isAsync 的 block)成为新的并行轨道

这适用于主轨道和 async 轨道 — async 轨道可以从自己的 async 连接中创建子轨道,形成并行执行的层次结构。

轨道生命周期 ​

  • onBeforeBlock 会为所有 block 调用(主轨道和 async 轨道) — resolve() 的详细信息请参阅 Lifecycle
  • async 轨道像主轨道一样将输出连接分为 main 和 async
  • 轨道在 scene 结束或调用 cancel() 时自动取消
  • 当轨道自然结束时(没有更多连接),其子轨道继续独立存在
  • 当轨道被显式取消时(cancel()),取消会级联到所有子轨道

waitForBlocks — 等待另一条轨道 ​

props.waitForBlocks 会保留一个 block,直到它所指定的 block 都已完成。它是 isAsync 所开分支的「汇合」一半:一条分支并行运行,下游的 block 等它结束之后才播放。

是完成,不是到达。 被列出的 block 在流程离开它的那一刻才算数:你的游戏调用了 next(),出口端口已解析,该 block 的 cleanup 也已执行。也就是说,在汇合的那句台词被派发之前,气泡已经离开画面、语音也已停止 —— 这正是画一条汇合线的意义所在。

规则已更改

在 2.x 的最初几个版本里,只要到达就够了。这让该属性在设计师真正会画的形状中几乎失效:分支到两个 block,然后在两者上汇合,等待会在它被登记的同一个 tick 就解除 —— 那两个 block 在几分之一毫秒前才刚被派发 —— 于是汇合的台词盖在它们上面说话。

getVisitedBlocks() 不受影响:它仍然列出玩家看到过的内容,其中包括一个话还没说完的 block。

block 在被派发之前就被保留。 handler 不会被调用,因此在等待解除之前,你的游戏根本不知道这个 block 存在 —— 它的任何内容都不会过早出现在画面上。这是引擎的决定,而不是你可以另作选择的渲染行为:waitForBlocks 是 native 属性,设计师在 LSDE 中勾选了它,引擎就应当兑现。

这条规则在每一条轨道上都相同,包括玩家正在观看的那一条。

  • 这些 id 指向本场景的 block。连线从未跨越过场景。
  • 必须全部完成,而不是其中之一。
  • 一个 block 完成后会连锁释放所有等待它的对象。
  • 永远不会完成的 block 会让轨道永久停留 —— 而一个永远等待玩家输入的 block 永远不会完成。当某个 id 根本不是场景中的 block 时,init() 会报告 UNKNOWN_WAIT_BLOCK,但它无法知道一个真实存在的 block 是否会被播放。

同时带有 waitForBlocks 和 delay 的 block 的顺序:

waitForBlocks 门控 → onBeforeBlock (delay) → handler → next()

waitInput — 玩家输入标志 ​

props.waitInput 是一个被动标志 — engine 公开它但不解释它。您的游戏 handler 读取它来决定是否等待明确的玩家输入。

TrackInfo API — 可观测性 ​

使用 scene.getTrackInfos() 来检查运行中的 async 轨道。返回每个轨道状态的只读快照:

ts
const tracks = scene.getTrackInfos();
for (const track of tracks) {
  console.log(`Track ${track.id} (parent: ${track.parentTrackId}) at block ${track.currentBlockId}`);
}
csharp
var tracks = scene.GetTrackInfos();
foreach (var track in tracks)
{
    Console.WriteLine($"Track {track.Id} (parent: {track.ParentTrackId}) at block {track.CurrentBlockId}");
}
cpp
auto tracks = scene->getTrackInfos();
for (const auto& track : tracks) {
    std::cout << "Track " << track.id
              << " (parent: " << track.parentTrackId << ")"
              << " at block " << track.currentBlockId << "\n";
}
gdscript
var tracks = scene.get_track_infos()
for track in tracks:
    print("Track %d (parent: %s) at block %s" % [
        track["id"], str(track["parentTrackId"]), track["currentBlockId"]])

每个 TrackInfo 包含:id、parentTrackId、startBlockId、currentBlockId、running。用于调试覆盖层、播放模式渲染器或验证。

async 轨道中的适用与不适用 ​

async 轨道非常适合与主对话并行发生的事物 — 环境效果、并行动画、同伴反应。但有一些限制。

推荐 — 并行内容:

用例适用原因
NPC 环境对话("barks")async 轨道上的 dialog block — NPC 在主对话进行时评论、反应或闲聊
与事件同步的角色反应使用 waitForBlocks 在到达特定 block 时触发反应
播放环境音效或音乐action block,无需玩家交互
触发摄像机移动action block,并行运行
精确计时的效果结合 waitForBlocks + delay 实现精确计时

不推荐 — 玩家交互或游戏逻辑分支:

用例问题原因
async 轨道中的 CHOICE block玩家已经在与主轨道交互 — 谁来响应 async 的 choice?
关键游戏状态变更如果 async 轨道被取消(scene 结束),action 永远不会执行

async 轨道中的 choice

async 轨道中的 CHOICE block 意味着玩家应该在已经参与主对话的同时进行选择。最常见的场景是 AI 驱动的"choice"(例如:同伴 NPC 基于个性自动选择)。如果 async 轨道在没有自动选择的 scene 级 handler 的情况下到达 CHOICE block,流程将停滞或静默结束。

多个 Scene 并行运行 ​

engine 支持同时运行多个 scene。每个 SceneHandle 拥有自己的状态、已访问的 block 和异步轨道。全局 handler(第 1 层)是共享的 — 使用 scene 参数来判断是哪个 scene 在调用:

ts
engine.onDialog(({ scene, block, context, next }) => {
  if (scene === mainDialogue) {
    showMainUI(block);
    // next() called later — by player input
  } else if (scene === tutorialOverlay) {
    showTutorialBubble(block);
    // auto-advance after display
    next();
  }
});

const mainDialogue = engine.scene('main-quest');
const tutorialOverlay = engine.scene('tutorial-hints');
mainDialogue.start();
tutorialOverlay.start();
csharp
engine.OnDialog(args => {
    if (args.Scene == mainDialogue)
    {
        ShowMainUI(args.Block);
        // next() called later — by player input
    }
    else if (args.Scene == tutorialOverlay)
    {
        ShowTutorialBubble(args.Block);
        // auto-advance after display
        args.Next();
    }
    return null;
});

var mainDialogue = engine.Scene("main-quest");
var tutorialOverlay = engine.Scene("tutorial-hints");
mainDialogue.Start();
tutorialOverlay.Start();
cpp
engine.onDialog([&](auto* scene, auto* block, auto*, auto next) -> CleanupFn {
    if (scene == mainDialogue.get()) {
        showMainUI(block);
        // next() called later — by player input
    } else if (scene == tutorialOverlay.get()) {
        showTutorialBubble(block);
        // auto-advance after display
        next();
    }
    return {};
});

auto mainDialogue = engine.scene("main-quest");
auto tutorialOverlay = engine.scene("tutorial-hints");
mainDialogue->start();
tutorialOverlay->start();
gdscript
engine.on_dialog(func(args):
    if args["scene"] == main_dialogue:
        show_main_ui(args["block"])
        # next() called later — by player input
    elif args["scene"] == tutorial_overlay:
        show_tutorial_bubble(args["block"])
        # auto-advance after display
        args["next"].call()
    return Callable()
)

var main_dialogue = engine.scene("main-quest")
var tutorial_overlay = engine.scene("tutorial-hints")
main_dialogue.start()
tutorial_overlay.start()

按 scene 路由

如果有多个并发 scene,建议在每个 handle 上注册 scene 级(第 2 层)handler,而不是在全局 handler 中进行路由。更清晰的分离,没有 if/else 链。

Visual Reference ​

  • Main track: A → B → C
  • Track 1 (parallel): D → E
  • Track 2 (sub-track of D): F
  • Scene cancel → all tracks cancelled
  • Track D ends naturally → F continues