Skip to content

生命周期与验证 ​

每个 Block 的执行顺序 ​

  1. 上一个 block 的清理 — 上一个 block 的 handler 返回的清理函数在转换时执行(next() 被调用时)
  2. onValidateNextBlock — 执行前的验证
  3. onBeforeBlock — 预处理(必须调用 resolve() 才能继续)
  4. 类型 handler(先第 2 层,再第 1 层)

Scene 事件 ​

ts
engine.onSceneEnter(({ scene, context }) => {
  // Called when handle.start() is executed
});

engine.onSceneExit(({ scene, context }) => {
  // Called when the scene ends. context.reason says why:
  // 'completed' | 'cancelled' | 'invalidated' | 'faulted' | 'deadlocked'
  // scene.getSceneId() says WHICH scene — a global handler needs it when several play.
  if (context.reason === 'faulted') {
    console.error(`Scene ${scene.getSceneId()} failed`, context.error);
  }
  if (context.reason === 'deadlocked') {
    console.warn('Scene stuck waiting for', context.waitingFor);
  }
});
csharp
engine.OnSceneEnter(args => {
    // Called when handle.Start() is executed
});

engine.OnSceneExit(args => {
    // Called when the scene ends. args.Context.Reason says why (SceneEndReason.*)
    // args.Scene.GetSceneId() says WHICH scene — a global handler needs it when several play.
    if (args.Context.Reason == SceneEndReason.Faulted)
        Debug.LogException(args.Context.Error);
    if (args.Context.Reason == SceneEndReason.Deadlocked)
        Debug.LogWarning($"{args.Scene.GetSceneId()} stuck waiting for {string.Join(", ", args.Context.WaitingFor)}");
});
cpp
engine.onSceneEnter([](const lsde::SceneLifecycleArgs& args) {
    // Called when handle->start() is executed
});

engine.onSceneExit([](const lsde::SceneLifecycleArgs& args) {
    // Called when the scene ends. args.context.reason says why (lsde::SceneEndReason::*)
    // args.scene->getSceneId() says WHICH scene — a global handler needs it when several play.
    if (args.context.reason == lsde::SceneEndReason::Faulted) {
        // args.context.error holds the exception: std::rethrow_exception it to read it
    }
    if (args.context.reason == lsde::SceneEndReason::Deadlocked) {
        // args.context.waitingFor names the blocks the scene was stuck on
    }
});
gdscript
engine.on_scene_enter(func(args):
    pass # Called when handle.start() is executed
)

engine.on_scene_exit(func(args):
    # Called when the scene ends. args["context"]["reason"] says why (LsdeTypes.SCENE_END_*)
    # args["scene"].get_scene_id() says WHICH scene — a global handler needs it when several play.
    if args["context"]["reason"] == LsdeTypes.SCENE_END_DEADLOCKED:
        push_warning("%s stuck waiting for %s" % [args["scene"].get_scene_id(), str(args["context"]["waitingFor"])])
)

onValidateNextBlock ​

拦截每次 block 转换进行验证。handler 接收下一个 block(nextContext)和上一个 block(fromContext)的已解析角色:

ts
engine.onValidateNextBlock(({ nextBlock, fromBlock, nextContext, fromContext }) => {
  return { valid: true };
});

engine.onInvalidateBlock(({ scene, reason }) => {
  console.error('Invalid block:', reason);
  scene.cancel();
});
csharp
engine.OnValidateNextBlock(args => {
    // args.NextContext.Character, args.FromContext?.Character
    return new ValidationResult { Valid = true };
});

engine.OnInvalidateBlock(args => {
    Console.Error.WriteLine($"Invalid block: {args.Reason}");
    args.Scene.Cancel();
});
cpp
engine.onValidateNextBlock([](const auto& args) {
    // args.nextContext.character, args.fromContext.character (check args.hasFromContext)
    return ValidationResult{true};
});

engine.onInvalidateBlock([](auto* scene, const auto& reason) {
    std::cerr << "Invalid block: " << reason << "\n";
    scene->cancel();
});
gdscript
engine.on_validate_next_block(func(args):
    # args["nextContext"]["character"], args["fromContext"]["character"]
    return {"valid": true}
)

engine.on_invalidate_block(func(args):
    printerr("Invalid block: %s" % args["reason"])
    args["scene"].cancel()
)

Character Gating ​

使用 nextContext.character 根据游戏状态控制哪些 block 可以执行:

ts
// Block if the character is stunned
engine.onValidateNextBlock(({ nextContext }) => {
  const { character } = nextContext;
  if (!character) return { valid: false, reason: 'no_character' };
  if (game.characterHasStatus(character, 'stunned'))
    return { valid: false, reason: 'character_stunned' };
  return { valid: true };
});
csharp
engine.OnValidateNextBlock(args => {
    var character = args.NextContext.Character;
    if (character == null)
        return ValidationResult.Fail("no_character");
    if (game.CharacterHasStatus(character, "stunned"))
        return ValidationResult.Fail("character_stunned");
    return ValidationResult.Ok();
});
cpp
engine.onValidateNextBlock([&game](const auto& args) {
    auto* character = args.nextContext.character;
    if (!character) return ValidationResult{false, "no_character"};
    if (game.characterHasStatus(character, "stunned"))
        return ValidationResult{false, "character_stunned"};
    return ValidationResult{true};
});
gdscript
engine.on_validate_next_block(func(args):
    var character = args["nextContext"]["character"]
    if character == null:
        return {"valid": false, "reason": "no_character"}
    if game.character_has_status(character, "stunned"):
        return {"valid": false, "reason": "character_stunned"}
    return {"valid": true}
)

使用 fromContext.character 验证角色之间的转换(例如:关系检查、冷却时间)。fromContext 在场景的第一个 block 中为 null。

onBeforeBlock ​

在每个 block 之前调用。必须调用 resolve() 才能继续:

ts
engine.onBeforeBlock(({ block, resolve }) => {
  const delay = LsdeUtils.getNativeProperties(block)?.delay;
  if (delay) {
    setTimeout(resolve, delay * 1000);
  } else {
    resolve();
  }
});
csharp
engine.OnBeforeBlock(args => {
    var delay = args.Block.NativeProperties?.Delay;
    if (delay.HasValue)
    {
        // use your engine's delay system (coroutine, DOTween, Invoke, etc.)
        DelayThenCall((float)delay.Value, args.Resolve);
    }
    else
    {
        args.Resolve();
    }
});
cpp
engine.onBeforeBlock([](const auto& args) {
    auto delay = args.block->props
        ? args.block->props->delay : std::nullopt;
    if (delay.has_value()) {
        // use your engine's timer system (FTimerManager, SDL_AddTimer, etc.)
        scheduleDelay(delay.value(), [&args]() { args.resolve(); });
    } else {
        args.resolve();
    }
});
gdscript
engine.on_before_block(func(args):
    var delay = args["block"].get("props", {}).get("delay", 0)
    if delay > 0:
        await get_tree().create_timer(delay).timeout
    args["resolve"].call()
)

在 onBeforeBlock 仍在执行时调用的 resolve(),会在它返回时才生效 — 与在 handler 内调用 next() 完全一样。类型 handler 在你的回调结束之后才被分发,因此写在 resolve() 之后的代码会在 block 之前运行。

清理函数 ​

handler 可以返回一个清理函数,在离开 block 时调用:

ts
engine.onDialog(({ block, next }) => {
  const element = showDialogUI(block);

  // next() is called later — by player input, timer, etc.

  return () => {
    element.remove(); // called when the engine moves to the next block
  };
});
csharp
engine.OnDialog(args => {
    var element = ShowDialogUI(args.Block);

    // next() is called later — by player input, timer, etc.

    return () => element.SetActive(false);
});
cpp
engine.onDialog([](auto*, auto* block, auto* ctx, auto next) -> CleanupFn {
    auto* element = showDialogUI(block);

    // next() is called later — by player input, timer, etc.

    return [element]() { element->remove(); };
});
gdscript
engine.on_dialog(func(args):
    var element = show_dialog_ui(args["block"])

    # next is called later — by player input, timer, etc.

    return func(): element.queue_free()
)

错误边界 ​

没有任何异常被吞掉。 当 engine 遍历图时,如果你的任何代码抛出异常 — 类型 handler、清理函数、 onValidateNextBlock、onInvalidateBlock、onBeforeBlock、onResolveCondition、onResolveCharacter 或 onSceneEnter — engine 会先关闭整个 scene,然后把错误重新抛给调用 start()、next() 或 resolve() 的一方。

这在每一条轨道上都成立。在 isAsync 分支上抛出异常的 handler 会关闭 scene,而不仅仅是它的分支。

正是这个顺序让它可用。当错误到达你的代码时:

  • 清理函数已经执行
  • async 轨道已经取消
  • onSceneExit 已经以 reason: 'faulted' 触发,错误本身在 context.error 中

对话干净地停止了,接下来做什么由你决定 — 不带它继续、显示一个画面,或者让它崩溃。请在 start()、next() 或 resolve() 外面放上你自己的 try/catch。

错误有意地出现在两个地方。它被抛给调用 next() 的一方 —— 在游戏中通常是一次点击或一个计时器 —— 同时通过 context.error 交给 onSceneExit,而等待对话结束的代码正是在那里监听。只在其中一处记录日志。

由 onSceneExit 本身抛出的异常也以同样的方式到达你这里,而 scene 依然会被释放:engine.isRunning() 不再计入它。除非 scene 本来就正因一个错误而关闭:那时你收到的是那个错误,正是它解释了其余的一切。

GDScript

GDScript 没有异常。handler 中的脚本错误会输出到 Godot 日志,调用返回 null:block 只会一直等待一个 永远不会到来的 next()。没有任何东西会替你关闭 scene。

为什么改变

v1 会静默吞掉 handler 的异常 — 连日志都没有 — 而同一个 handler 返回的清理函数抛出的异常却会到达 调用方。同一种故障,两种相反的行为,而安静的那一种在项目运行的整个期间掩盖了真正的 bug。

2.0.0 只为类型 handler 修复了这一点。抛出异常的验证、onBeforeBlock 或解析器 — 或者并行轨道上的 handler — 仍会让 scene 保持打开,却没有任何东西能让它前进,等待对话结束的游戏会永远等下去。

Scene 结束的原因 ​

onSceneExit 通过 context.reason 得知原因:

reason何时
completed流程走到了图的尽头
cancelledscene.cancel() 或 engine.stop()
invalidatedonValidateNextBlock 拒绝了最后一条运行中的轨道正要进入的 block
faulted遍历过程中你的代码抛出了异常。context.error 保存了被抛出的内容 — 参见错误边界
deadlocked剩下的所有轨道都停在一个没有任何东西能完成的 waitForBlocks 上。context.waitingFor 列出这些 block

onSceneEnter 不会收到原因。

scene.getSceneId() 说明哪一个 scene 结束了,scene.getScenePath() 给出它的路径。只要有两个 scene 同时播放,全局的 onSceneExit 就需要它。请保存 id 而不是路径:有人重命名 scene 的那一天,路径就会改变。

死锁会关闭 scene。 一个等待永远不会有轨道完成的 block 的 block — 例如流程没有走的分支上的 block — 过去会让 scene 永久保持打开,也没有 onSceneExit。现在,只要最后一条能前进的轨道停下,scene 就会以 deadlocked 关闭,waitingFor 会告诉你哪一个汇合接错了。

单一线程 ​

engine 不是线程安全的。请从启动 scene 的线程调用 next()、resolve() 和 cancel() — 在 Unity 中是 主线程,在 Unreal 中是游戏线程。

在 C# 和 C++ 中,来自其他线程的调用会被一个指明两个线程的异常拒绝,并且不会改变任何东西:从正确的 线程调用时,同一个 next() 依然有效。在 Unity 中,调用 engine 之前请先切回主线程 (await UniTask.SwitchToMainThread(),或把调用排入主线程队列)。

cancel() ​

调用 scene.cancel() 会触发以下序列:

  1. 所有异步轨道被取消
  2. 当前 block 的清理函数被执行
  3. onSceneExit handler 以 reason: 'cancelled' 被调用
  4. scene 被标记为已完成

当 scene 已经在关闭时,清理函数调用 scene.cancel() 或 engine.stop() 会被忽略:onSceneExit 只触发一次。

ts
engine.onInvalidateBlock(({ scene, reason }) => {
  console.error('Validation failed:', reason);
  scene.cancel();
});
csharp
engine.OnInvalidateBlock(args => {
    Console.Error.WriteLine($"Validation failed: {args.Reason}");
    args.Scene.Cancel();
});
cpp
engine.onInvalidateBlock([](auto* scene, const auto& reason) {
    std::cerr << "Validation failed: " << reason << "\n";
    scene->cancel();
});
gdscript
engine.on_invalidate_block(func(args):
    printerr("Validation failed: %s" % args["reason"])
    args["scene"].cancel()
)

NativeProperties ​

控制 engine 如何调度 block 的执行属性:

字段类型描述
isAsyncboolean?在并行异步轨道上执行
delaynumber?block 播放前的毫秒数。由 onBeforeBlock 应用,engine 从不应用
timeoutnumber?台词说完之后 block 留在屏幕上的毫秒数,随后自己离开 — block 的自动推进。优先于 waitInput。原样传递,engine 不做任何强制
portPerCharacterboolean?metadata 中每个角色一个输出端口
skipIfMissingActorboolean?如果引用的角色不存在则跳过 block
debugboolean?编辑器调试标志
waitForBlocksstring[]?本 scene 的 block id。在它们全部完成之前,block 会在被分发之前被扣住。如果没有任何东西能完成它们,scene 会以 deadlocked 关闭
waitInputboolean?用于显式玩家输入控制的被动标志 — timeout 优先于它

Visual Reference ​

Block Execution Flow ​

Character Gating Flow ​