ライフサイクルと検証
各 Block の実行順序
- 前の block のクリーンアップ — 前の block の handler が返したクリーンアップ関数が遷移時に実行されます(
next()が呼ばれた時点) onValidateNextBlock— 実行前の検証onBeforeBlock— 前処理(続行するにはresolve()を呼び出す必要あり)- タイプ handler(Tier 2、次に Tier 1)
Scene イベント
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);
}
});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)}");
});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
}
});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) の解決されたキャラクターを受け取ります:
engine.onValidateNextBlock(({ nextBlock, fromBlock, nextContext, fromContext }) => {
return { valid: true };
});
engine.onInvalidateBlock(({ scene, reason }) => {
console.error('Invalid block:', reason);
scene.cancel();
});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();
});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();
});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 の実行を制御します:
// 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 };
});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();
});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};
});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() を呼び出す必要があります:
engine.onBeforeBlock(({ block, resolve }) => {
const delay = LsdeUtils.getNativeProperties(block)?.delay;
if (delay) {
setTimeout(resolve, delay * 1000);
} else {
resolve();
}
});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();
}
});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();
}
});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 から離れる際に呼び出されます:
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
};
});engine.OnDialog(args => {
var element = ShowDialogUI(args.Block);
// next() is called later — by player input, timer, etc.
return () => element.SetActive(false);
});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(); };
});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 | フローがグラフの終わりに達した |
cancelled | scene.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() を呼び出すと、以下のシーケンスが実行されます:
- すべての async トラック がキャンセルされます
- 現在の block のクリーンアップ関数が実行されます
onSceneExithandler がreason: 'cancelled'で呼び出されます- scene が完了としてマークされます
scene がすでに閉じつつあるときにクリーンアップが scene.cancel() や engine.stop() を呼んでも無視され ます:onSceneExit は一度だけ発火します。
engine.onInvalidateBlock(({ scene, reason }) => {
console.error('Validation failed:', reason);
scene.cancel();
});engine.OnInvalidateBlock(args => {
Console.Error.WriteLine($"Validation failed: {args.Reason}");
args.Scene.Cancel();
});engine.onInvalidateBlock([](auto* scene, const auto& reason) {
std::cerr << "Validation failed: " << reason << "\n";
scene->cancel();
});engine.on_invalidate_block(func(args):
printerr("Validation failed: %s" % args["reason"])
args["scene"].cancel()
)NativeProperties
engine が block をディスパッチする方法を制御する実行プロパティ:
| フィールド | 型 | 説明 |
|---|---|---|
isAsync | boolean? | 並列 async トラックで実行 |
delay | number? | block が再生されるまでのミリ秒。onBeforeBlock が適用し、engine は決して適用しません |
timeout | number? | セリフを言い終えたあと block が画面に残るミリ秒。そのあと自分から離れます — block のオートアドバンス。waitInput より優先。そのまま渡され、engine は何も強制しません |
portPerCharacter | boolean? | metadata 内のキャラクターごとに出力ポートを作成 |
skipIfMissingActor | boolean? | 参照されたアクターが不在の場合、block をスキップ |
debug | boolean? | エディタ用デバッグフラグ |
waitForBlocks | string[]? | この scene の block id。それらがすべて完了するまで、block はディスパッチされる前に保持されます。何もそれらを完了させられない場合、scene は deadlocked で閉じます |
waitInput | boolean? | 明示的なプレイヤー入力制御用のパッシブフラグ — timeout が優先します |
