Skip to content

ライフサイクルと検証 ​

各 Block の実行順序 ​

  1. 前の block のクリーンアップ — 前の block の handler が返したクリーンアップ関数が遷移時に実行されます(next() が呼ばれた時点)
  2. onValidateNextBlock — 実行前の検証
  3. onBeforeBlock — 前処理(続行するには resolve() を呼び出す必要あり)
  4. タイプ handler(Tier 2、次に Tier 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() を呼び出した側 — ゲームでは多くの場合クリックやタイマー — へスローされ、同時に onSceneExit の context.error にも渡されます。対話の終了を待つコードが聞いて いるのはこちらです。ログはどちらか一方でのみ出してください。

onSceneExit 自体がスローした例外も同じ経路で届き、それでも scene は解放されます: engine.isRunning() はもうそれを数えません。ただし scene がすでにエラーで閉じようとしていた場合は、 そのエラーの方が届きます — 残りを説明するのはそちらです。

GDScript

GDScript には例外がありません。handler 内のスクリプトエラーは Godot のログに出力され、呼び出しは null を返します:block は来ることのない next() をただ待ち続けます。scene を代わりに閉じるものは何もありません。

なぜ変わったのか

v1 は handler の例外を静かに飲み込んでいました — ログにも残さず — 一方で、その同じ handler が返した クリーンアップからの例外は呼び出し側に届いていました。ひとつの障害に対して二つの正反対の挙動であり、 静かな方はプロジェクトが動き続ける限り本物のバグを隠していました。

2.0.0 はそれをタイプ handler についてだけ直していました。スローする検証、onBeforeBlock、リゾルバ — あるいは並列トラック上の handler — は、進めるものが何もないまま scene を開いたままにし、対話の終了を 待つゲームは永遠に待ち続けていました。

Scene が終わった理由 ​

onSceneExit は context.reason でその理由を受け取ります:

reasonいつ
completedフローがグラフの終わりに達した
cancelledscene.cancel() または engine.stop()
invalidated最後に動いていたトラックが入ろうとした block を onValidateNextBlock が拒否した
faulted走査中にあなたのコードがスローした。context.error にスローされたものが入っている — エラー境界を参照
deadlocked残っているトラックがすべて、何も完了させられない waitForBlocks で待機している。context.waitingFor がそれらの block を示す

onSceneEnter には理由が渡されません。

scene.getSceneId() はどの scene が終わったかを示し、scene.getScenePath() はそのパスを返します。 2 つの scene が同時に再生されると、グローバルな onSceneExit にはこれが必要になります。パスではなく id を保存してください:scene の名前が変わった日にパスは変わります。

デッドロックは scene を閉じます。 どのトラックも完了させない block を待つ block — たとえばフローが 通らなかったブランチの block — は、onSceneExit もなく scene を永久に開いたままにしていました。いまは 動けるトラックが最後に止まった瞬間に scene が deadlocked で閉じ、waitingFor がどの合流の配線が 誤っているかを教えてくれます。

ひとつのスレッド ​

engine はスレッドセーフではありません。next()、resolve()、cancel() は scene を開始したスレッド から呼び出してください — Unity ではメインスレッド、Unreal ではゲームスレッドです。

C# と C++ では、別のスレッドからの呼び出しは両方のスレッドを示す例外で拒否され、何も変わりません: 正しいスレッドから呼べば同じ next() がそのまま動きます。Unity では engine を呼ぶ前にメインスレッドへ 戻してください(await UniTask.SwitchToMainThread()、またはメインスレッド用のキューに入れる)。

cancel() ​

scene.cancel() を呼び出すと、以下のシーケンスが実行されます:

  1. すべての async トラック がキャンセルされます
  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?並列 async トラックで実行
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 ​