异步轨道
当 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 轨道。返回每个轨道状态的只读快照:
const tracks = scene.getTrackInfos();
for (const track of tracks) {
console.log(`Track ${track.id} (parent: ${track.parentTrackId}) at block ${track.currentBlockId}`);
}var tracks = scene.GetTrackInfos();
foreach (var track in tracks)
{
Console.WriteLine($"Track {track.Id} (parent: {track.ParentTrackId}) at block {track.CurrentBlockId}");
}auto tracks = scene->getTrackInfos();
for (const auto& track : tracks) {
std::cout << "Track " << track.id
<< " (parent: " << track.parentTrackId << ")"
<< " at block " << track.currentBlockId << "\n";
}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 在调用:
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();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();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();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
