Skip to content

Lifecycle & Validation ​

Ordre d'exécution pour chaque block ​

  1. Cleanup du block précédent — La fonction de cleanup retournée par le handler du block précédent s'exécute au moment de la transition (quand next() est appelé)
  2. onValidateNextBlock — Validation avant exécution
  3. onBeforeBlock — Pré-traitement (doit appeler resolve() pour continuer)
  4. Handler de type (Tier 2 puis Tier 1)

Events de scène ​

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 ​

Intercepte chaque transition de block pour validation. Le handler reçoit le personnage résolu du prochain block (nextContext) et du block précédent (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 ​

Utilisez nextContext.character pour contrôler quels blocks peuvent s'exécuter selon l'état du jeu :

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}
)

Utilisez fromContext.character pour valider les transitions entre personnages (ex: relations, cooldowns). fromContext est null pour le premier block d'une scène.

onBeforeBlock ​

Appelé avant chaque block. resolve() doit être appelé pour continuer :

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()
)

Un resolve() appelé pendant que onBeforeBlock s'exécute encore prend effet à son retour — exactement comme next() appelé dans un handler. Le handler de type est dispatché une fois votre callback terminé : une ligne écrite après resolve() s'exécute donc avant le block, pas après.

Fonctions de cleanup ​

Un handler peut retourner une fonction de cleanup, appelée quand le block est quitté :

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()
)

Error Boundaries ​

Rien n'est avalé. Si votre code throw pendant que le engine parcourt le graphe — un handler de type, une fonction de cleanup, onValidateNextBlock, onInvalidateBlock, onBeforeBlock, onResolveCondition, onResolveCharacter ou onSceneEnter — le engine ferme d'abord toute la scène, puis re-throw l'erreur à celui qui a appelé start(), next() ou resolve().

C'est vrai sur chaque track. Un handler qui throw sur une branche isAsync ferme la scène, pas seulement sa branche.

C'est l'ordre qui rend la chose utilisable. Au moment où l'erreur atteint votre code :

  • les fonctions de cleanup ont tourné
  • les tracks async sont annulés
  • onSceneExit a été appelé, avec reason: 'faulted' et l'erreur elle-même dans context.error

Le dialogue s'est arrêté proprement, et c'est vous qui décidez de la suite — continuer sans lui, afficher un écran, ou laisser planter. Mettez votre propre try/catch autour de start(), next() ou resolve().

L'erreur arrive à deux endroits, exprès. Elle est lancée à celui qui a appelé next() — dans un jeu, souvent un clic ou un timer — et elle est remise à onSceneExit dans context.error, là où écoute le code qui attend la fin du dialogue. Journalisez-la à un seul des deux endroits.

Une exception levée par onSceneExit lui-même vous parvient de la même façon, et la scène est libérée malgré tout : engine.isRunning() ne la compte plus. Sauf si la scène se fermait déjà sur une faute : vous recevez alors cette faute, celle qui explique le reste.

GDScript

GDScript n'a pas d'exceptions. Une erreur de script dans un handler part dans le log de Godot et l'appel retourne null : le block attend simplement un next() qui ne viendra jamais. Rien ne ferme la scène à votre place.

Pourquoi cela a changé

La v1 avalait silencieusement l'exception d'un handler — sans même la loguer — pendant qu'une exception venant du cleanup retourné par ce même handler, elle, atteignait l'appelant. Une seule faute, deux comportements opposés, et le silencieux a caché de vrais bugs aussi longtemps qu'un projet a tourné.

La 2.0.0 l'avait corrigé pour le handler de type seulement. Une validation, un onBeforeBlock ou un resolver qui throw — ou un handler sur un track parallèle — laissait encore la scène ouverte sans rien pour la faire avancer, et un jeu qui attendait la fin du dialogue attendait pour toujours.

Pourquoi une scène s'est terminée ​

onSceneExit reçoit la raison dans context.reason :

reasonQuand
completedLe flux n'a plus de graphe
cancelledscene.cancel() ou engine.stop()
invalidatedonValidateNextBlock a refusé le block où entrait le dernier track vivant
faultedVotre code a throw pendant le parcours. context.error contient ce qui a été lancé — voir Error Boundaries
deadlockedTous les tracks restants sont parqués sur un waitForBlocks que rien ne peut terminer. context.waitingFor nomme ces blocks

onSceneEnter ne reçoit pas de raison.

scene.getSceneId() dit quelle scène s'est terminée, et scene.getScenePath() donne son chemin. Un onSceneExit global en a besoin dès que deux scènes jouent en même temps. Stockez l'id, pas le chemin : le chemin change le jour où quelqu'un renomme la scène.

Un interblocage ferme la scène. Un block qui attend un block qu'aucun track ne terminera jamais — par exemple un block d'une branche que le flux n'a pas prise — gardait la scène ouverte pour de bon, sans onSceneExit. La scène se ferme maintenant en deadlocked dès que le dernier track capable d'avancer s'arrête, et waitingFor vous dit quelle jointure est mal câblée.

Un seul thread ​

Le engine n'est pas thread-safe. Appelez next(), resolve() et cancel() depuis le thread qui a démarré la scène — dans Unity, le thread principal ; dans Unreal, le game thread.

En C# et en C++, un appel venant d'un autre thread est refusé par une exception qui nomme les deux threads, et il ne change rien : le même next() fonctionne encore une fois appelé depuis le bon thread. Dans Unity, revenez sur le thread principal avant d'appeler le engine (await UniTask.SwitchToMainThread(), ou mettez l'appel en file pour le thread principal).

cancel() ​

Appeler scene.cancel() déclenche cette séquence :

  1. Tous les async tracks sont annulés
  2. La fonction de cleanup du block courant est exécutée
  3. Le handler onSceneExit est appelé, avec reason: 'cancelled'
  4. La scène est marquée comme terminée

Un cleanup qui appelle scene.cancel() ou engine.stop() pendant que la scène se ferme déjà est ignoré : onSceneExit n'est appelé qu'une fois.

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 ​

Propriétés d'exécution qui contrôlent comment un block est dispatché par le engine :

ChampTypeDescription
isAsyncboolean?Exécuter sur un track async parallèle
delaynumber?MILLISECONDES avant que le block joue. Appliqué par onBeforeBlock, jamais par le engine
timeoutnumber?MILLISECONDES pendant lesquelles le block RESTE après que sa réplique a été dite, puis il repart de lui-même — une auto-avance pour les blocks. Prime sur waitInput. Passé tel quel ; le engine n'impose rien
portPerCharacterboolean?Un port de sortie par personnage dans metadata
skipIfMissingActorboolean?Sauter le block si l'acteur référencé est absent
debugboolean?Flag de debug pour l'éditeur
waitForBlocksstring[]?Ids de blocks de cette scène. Le block est retenu avant d'être dispatché tant qu'ils ne sont pas tous terminés. Si rien ne peut jamais les terminer, la scène se ferme en deadlocked
waitInputboolean?Flag passif pour contrôle explicite de l'input joueur

Référence visuelle ​

Flow d'exécution des blocks ​

Flow de Character Gating ​