Skip to content

非同期トラック ​

block に props.isAsync = true が設定されている場合、engine はメインフローとは独立して動作する並列トラックを作成します。

トラックの作成方法 ​

port 解決中に複数の送出 connection が存在する場合:

  • 最初の非 async connection が現在のフローの継続となります
  • その他の connection(isAsync を持つ block へ)が新しい並列トラックになります

これはメイントラックと async トラックの両方に適用されます — async トラックは独自の async connection からサブトラックを spawn でき、並列実行の階層を作成できます。

トラックのライフサイクル ​

  • onBeforeBlock はすべての block で呼び出されます(メインおよび async トラック) — resolve() の詳細は Lifecycle を参照
  • async トラックはメイントラックと同様に、送出 connection をメイン vs async に分離します
  • トラックは scene 終了時または cancel() 呼び出し時に自動的にキャンセルされます
  • トラックが自然に終了した場合(connection がなくなった)、サブトラックは独立して存続します
  • トラックが明示的にキャンセルされた場合(cancel())、キャンセルはすべての子トラックにカスケードします

waitForBlocks — 他のトラックを待つ ​

props.waitForBlocks は、指定された block が完了するまでその block を保留します。isAsync が開く分岐の「合流」側です:ブランチが並列で走り、下流の block はそれが終わるまで待ってから再生されます。

到達ではなく完了です。 リストされた block は、フローがそれを離れた時点で数えられます:ゲームが next() を呼び、出口ポートが解決され、その block の cleanup が実行された時点です。つまり合流するセリフが dispatch される前に吹き出しは画面から消え、音声も止まっています — 合流を描く目的はまさにそれです。

変更されたルール

2.x の最初のリリースでは、到達だけで十分でした。それはデザイナーが実際に描く形において、このプロパティをほぼ無効にしていました:2 つの block への分岐、そして両方への合流は、登録されたまさにその tick で解除されていたのです — 2 つはほんの数分の 1 ミリ秒前に dispatch されていたため — そして合流するセリフがその上に重なって話していました。

getVisitedBlocks() は影響を受けません:プレイヤーが見たものを引き続き列挙し、それには話している途中の block も含まれます。

block は dispatch される前に保留されます。 handler は呼び出されないため、待機が解除されるまでゲーム側は block の存在すら知りません — その内容が早すぎるタイミングで画面に出ることはありません。これは描画の選択ではなくエンジンの決定です:waitForBlocks は native プロパティであり、デザイナーが LSDE でチェックした以上、エンジンがその動作を保証します。

このルールはプレイヤーが見ているトラックを含め、すべてのトラックで同じです。

  • id はこのシーンの block を指します。ワイヤーがシーンをまたいだことは一度もありません。
  • 1 つではなく、すべてが完了している必要があります。
  • block が完了すると、それを待っていたものが連鎖的に解放されます。
  • 決して完了しない block はトラックを永久に停留させます — プレイヤー入力を永遠に待つ block は決して完了しません。id がシーンの block でない場合、init() は UNKNOWN_WAIT_BLOCK を報告しますが、実在する block が実際に再生されるかどうかは知り得ません。

waitForBlocks と delay の両方を持つ block のシーケンス:

waitForBlocks ゲート → onBeforeBlock (delay) → handler → next()

waitInput — プレイヤー入力フラグ ​

props.waitInput はパッシブフラグです — engine はそれを公開しますが解釈しません。ゲーム handler がそれを読み取り、明示的なプレイヤー入力を待つかどうかを決定します。

TrackInfo API — 可観測性 ​

scene.getTrackInfos() を使用して実行中の async トラックを検査します。各トラックの状態の読み取り専用スナップショットを返します:

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"]])

各 TrackInfo には id、parentTrackId、startBlockId、currentBlockId、running が含まれます。デバッグオーバーレイ、プレイモードレンダラー、検証に使用します。

Async トラックで動作するもの(と動作しないもの) ​

async トラックは、メインの会話と並行して起こること — 環境エフェクト、並列アニメーション、仲間の反応 — に最適です。ただし制限があります。

推奨 — 並列コンテンツ:

ユースケース動作する理由
NPC の環境セリフ(「バーク」)async トラック上の dialog block — NPC がメインの会話の進行中にコメント、反応、掛け合いを行います
イベントに同期したキャラクター反応waitForBlocks を使用して特定の block に到達したときに反応をトリガー
環境音やBGMの再生action block、プレイヤーのインタラクション不要
カメラ移動のトリガーaction block、並列実行
精密なタイミングのエフェクトwaitForBlocks + delay を組み合わせて精密なタイミングを実現

非推奨 — プレイヤーインタラクションやゲームロジック分岐:

ユースケース問題となる理由
async トラック内の CHOICE blockプレイヤーは既にメイントラックとインタラクション中 — 誰が async の choice に応答するのか?
重要なゲームステート変更async トラックがキャンセルされた場合(scene 終了)、action は実行されません

async トラック内の choice

async トラック内の CHOICE block は、プレイヤーがメインの対話に既に参加している間に選択を行うべきことを意味します。最も一般的なシナリオは AI 駆動の「choice」(例:仲間の NPC がパーソナリティに基づいて自動選択する)です。async トラックが自動選択する scene レベル handler なしで CHOICE block に到達した場合、フローは停止するか無言で終了します。

複数の Scene の並列実行 ​

engine は複数の scene の同時実行をサポートしています。各 SceneHandle は独自のステート、訪問済み block、async トラックを持ちます。グローバル handler(Tier 1)は共有されます — どの scene が呼び出しているかは scene 引数で判別できます:

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

Scene ごとのルーティング

並行する scene が多い場合は、グローバル handler 内でルーティングする代わりに、各ハンドルに scene レベル(Tier 2)の handler を登録することを検討してください。よりクリーンな分離が実現でき、if/else チェーンが不要になります。

Visual Reference ​

  • Main track: A → B → C
  • Track 1 (parallel): D → E
  • Track 2 (sub-track of D): F
  • Scene cancel → all tracks cancelled
  • Track D ends naturally → F continues