Skip to content

Async Tracks ​

Quand un block a props.isAsync = true, le engine crée un track parallèle qui s'exécute indépendamment du flow principal.

Comment les tracks sont créés ​

Pendant la résolution des ports, si plusieurs connections sortantes existent :

  • La première connection non-async devient la continuation du flow courant
  • Les autres connections (vers des blocks avec isAsync) deviennent des tracks parallèles

Ceci s'applique au main track et aux async tracks — un async track peut créer des sub-tracks depuis ses propres connections async, formant une hiérarchie d'exécution parallèle.

Cycle de vie des tracks ​

  • onBeforeBlock est appelé pour tous les blocks (main et async tracks) — voir Lifecycle pour le détail de resolve()
  • Les async tracks séparent les connections sortantes en main vs async, comme le main track
  • Les tracks sont automatiquement annulés quand la scène se termine ou que cancel() est appelé
  • Quand un track se termine naturellement (plus de connections), ses sub-tracks continuent de vivre indépendamment
  • Quand un track est explicitement annulé (cancel()), l'annulation cascade vers tous les tracks enfants

waitForBlocks — attendre une autre piste ​

props.waitForBlocks retient un bloc tant que les blocs qu'il nomme ne sont pas terminés. C'est la moitié « jonction » de l'embranchement qu'ouvre isAsync : une branche part en parallèle, et un bloc en aval attend qu'elle soit finie avant de jouer.

Terminé, pas atteint. Un bloc listé compte à partir du moment où le flux l'a quitté : votre jeu a appelé next(), le port de sortie a été résolu, et le nettoyage du bloc a tourné. Donc la bulle est retirée de l'écran et la voix est coupée avant que la réplique de jonction ne soit dispatchée — c'est exactement à quoi sert de dessiner une jonction.

Changement de règle

Dans les premières versions 2.x, être atteint suffisait. Ça rendait la propriété quasi inerte dans la forme que les designers dessinent réellement : un embranchement vers deux blocs, puis une jonction sur les deux, se levait dans le tick même où elle était enregistrée — les deux blocs venaient d'être dispatchés une fraction de milliseconde plus tôt — et la réplique de jonction parlait par-dessus eux.

getVisitedBlocks() n'est pas touché : il liste toujours ce que le joueur a vu, ce qui inclut un bloc encore en train de parler.

Le bloc est retenu AVANT d'être dispatché. Aucun handler n'est appelé, donc votre jeu n'apprend jamais que le bloc existe tant que l'attente n'est pas levée — rien de lui ne peut arriver à l'écran trop tôt. C'est une décision du moteur, pas un choix d'affichage que vous pourriez faire autrement : waitForBlocks est une propriété native, le designer la coche dans LSDE, et le moteur la lui doit.

La règle est la même sur toutes les pistes, celle que le joueur regarde comprise.

  • Les ids nomment des blocs de cette scène. Un fil n'a jamais franchi de frontière de scène.
  • Il faut que tous soient terminés, pas seulement un.
  • Terminer un bloc libère tout ce qui l'attendait, en chaîne.
  • Un bloc qui ne se termine jamais gare sa piste pour de bon — et un bloc qui attend une entrée du joueur pour toujours ne se termine jamais. init() signale UNKNOWN_WAIT_BLOCK quand un id n'est pas un bloc de la scène, mais il ne peut pas savoir si un bloc réel sera jamais joué.

La séquence pour un bloc portant à la fois waitForBlocks et delay :

waitForBlocks gate → onBeforeBlock (delay) → handler → next()

waitInput — Flag d'input joueur ​

props.waitInput est un flag passif — le engine l'expose mais ne l'interprète pas. Votre handler de jeu le lit pour décider s'il faut attendre un input explicite du joueur.

API TrackInfo — Observabilité ​

Utilisez scene.getTrackInfos() pour inspecter les async tracks en cours. Retourne un snapshot readonly de l'état de chaque track :

ts
const tracks = scene.getTrackInfos();
for (const track of tracks) {
  console.log(`Track ${track.id} (parent: ${track.parentTrackId}) at block ${track.currentBlockId}`);
}
csharp
var tracks = scene.GetTrackInfos();
foreach (var track in tracks)
{
    Console.WriteLine($"Track {track.Id} (parent: {track.ParentTrackId}) at block {track.CurrentBlockId}");
}
cpp
auto tracks = scene->getTrackInfos();
for (const auto& track : tracks) {
    std::cout << "Track " << track.id
              << " (parent: " << track.parentTrackId << ")"
              << " at block " << track.currentBlockId << "\n";
}
gdscript
var tracks = scene.get_track_infos()
for track in tracks:
    print("Track %d (parent: %s) at block %s" % [
        track["id"], str(track["parentTrackId"]), track["currentBlockId"]])

Chaque TrackInfo contient : id, parentTrackId, startBlockId, currentBlockId, running.

Ce qui fonctionne dans les async tracks (et ce qui ne fonctionne pas) ​

Les async tracks sont conçus pour du contenu qui se déroule en parallèle de la conversation principale — effets ambient, animations parallèles, réactions de compagnons. Mais il y a des limites.

Contenu parallèle — cas d'usage valides :

Cas d'utilisationPourquoi ça fonctionne
Dialogue ambient de NPC ("barks")Blocks dialog sur un async track — les NPCs commentent ou réagissent pendant que la conversation principale continue
Réactions de personnages synchroniséesUtilisez waitForBlocks pour déclencher une réaction quand un block spécifique est terminé
Jouer des sons ambient ou de la musiqueBlock action, pas d'interaction joueur nécessaire
Mouvements de caméra parallèlesBlock action, s'exécute en parallèle
Effets avec timing précisCombinez waitForBlocks + delay pour un timing précis

Interaction joueur ou branching — à éviter :

Cas d'utilisationPourquoi c'est problématique
Block CHOICE dans un async trackLe joueur est déjà en interaction avec le main track — qui répond au choice async?
Changements critiques de game stateSi le async track est annulé (la scène se termine), l'action ne s'exécute jamais

Choices dans les async tracks

Un block CHOICE dans un async track implique que le joueur devrait faire une sélection pendant qu'il est déjà engagé avec le dialogue principal. Le scénario le plus courant est un "choix" piloté par l'IA (ex. un compagnon NPC auto-sélectionne basé sur sa personnalité). Si un async track atteint un block CHOICE sans scene-level handler qui auto-sélectionne, le flow va se bloquer silencieusement.

Plusieurs scènes en parallèle ​

Le engine supporte l'exécution de plusieurs scènes en même temps. Chaque SceneHandle a son propre state, ses blocks visités et ses async tracks. Les handlers globaux (Tier 1) sont partagés — utilisez l'argument scene pour identifier quelle scène appelle :

ts
engine.onDialog(({ scene, block, context, next }) => {
  if (scene === mainDialogue) {
    showMainUI(block);
    // next() called later — by player input
  } else if (scene === tutorialOverlay) {
    showTutorialBubble(block);
    // auto-advance after display
    next();
  }
});

const mainDialogue = engine.scene('main-quest');
const tutorialOverlay = engine.scene('tutorial-hints');
mainDialogue.start();
tutorialOverlay.start();
csharp
engine.OnDialog(args => {
    if (args.Scene == mainDialogue)
    {
        ShowMainUI(args.Block);
        // next() called later — by player input
    }
    else if (args.Scene == tutorialOverlay)
    {
        ShowTutorialBubble(args.Block);
        // auto-advance after display
        args.Next();
    }
    return null;
});

var mainDialogue = engine.Scene("main-quest");
var tutorialOverlay = engine.Scene("tutorial-hints");
mainDialogue.Start();
tutorialOverlay.Start();
cpp
engine.onDialog([&](auto* scene, auto* block, auto*, auto next) -> CleanupFn {
    if (scene == mainDialogue.get()) {
        showMainUI(block);
        // next() called later — by player input
    } else if (scene == tutorialOverlay.get()) {
        showTutorialBubble(block);
        // auto-advance after display
        next();
    }
    return {};
});

auto mainDialogue = engine.scene("main-quest");
auto tutorialOverlay = engine.scene("tutorial-hints");
mainDialogue->start();
tutorialOverlay->start();
gdscript
engine.on_dialog(func(args):
    if args["scene"] == main_dialogue:
        show_main_ui(args["block"])
        # next() called later — by player input
    elif args["scene"] == tutorial_overlay:
        show_tutorial_bubble(args["block"])
        # auto-advance after display
        args["next"].call()
    return Callable()
)

var main_dialogue = engine.scene("main-quest")
var tutorial_overlay = engine.scene("tutorial-hints")
main_dialogue.start()
tutorial_overlay.start()

Routing par scène

Avec plusieurs scènes concurrentes, il est préférable d'enregistrer des handlers scene-level (Tier 2) sur chaque handle au lieu de router dans le handler global. Meilleure séparation, pas de chaînes if/else.

Référence visuelle ​

  • Main track: A → B → C
  • Track 1 (parallèle): D → E
  • Track 2 (sub-track de D): F
  • Scene cancel → tous les tracks annulés
  • Track D se termine naturellement → F continue