LSDE Dialog Engine — Full Guide [Chinese] (plain text, auto-generated) ============================================================ Concatenates all guide sections for LLM consumption. Source: lsde-ts/docs/zh/guide/*.md ============================================================ # 什么是 LSDEDE? **LSDE**(LS Dialog Editor)是一款面向游戏和软件开发者的免费工具,集成了可视化对话图编辑、AI翻译、语音生成、i18n代码集成和项目诊断等功能。更多信息请参阅 [lepasoft.com/zh/software/ls-dialog-editor](https://lepasoft.com/zh/software/ls-dialog-editor)。LSDE 将对话图导出为 blueprint(JSON、XML、YAML 或 CSV),其中包含 scene、block、connection、dictionary 和 action signature。 **LSDEDE**(LSDE Dialog Engine)是加载并执行这些 blueprint 的多运行时 engine。它提供多种语言版本,可原生集成到各种游戏引擎或框架中。 ## 可用运行时 | 运行时 | 语言 | 目标 | 源码 | |---------|------|------|------| | **TypeScript** | TypeScript / JavaScript | 参考实现 | [lsde-ts](https://github.com/jonlepage/LS-Dialog-Editor-Engine/tree/master/lsde-ts) | | **C#** | C# (.NET Standard 2.1) | Unity, Godot Mono, .NET | [lsde-csharp](https://github.com/jonlepage/LS-Dialog-Editor-Engine/tree/master/lsde-csharp) | | **C++** | C++17 | Unreal Engine, 自定义引擎 | [lsde-cpp](https://github.com/jonlepage/LS-Dialog-Editor-Engine/tree/master/lsde-cpp) | | **GDScript** | GDScript | Godot 4 | [lsde-gdscript](https://github.com/jonlepage/LS-Dialog-Editor-Engine/tree/master/lsde-gdscript) | 所有运行时共享相同的 blueprint 格式,并通过一套通用的跨语言测试套件(42 个测试用例)。 ## 架构 每个运行时都遵循相同的**回调驱动图调度器**模式: 1. **Blueprint** — 从 LSDE 导出的文件(JSON、XML 或 YAML),包含 scene、block 和 connection。 2. **Engine** — 验证 blueprint,构建内部图,并将 block 分发给已注册的 handler。 3. **Handler** — 由开发者编写的函数,用于响应每种 block 类型(dialog、choice、condition、action)。 4. **宿主应用程序** — condition、action 和角色解析由 handler callback 处理。 ``` Blueprint │ ▼ Engine ◄── next() ──┐ │ │ dispatch │ │ │ ▼ │ Handlers ────────────┘ ``` ## 设计原则 - **零依赖** — 任何语言版本都没有运行时依赖。 - **框架无关** — 可与任何游戏引擎或 UI 框架配合使用。 - **回调驱动** — 没有内部渲染循环。在准备好时调用 `next()` 即可。 - **双层 handler** — 全局(engine 级别)和 scene 级别 handler,支持 `preventGlobalHandler()`。 - **跨语言一致性** — 所有运行时对相同的 blueprint 产生相同的输出。 ================================================================================ # 快速入门 ## 安装 ## 基本用法 engine 是一个图遍历机器 — 它将 block 分发给已注册的 handler,由 handler 赋予其意义。没有 handler 的话,engine 不会产生任何输出。 > TIP: engine 接收 `BlueprintExport` 对象,而非文件。您可以使用平台适配的解析器从 JSON、XML 或 YAML 加载蓝图。请参阅[解析与导入](./parsing)。 ## Blueprint 验证 `engine.init()` 返回包含错误、警告和统计信息的[诊断报告](/api-ref/interfaces/DiagnosticReport)。`check` 选项可与宿主应用程序的功能进行交叉验证: ================================================================================ # Blueprint 与 Scene ## Blueprint 结构 `BlueprintExport` 是从 [LSDE](https://lepasoft.com/zh/software/ls-dialog-editor "Lepasoft Dialog Editor") 编辑器导出的 JSON 文件。它包含 engine 所需的全部数据。 ## Scene scene 是一个独立的对话序列 — 一段对话、一段过场动画、一个教程提示、一次商店交互。在游戏中,scene 通常由脚本事件触发:玩家与 NPC 对话、进入区域或拾取物品。 每个 scene 拥有自己的入口 block、独立的流程和独立的状态。多个 scene 可以并行运行(例如:主对话和教程覆盖层)。scene 由 [`BlueprintScene`](/api-ref/interfaces/BlueprintScene) 接口定义: ## Connection Connection 是 block 之间的连线 — 定义哪个 block 通向哪个 block。在编辑器中可视化绘制,导出后变为源 → 目标的扁平列表,由 [`BlueprintConnection`](/api-ref/interfaces/BlueprintConnection) 接口定义: 通常不需要直接检查 connection — engine 会在内部处理路由。如有需要,可以通过 [`onValidateNextBlock`](/api-ref/classes/DialogueEngine#onvalidatenextblock) 访问。 ## Dictionary Dictionary 描述游戏的寄存器 — 开关、变量、背包等。开发者在 [LSDE](https://lepasoft.com/zh/software/ls-dialog-editor "Lepasoft Dialog Editor") 编辑器中声明,向叙事设计师公开游戏中可用的变量。运行时,开发者将每个 dictionary 映射到游戏中对应的系统。[`condition`](/api-ref/interfaces/ExportCondition) 和 [`onResolveCondition`](/api-ref/classes/DialogueEngine#onresolvecondition) 使用这些键来评估游戏状态。由 [`Dictionary`](/api-ref/interfaces/Dictionary) 接口定义: ## Action Signature Signature 描述游戏中可用的动作类型 — `set_flag`、`play_sound`、`give_item`。开发者在 [LSDE](https://lepasoft.com/zh/software/ls-dialog-editor "Lepasoft Dialog Editor") 编辑器中声明,让叙事设计师使用类型化参数组合动作序列。运行时,开发者将 signature 的 `id` 映射到自己的系统。由 [`ActionSignature`](/api-ref/interfaces/ActionSignature) 接口定义: ================================================================================ # Block 类型 block 是对话场景的构建单元 — 编辑器图中的每个节点都是一个 block。engine 将流程从一个 block 路由到下一个,并为每种类型调用对应的 handler。 共有 5 种类型:**Dialog**、**Choice**、**Condition**、**Action** 和 **Note**。前四种是内容 block,各有专用的 handler(`onDialog`、`onChoice`、`onCondition`、`onAction`)— 四个都是**必需的**,在调用 `start()` 时验证。Note block 会被自动跳过。 handler 分为两个层级:**global handler**(注册在 engine 上)覆盖所有 scene,对大多数游戏来说足够。**scene handler**(注册在 [`SceneHandle`](/api-ref/interfaces/SceneHandle) 上)可以为特定 scene 补充或覆盖 global handler。详见 [Handlers](/zh/guide/handlers)。 ## DIALOG dialog block 代表一句台词 — 角色对话、旁白、屏幕文字。engine 通过 `onResolveCharacter` callback 解析说话的角色,并以 `context.character` 暴露给 handler。典型的 dialog handler 在游戏中创建一个文本实例(文本框、气泡、字幕…),等待玩家或动画完成,然后调用 `next()` 推进 engine。可选的 cleanup 函数可以在 engine 进入下一个 block 时清理副作用。 当叙事设计师为每个角色分配了专用输出([`portPerCharacter`](/api-ref/interfaces/NativeProperties#portpercharacter))时,handler 必须调用 `resolveCharacterPort()` 来告诉 engine 在 `next()` 时走哪条路径。 ## CHOICE choice block 是玩家做出选择的分支点 — 对话菜单、选项列表。`context.choices` 包含所有可用选项。当配置了 [`onResolveCondition()`](/zh/guide/choice-visibility) 时,每个选项被标记为 `visible: true | false` — handler 过滤并显示想要的选项。玩家交互后,`selectChoice(uuid)` 告诉 engine 走哪条路径,然后 `next()` 推进 flow。 参见 [Choice 可见性](/zh/guide/choice-visibility) 了解完整的可选标记系统。 ## CONDITION condition block 是一个不可见的开关 — 它评估游戏状态,在玩家看不到的情况下将 flow 送入两条路径之一。handler 评估 block 中的条件(变量、标志、背包…)然后调用 `context.resolve(result)` — `true` 走 port 0,`false` 走 port 1。以 `choice:` 开头的 key 的条件引用了玩家之前的选择 — `scene.evaluateCondition(cond)` 通过内部历史自动解析。 condition block 支持两种评估模式: - **switch 模式**(默认):按顺序评估条件组。第一个匹配的组路由到对应的 port(`true`/`case_N`)。如果没有匹配,则走 `false`/`default` port。 - **dispatcher 模式**([`enableDispatcher`](/api-ref/interfaces/NativeProperties#enabledispatcher) `= true`):**所有**匹配的组同时作为 async track 触发。`false`/`default` port 变为主继续 track("Continue"),**始终执行**。连接到条件 port 的 block 必须是 async 的。 ## ACTION action block 在游戏中触发副作用 — 给予物品、播放音效、设置标志。每个 action 引用一个 `actionId`,由开发者映射到自己的系统。handler 执行 action 列表后调用 `context.resolve()` 走 "then" port,或调用 `context.reject(error)` 走 "catch" port(如果没有 "catch" 连接则回退到 "then")。 ## NOTE note block 是叙事设计师的便签 — 注释、提醒、上下文。在遍历过程中自动跳过。虽然技术上可以通过 [`onBeforeBlock`](/zh/guide/lifecycle) 拦截 note block,但不推荐这样做 — action block 应该能覆盖所有副作用需求。 ## 通用属性 所有 block 共享以下基础字段([`BlueprintBlockBase`](/api-ref/interfaces/BlueprintBlockBase)): | 字段 | 类型 | 描述 | |------|------|------| | [`uuid`](/api-ref/interfaces/BlueprintBlockBase#uuid) | `string` | 唯一标识符 | | [`type`](/api-ref/interfaces/BlueprintBlockBase#type) | `BlockType` | 判别类型 | | [`label`](/api-ref/interfaces/BlueprintBlockBase#label) | `string?` | 人类可读的名称 | | [`parentLabels`](/api-ref/interfaces/BlueprintBlockBase#parentlabels) | `string[]?` | 编辑器中的父文件夹层级 | | [`properties`](/api-ref/interfaces/BlueprintBlockBase#properties) | `BlockProperty[]` | 键值属性 | | [`userProperties`](/api-ref/interfaces/BlueprintBlockBase#userproperties) | `Record?` | 自由格式的用户属性 | | [`nativeProperties`](/api-ref/interfaces/BlueprintBlockBase#nativeproperties) | `NativeProperties?` | 执行属性 | | [`metadata`](/api-ref/interfaces/BlueprintBlockBase#metadata) | `BlockMetadata?` | 显示元数据(角色、标签、颜色) | | [`isStartBlock`](/api-ref/interfaces/BlueprintBlockBase#isstartblock) | `boolean?` | 标记入口 block | ### NativeProperties | 字段 | 类型 | 描述 | |------|------|------| | [`isAsync`](/api-ref/interfaces/NativeProperties#isasync) | `boolean?` | 在并行异步轨道上执行 | | [`delay`](/api-ref/interfaces/NativeProperties#delay) | `number?` | 执行前的延迟(由 `onBeforeBlock` 消费) | | [`timeout`](/api-ref/interfaces/NativeProperties#timeout) | `number?` | 执行超时时间 | | [`portPerCharacter`](/api-ref/interfaces/NativeProperties#portpercharacter) | `boolean?` | metadata 中每个角色对应一个输出 port | | [`skipIfMissingActor`](/api-ref/interfaces/NativeProperties#skipifmissingactor) | `boolean?` | 如果引用的 actor 不存在则跳过 block | | [`debug`](/api-ref/interfaces/NativeProperties#debug) | `boolean?` | 编辑器调试标志 | | [`waitForBlocks`](/api-ref/interfaces/NativeProperties#waitforblocks) | `string[]?` | 此 block 可以继续之前必须已访问的 block UUID | | [`waitInput`](/api-ref/interfaces/NativeProperties#waitinput) | `boolean?` | 用于显式玩家输入控制的被动标志 | | [`enableDispatcher`](/api-ref/interfaces/NativeProperties#enabledispatcher) | `boolean?` | dispatcher 模式:所有匹配的条件作为 async track 触发,false/default port 变为继续 track | ================================================================================ # Choice 可见性 ## 概述 当 CHOICE block 被分发时,`context.choices` 始终包含 blueprint 中定义的**所有** choice — 不会有任何被预先过滤。engine 永远不会从数组中移除 choice。 如果需要可见性过滤(例如,根据游戏状态或之前的选择来隐藏 choice),engine 提供了一个**可选的标记**系统。安装一次 condition 解析器后,engine 会在 `onChoice` handler 接收数据之前,为每个 choice 标记 `visible: true | false`。 ## 设置 在 engine 上注册一个 condition 解析器 — 在启动任何 scene 之前注册一次: 安装后,engine 在调用 `onChoice` **之前**评估每个 choice 的 `visibilityConditions`。同一解析器也会预评估 condition block 的组 — 详见 [Condition blocks](/zh/guide/block-types#condition)。 - **`choice:` condition**(引用之前的玩家选择)由 engine 通过其内部选择历史自动解析 — callback 永远不会接收到它们。 - **游戏状态 condition**(其他所有情况)委托给已注册的 callback。 - 使用 `&`(AND)和 `|`(OR)的链式组合在两种类型之间都能正确工作。 ## 在 onChoice 中过滤 在 handler 中,用一行代码进行过滤: ### 为什么用 `visible !== false` 而不是 `=== true`? 当**未安装解析器**时,`visible` 是 `undefined`。由于 `undefined !== false` 求值为 `true`,所有 choice 都会通过 — 默认向后兼容。当解析器**已安装**时,choice 会被显式标记为 `true` 或 `false`。 | `visible` 值 | 含义 | `!== false` | |---|---|---| | `true` | 已安装解析器,choice 通过 | `true` | | `false` | 已安装解析器,choice 隐藏 | `false` | | `undefined` | 未安装解析器 | `true` | ## RuntimeChoiceItem 安装解析器后,`context.choices` 中的每个 choice 都是 `RuntimeChoiceItem` — 它是 `ChoiceItem` 的扩展,增加了 `visible` 标记: ```ts [TypeScript] interface RuntimeChoiceItem extends ChoiceItem { visible?: boolean; // true | false | undefined } ``` ```csharp [C#] public class RuntimeChoiceItem : ChoiceItem { public bool? Visible { get; set; } // true | false | null } ``` ```cpp [C++] struct RuntimeChoiceItem : ChoiceItem { std::optional visible; // true | false | nullopt }; ``` ```gdscript [GDScript] # RuntimeChoiceItem is a Dictionary with an extra "visible" key: # { "uuid": "...", "dialogueText": {...}, "visible": true/false/absent } ``` 未安装解析器时,choice 仍然是 `RuntimeChoiceItem`,但 `visible` 保持为 `undefined`/`null`/`nullopt`/absent。 ## 示例 ### 标准用法 — 显示可见的 choice ```ts [TypeScript] engine.onChoice(({ context, next }) => { const visible = context.choices.filter(c => c.visible !== false); ui.showChoices(visible, (uuid) => { context.selectChoice(uuid); next(); }); }); ``` ```csharp [C#] engine.OnChoice(args => { var visible = args.Context.Choices .Where(c => c.Visible != false).ToList(); ShowChoicesUI(visible, uuid => { args.Context.SelectChoice(uuid); args.Next(); }); return null; }); ``` ```cpp [C++] engine.onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn { std::vector visible; for (const auto& c : ctx->choices()) if (!c.visible.has_value() || c.visible.value()) visible.push_back(&c); showChoicesUI(visible, [ctx, next](const auto& uuid) { ctx->selectChoice(uuid); next(); }); return {}; }); ``` ```gdscript [GDScript] engine.on_choice(func(args): var visible = [] for c in args["context"].choices: if c.get("visible") != false: visible.append(c) show_choices_ui(visible, func(uuid): args["context"].select_choice(uuid) args["next"].call() ) return Callable() ) ``` ### 限时选择 — 超时自动选择 ```ts [TypeScript] engine.onChoice(({ block, context, next }) => { const visible = context.choices.filter(c => c.visible !== false); const timeout = block.nativeProperties?.timeout; const resolve = (choice) => { context.selectChoice(choice.uuid); next(); }; if (timeout) { const timer = setTimeout(() => resolve(visible[0]), timeout * 1000); ui.showChoices(visible, (uuid) => { clearTimeout(timer); resolve(visible.find(c => c.uuid === uuid)); }); } else { ui.showChoices(visible, (uuid) => resolve(visible.find(c => c.uuid === uuid))); } }); ``` ```csharp [C#] engine.OnChoice(args => { var (_, block, context, next) = args; var visible = context.Choices .Where(c => c.Visible != false).ToList(); var timeout = block.NativeProperties?.Timeout; void Resolve(RuntimeChoiceItem choice) { context.SelectChoice(choice.Uuid); next(); } if (timeout.HasValue) { // use your engine's timer — cancel on player selection var timer = ScheduleTimer((float)timeout.Value, () => Resolve(visible[0])); ShowChoicesUI(visible, uuid => { timer.Cancel(); Resolve(visible.First(c => c.Uuid == uuid)); }); } else { ShowChoicesUI(visible, uuid => Resolve(visible.First(c => c.Uuid == uuid))); } return null; }); ``` ```cpp [C++] engine.onChoice([](auto*, auto* block, auto* ctx, auto next) -> CleanupFn { std::vector visible; for (const auto& c : ctx->choices()) if (!c.visible.has_value() || c.visible.value()) visible.push_back(&c); auto timeout = block->nativeProperties ? block->nativeProperties->timeout : std::nullopt; auto resolve = [ctx, next](const std::string& uuid) { ctx->selectChoice(uuid); next(); }; if (timeout.has_value()) { // use your engine's timer — cancel on player selection auto timer = scheduleDelay(timeout.value(), [&]() { resolve(visible[0]->uuid); }); showChoicesUI(visible, [resolve, timer](const auto& uuid) { timer->cancel(); resolve(uuid); }); } else { showChoicesUI(visible, resolve); } return {}; }); ``` ```gdscript [GDScript] engine.on_choice(func(args): var ctx = args["context"] var next_fn = args["next"] var block = args["block"] var visible = [] for c in ctx.choices: if c.get("visible") != false: visible.append(c) var timeout_val = block.get("nativeProperties", {}).get("timeout", 0) if timeout_val > 0: # use your engine's timer — cancel on player selection var timer = get_tree().create_timer(timeout_val) timer.timeout.connect(func(): ctx.select_choice(visible[0]["uuid"]) next_fn.call() ) show_choices_ui(visible, func(uuid): timer.time_left = 0 # cancel ctx.select_choice(uuid) next_fn.call() ) else: show_choices_ui(visible, func(uuid): ctx.select_choice(uuid) next_fn.call() ) return Callable() ) ``` ### 隐藏的 choice 显示为灰色 ```ts [TypeScript] engine.onChoice(({ context, next }) => { for (const choice of context.choices) { if (choice.visible === false) { ui.addGreyed(choice); // show but disabled } else { ui.addNormal(choice); // selectable } } // wait for player selection... }); ``` ```csharp [C#] engine.OnChoice(args => { foreach (var choice in args.Context.Choices) { if (choice.Visible == false) AddGreyed(choice); // show but disabled else AddNormal(choice); // selectable } // wait for player selection... return null; }); ``` ```cpp [C++] engine.onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn { for (const auto& choice : ctx->choices()) { if (choice.visible.has_value() && !choice.visible.value()) addGreyed(choice); // show but disabled else addNormal(choice); // selectable } // wait for player selection... return {}; }); ``` ```gdscript [GDScript] engine.on_choice(func(args): for choice in args["context"].choices: if choice.get("visible") == false: add_greyed(choice) # show but disabled else: add_normal(choice) # selectable # wait for player selection... return Callable() ) ``` ### 教程模式 — 完全忽略可见性 ```ts [TypeScript] tutorial.onChoice(({ context, next }) => { // force-select the first choice, no filtering context.selectChoice(context.choices[0].uuid); next(); }); ``` ```csharp [C#] tutorial.OnChoice(args => { // force-select the first choice, no filtering args.Context.SelectChoice(args.Context.Choices[0].Uuid); args.Next(); return null; }); ``` ```cpp [C++] tutorial->onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn { // force-select the first choice, no filtering ctx->selectChoice(ctx->choices()[0].uuid); next(); return {}; }); ``` ```gdscript [GDScript] tutorial.on_choice(func(args): # force-select the first choice, no filtering args["context"].select_choice(args["context"].choices[0]["uuid"]) args["next"].call() return Callable() ) ``` ## 共享求值器 使用 `onResolveCondition`,一个 callback 即可处理 choice 可见性和 condition block 预评估**两者**。无需再重复逻辑: > TIP: 在 `onResolveCondition` 之前,相同的 `gameState.check(...)` 逻辑需要分别在 `setChoiceFilter` 和 `onCondition` 中注册。使用统一解析器后,只需一个 callback — engine 自动处理两者。 ## 高级用法:手动过滤 如果不需要安装全局解析器,`LsdeUtils` 提供了一个底层工具函数: ```ts [TypeScript] import { LsdeUtils } from '@lsde/dialog-engine'; const visible = LsdeUtils.filterVisibleChoices( block.choices ?? [], (cond) => gameState.check(cond.key, cond.operator, cond.value), scene, // optional — enables choice: condition resolution via history ); ``` ```csharp [C#] var visible = LsdeUtils.FilterVisibleChoices( block.Choices ?? new(), cond => GameState.Check(cond.Key, cond.Operator, cond.Value), scene // optional — enables choice: condition resolution via history ); ``` ```cpp [C++] auto visible = lsde::LsdeUtils::FilterVisibleChoices( block->choices, [](const auto& cond) { return gameState.check(cond.key, cond.op, cond.value); }, scene // optional — enables choice: condition resolution via history ); ``` ```gdscript [GDScript] var visible = LsdeUtils.filter_visible_choices( block.get("choices", []), func(cond): return game_state.check(cond), scene # optional — enables choice: condition resolution via history ) ``` `scene` 参数启用自动的 `choice:` condition 解析。如果不提供,所有 condition 都将委托给解析器 callback。 ================================================================================ # 处理器 ## Handler handler 是 engine 与游戏之间的桥梁。它们像观察者一样工作 — 你注册一个函数,当对应事件发生时 engine 会调用它。显示文本、播放动画、评估状态等 — 都是通过 handler 在游戏引擎中触发相应行为。 engine 公开以下 handler: | Handler | 级别 | 描述 | |---------|------|------| | [`onDialog`](/api-ref/classes/DialogueEngine#ondialog) | global / scene | dialog block — 显示文本 | | [`onChoice`](/api-ref/classes/DialogueEngine#onchoice) | global / scene | choice block — 呈现选项 | | [`onCondition`](/api-ref/classes/DialogueEngine#oncondition) | global / scene | condition block — 评估和分支 | | [`onAction`](/api-ref/classes/DialogueEngine#onaction) | global / scene | action block — 触发副作用 | | [`onResolveCharacter`](/api-ref/classes/DialogueEngine#onresolvecharacter) | global / scene | 解析哪个角色在说话 | | [`onBeforeBlock`](/api-ref/classes/DialogueEngine#onbeforeblock) | global | 每个 block 之前(delay、入场动画…) | | [`onValidateNextBlock`](/api-ref/classes/DialogueEngine#onvalidatenextblock) | global | 进入 block 前的验证 | | [`onInvalidateBlock`](/api-ref/classes/DialogueEngine#oninvalidateblock) | global | 验证失败时的处理 | | [`onSceneEnter`](/api-ref/classes/DialogueEngine#onsceneenter) | global / scene | scene 开始 | | [`onSceneExit`](/api-ref/classes/DialogueEngine#onsceneexit) | global / scene | scene 结束 | | [`onBlock`](/api-ref/interfaces/SceneHandle#onblock) | scene | 按 UUID 覆盖特定 block | | [`onDialogId`](/api-ref/interfaces/SceneHandle#ondialogid) | scene | 按 UUID 覆盖特定 DIALOG block(类型安全) | | [`onChoiceId`](/api-ref/interfaces/SceneHandle#onchoiceid) | scene | 按 UUID 覆盖特定 CHOICE block(类型安全) | | [`onConditionId`](/api-ref/interfaces/SceneHandle#onconditionid) | scene | 按 UUID 覆盖特定 CONDITION block(类型安全) | | [`onActionId`](/api-ref/interfaces/SceneHandle#onactionid) | scene | 按 UUID 覆盖特定 ACTION block(类型安全) | | [`onResolveCondition`](/api-ref/classes/DialogueEngine#onresolvecondition) | global | 统一 condition 解析器(choice 可见性 + condition 预评估) | | ~~[`setChoiceFilter`](/api-ref/classes/DialogueEngine#setchoicefilter)~~ | global | _已弃用 — 请使用 `onResolveCondition` 代替_ | `onDialog`、`onChoice` 和 `onAction` 是**必需的** — `start()` 调用时 engine 验证它们是否存在,缺失时抛出描述性错误。当安装了 `onResolveCondition` 时,`onCondition` 是**可选的** — engine 从预评估的 condition 组中自动路由。 ## Two-Tier Handler System engine 在两个层级上解析 handler: - **Global handler** — 注册在 engine 上,定义每个 scene 的默认行为。大多数情况下这就够了。 - **Scene handler** — 注册在特定的 [`SceneHandle`](/api-ref/interfaces/SceneHandle) 上,当 scene 需要不同的渲染或控制流程时,可以覆盖或扩展默认行为。这种情况很少见,但可用。 当一个 block 被分发时,engine 按以下顺序解析 handler: 1. `handle.onBlock(uuid)` 或 `handle.onDialogId(uuid)` / `handle.onActionId(uuid)` / ... — block 级别的覆盖 2. `handle.onDialog()` / `handle.onChoice()` / ... — scene 级别的类型 handler 3. `engine.onDialog()` / `engine.onChoice()` / ... — global handler 当两个层级都存在时,两者按顺序执行 — 先 scene,再 global — 除非 scene handler 调用了 `context.preventGlobalHandler()` 来抑制 global 的执行。 ## Character Resolution 角色解析是可选的。通过注册 `onResolveCharacter` callback,engine 会在每个 `metadata.characters` 中包含角色的 block 之前调用它。callback 接收分配给 block 的角色列表,返回应该激活的角色 — 如果没有可用角色则返回 `undefined`。解析后的角色可通过所有 handler 中的 `context.character` 访问。 这是查询游戏状态的理想集成点:检查角色是否在场景中、是否存活、是否在镜头范围内等。返回 `undefined` 可以触发多种策略:通过 [`skipIfMissingActor`](/api-ref/interfaces/NativeProperties#skipifmissingactor) 跳过 block、通过 `handle.cancel()` 取消 scene、或直接在 handler 中处理。 ## Scene Lifecycle `onSceneEnter` 和 `onSceneExit` callback 可以在 scene 开始和结束时做出反应 — 启用电影模式、冻结 NPC、准备 UI、清理资源等。它们在 global 级别(engine 上)和 scene 级别(通过 `handle.onEnter()` / `handle.onExit()`)都可用。如果定义了 scene handler,它会替代 global handler。 ## Block Override `onBlock(uuid)` 可以通过标识符定位特定 block 并为其分配专用 handler。这是一个罕见的用例 — 通用 handler 覆盖了绝大多数需求 — 但对于个别 block 需要不同行为的非常特殊的场景,它是可用的。 ## Type-Safe Block Override `onDialogId(uuid)`、`onChoiceId(uuid)`、`onConditionId(uuid)` 和 `onActionId(uuid)` 是 `onBlock(uuid)` 的类型安全替代方法。工作方式完全相同 — 相同的优先级、相同的 `preventGlobalHandler` 支持 — 但 handler 接收特殊化的 block 类型和 context,而不是通用联合类型。 当你在注册时已知 block 类型,并需要 `block` 和 `context` 的完整自动补全时使用这些方法。 ## Visual Reference ### Two-Tier Handler Dispatch ```mermaid flowchart TD A[block dispatched] --> B{resolve scene handler} B --> B1{"onBlock(uuid) /\nonDialogId(uuid) etc.?"} B1 -- found --> S B1 -- not found --> B2{"handle.onDialog() etc.?"} B2 -- found --> S B2 -- not found --> G S[execute scene handler] --> D{preventGlobalHandler?} D -- yes --> Z[done] D -- no --> G["execute global handler\nengine.onDialog() etc."] G --> Z ``` ================================================================================ # 游戏引擎集成 LSDE 与引擎无关 — 不依赖任何游戏引擎、UI 框架或音频系统。它遍历图并调用注册的 handler。本页展示如何将其接入主流游戏引擎。 handler 的详细实现请参阅 [Block Types](./block-types) 和 [Handlers](./handlers)。 ## 完整集成示例 以下示例展示了在每个引擎中集成 LSDE 的一种方式。它将 4 个必需的 handler — dialog、choice、condition、action — 包含在一个类中,作为起点。 每个游戏都有自己的需求。请根据项目调整结构、布局和 UI。 ## 4 个 Handler 每个 handler 接收 block 数据和 `next()` 回调。开发者在引擎中处理数据,block 处理完成后调用 `next()`。调用时机完全由游戏决定。 - **Dialog** — 文本、角色、原生属性。在 UI 中显示对话,等待玩家输入或延迟,然后调用 `next()`。返回清理函数,在 engine 移到下一个 block 时隐藏 UI。 - **Choice** — 配置 `choiceFilter` 后带有 `visible` 标签的选项列表。创建对应的 UI 元素 — 按钮、列表、径向菜单。玩家选择后,`selectChoice(uuid)` 告诉 engine 走哪条分支,然后 `next()` 推进流程。 - **Condition** — block 中定义的条件。用游戏逻辑评估 — 检查标记、任务、背包。`context.resolve(true)` 将流程发送到端口 0,`context.resolve(false)` 发送到端口 1。 - **Action** — block 中定义的动作。在引擎中执行 — 播放音效、给予物品、触发过场动画。`context.resolve()` 确认成功,`context.reject(err)` 通知失败。 ## 实用技巧 - **`next()` 是遥控器。** 立即调用实现快速对话,或保留它直到动画结束。engine 会等待 — 它没有时间概念。 - **清理函数负责善后。** 从任何 handler 返回一个函数,engine 在移到下一个 block 时会调用它。非常适合隐藏 UI、停止音频或释放节点。 - **`onBeforeBlock` 处理 delay。** engine 不强制执行 `nativeProperties.delay` — 由 `onBeforeBlock` 读取它并在定时器后调用 `resolve()`。完全控制。 - **async track 是并行流。** 当过场动画需要同时进行对话和摄像机移动时,在编辑器中标记为 `isAsync` 的 block 会在独立 track 上运行。 ================================================================================