Skip to content

Block タイプ

block は dialogue scene の構成要素です — エディターグラフの各ノードが block です。engine は block から block へフローをルーティングし、各タイプに対応する handler を呼び出します。

タイプは5種類あります:DialogChoiceConditionActionNote。最初の4つは専用の handler(onDialogonChoiceonConditiononAction)を持つコンテンツ block です — 4つとも必須で、start() 呼び出し時に検証されます。Note block は自動的にスキップされます。

handler は2つのレベルで構成されます:global handler(engine に登録)はすべての scene をカバーし、ほとんどのゲームではこれだけで十分です。scene handlerSceneHandle に登録)は、特定の scene で global を補完または上書きできます。詳細は Handlers を参照してください。

DIALOG

dialog block はセリフを表します — キャラクターの会話、ナレーター、画面上のテキスト。engine は onResolveCharacter callback で話しているキャラクターを解決し、context.character として公開します。典型的な dialog handler はゲーム内でテキストインスタンス(テキストボックス、吹き出し、字幕…)を作成し、プレイヤーやアニメーションの完了を待ち、next() を呼び出して engine を進めます。オプションの cleanup 関数で、engine が次の block に移る際に副作用をクリーンアップできます。

ts
engine.onDialog(({ block, context, next }) => {
  const { dialogueText, nativeProperties } = block;
  const { character, resolveCharacterPort } = context;
  const text = game.getLocalizedText(dialogueText);
  const emotion = game.getCharacterEmotion(character);

  character && resolveCharacterPort(character.uuid);

  game.moveCameraToCharacter(character);
  game.animateCharacter(character, emotion);

  const dialog = game.createDialog(text, character, emotion);
  const shouldWaitInput = game.shouldWaitPlayerInputForDialog(nativeProperties);

  // next() tells the engine this block is done — call it when the player
  // dismisses the dialog or when the text animation finishes on its own
  if (shouldWaitInput) {
    dialog.onInput(() => next(), { once: true });
  } else {
    dialog.then(() =>
      game.wait(nativeProperties?.timeout ?? 0).then(() => next()),
    );
  }

  // cleanup: runs when the engine moves to the next block
  return () => {
    dialog.destroy();
    game.animateCharacter(character, false);
  };
});
csharp
engine.OnDialog(args => {
    var (scene, block, context, next) = args;
    var (text, ch, emotion) = (
        Game.GetLocalizedText(block.DialogueText),
        context.Character,
        Game.GetCharacterEmotion(context.Character)
    );

    if (ch != null) context.ResolveCharacterPort(ch.Uuid);

    Game.MoveCameraToCharacter(ch);
    Game.AnimateCharacter(ch, emotion);

    var dialog = Game.CreateDialog(text, ch, emotion);
    var shouldWaitInput = Game.ShouldWaitPlayerInputForDialog(block.NativeProperties);

    // next() tells the engine this block is done — call it when the player
    // dismisses the dialog or when the text animation finishes on its own
    if (shouldWaitInput)
        dialog.OnInput(() => next(), once: true);
    else
        dialog.Then(() =>
            Game.Wait(block.NativeProperties?.Timeout ?? 0).Then(() => next()));

    // cleanup: runs when the engine moves to the next block
    return () => {
        dialog.Destroy();
        Game.AnimateCharacter(ch, false);
    };
});
cpp
engine.onDialog([&game](auto* scene, auto* block, auto* ctx, auto next) -> CleanupFn {
    auto* ch = ctx->character();
    auto text = game.getLocalizedText(block->dialogueText);
    auto emotion = game.getCharacterEmotion(ch);

    if (ch) ctx->resolveCharacterPort(ch->uuid);

    game.moveCameraToCharacter(ch);
    game.animateCharacter(ch, emotion);

    auto* dialog = game.createDialog(text, ch, emotion);
    auto shouldWaitInput = game.shouldWaitPlayerInputForDialog(block->nativeProperties);

    // next() tells the engine this block is done — call it when the player
    // dismisses the dialog or when the text animation finishes on its own
    if (shouldWaitInput) {
        dialog->onInput([next]() { next(); });
    } else {
        dialog->then([&game, next, block]() {
            game.wait(block->nativeProperties ? block->nativeProperties->timeout.value_or(0) : 0)
                .then([next]() { next(); });
        });
    }

    // cleanup: runs when the engine moves to the next block
    return [dialog, &game, ch]() {
        dialog->destroy();
        game.animateCharacter(ch, false);
    };
});
gdscript
engine.on_dialog(func(args):
    var block = args["block"]
    var ctx = args["context"]
    var next_fn = args["next"]
    var ch = ctx.character
    var text = game.get_localized_text(block.get("dialogueText"))
    var emotion = game.get_character_emotion(ch)

    if ch:
        ctx.resolve_character_port(ch.get("uuid", ""))

    game.move_camera_to_character(ch)
    game.animate_character(ch, emotion)

    var dialog = game.create_dialog(text, ch, emotion)
    var should_wait = game.should_wait_player_input(block.get("nativeProperties"))

    # next_fn.call() tells the engine this block is done — call it when the player
    # dismisses the dialog or when the text animation finishes on its own
    if should_wait:
        dialog.on_input(func(): next_fn.call(), true)
    else:
        await dialog.wait()
        await game.wait(block.get("nativeProperties", {}).get("timeout", 0))
        next_fn.call()

    # cleanup: runs when the engine moves to the next block
    return func():
        dialog.destroy()
        game.animate_character(ch, false)
)

ナラティブデザイナーがキャラクターごとに専用の出力を割り当てた場合(portPerCharacter)、handler は resolveCharacterPort() を呼び出して next() 時にどのパスを辿るかを engine に伝える必要があります。

CHOICE

choice block はプレイヤーが選択する分岐点です — ダイアログメニュー、選択肢リスト。context.choices に全ての選択肢が含まれます。onResolveCondition() が設定されている場合、各選択肢は visible: true | false でタグ付けされ、handler が表示する選択肢をフィルタリングします。プレイヤーの操作後、selectChoice(uuid) で engine にどのパスを辿るかを伝え、next() でフローを進めます。

ts
engine.onChoice(({ block, context, next }) => {
  const { nativeProperties } = block;
  const { choices, selectChoice } = context;

  const visible = choices.filter(c => c.visible !== false);
  const dialog = game.createChoice(visible);

  // when the player picks a choice, select it and advance
  dialog
    .then((selected) => selectChoice(selected))
    .finally(() => next());

  // optional: if the narrative designer set a timeout on this block
  if (nativeProperties?.timeout) {
    const timeout = game.wait(nativeProperties.timeout).then(() => next());
    dialog.finally(() => timeout.cancel());
  }

  return () => dialog.destroy();
});
csharp
engine.OnChoice(args => {
    var (scene, block, context, next) = args;
    var visible = context.Choices
        .Where(c => c.Visible != false).ToList();

    var dialog = Game.CreateChoice(visible);

    // when the player picks a choice, select it and advance
    dialog.OnSelect(selected => {
        context.SelectChoice(selected);
        next();
    });

    // optional: if the narrative designer set a timeout on this block
    if (block.NativeProperties?.Timeout is { } timeout)
        Game.Wait(timeout).Then(() => next());

    return () => dialog.Destroy();
});
cpp
engine.onChoice([&game](auto* scene, auto* block, auto* ctx, auto next) -> CleanupFn {
    std::vector<const RuntimeChoiceItem*> visible;
    for (const auto& c : ctx->choices())
        if (!c.visible.has_value() || c.visible.value())
            visible.push_back(&c);

    auto* dialog = game.createChoice(visible);

    // when the player picks a choice, select it and advance
    dialog->onSelect([ctx, next](const auto& selected) {
        ctx->selectChoice(selected);
        next();
    });

    // optional: if the narrative designer set a timeout on this block
    if (block->nativeProperties && block->nativeProperties->timeout) {
        game.wait(*block->nativeProperties->timeout).then([next]() { next(); });
    }

    return [dialog]() { dialog->destroy(); };
});
gdscript
engine.on_choice(func(args):
    var block = args["block"]
    var ctx = args["context"]
    var next_fn = args["next"]

    var visible = []
    for c in ctx.choices:
        if c.get("visible") != false:
            visible.append(c)

    var dialog = game.create_choice(visible)

    # when the player picks a choice, select it and advance
    var selected = await dialog.choice_selected
    ctx.select_choice(selected)
    next_fn.call()

    # optional: if the narrative designer set a timeout on this block
    # use a Timer node or game.wait() to handle timeouts natively

    return func(): dialog.destroy()
)

完全なオプトイン方式のタグ付けシステムについては Choice の表示制御 を参照してください。

CONDITION

condition block は不可視のスイッチです — ゲーム状態を評価し、プレイヤーに見えることなくフローを2つのパスのどちらかに送ります。handler は block の条件(変数、フラグ、インベントリ…)を評価し、context.resolve(result) を呼び出します — true は port 0 に、false は port 1 に従います。choice: で始まるキーの条件はプレイヤーの過去の選択を参照しており、scene.evaluateCondition(cond) が内部の履歴から自動的に解決します。

condition block は2つの評価モードをサポートしています:

  • switch モード(デフォルト):条件グループを順番に評価します。最初にマッチしたグループが対応する port(true/case_N)にルーティングします。マッチしない場合は false/default port に従います。
  • dispatcher モードenableDispatcher = true):マッチしたすべてのグループが async track として同時に発火します。false/default port はメインの継続 track("Continue")となり、常に実行されます。条件 port に接続される block は async でなければなりません。
ts
engine.onCondition(({ block, context, next }) => {
  const { conditionGroups } = context;
  const isDispatcher = !!block.nativeProperties?.enableDispatcher;

  // conditionGroups are pre-evaluated when onResolveCondition is installed.
  // Each group has: conditions, portIndex, result (true/false/undefined).
  const matched = conditionGroups
    .filter((g) => g.result)
    .map((g) => g.portIndex);

  // switch mode: first match index or -1 (default port)
  // dispatcher mode: all matched indices (each fires an async track)
  const result = isDispatcher ? matched : (matched[0] ?? -1);
  context.resolve(result);
  next();
});
csharp
engine.OnCondition(args => {
    var groups = args.Context.ConditionGroups!;
    var isDispatcher = args.Block.NativeProperties?.EnableDispatcher == true;

    var matched = groups.Where(g => g.Result == true).Select(g => g.PortIndex).ToList();
    object result = isDispatcher ? (object)matched : (object)(matched.Count > 0 ? matched[0] : -1);
    args.Context.Resolve(result);
    args.Next();
    return null;
});
cpp
engine.onCondition([](auto*, auto* block, auto* ctx, auto next) -> CleanupFn {
    // Result is auto-resolved by the engine when onResolveCondition is installed.
    // Override with ctx->resolve(result) if needed.
    next();
    return {};
});
gdscript
engine.on_condition(func(args):
    var ctx = args["context"]
    var groups = ctx.condition_groups
    var np = args["block"].get("nativeProperties")
    var is_dispatcher = np is Dictionary and np.get("enableDispatcher", false)

    var matched = []
    for g in groups:
        if g.get("result", false):
            matched.append(g.get("port_index", 0))

    var result = matched if is_dispatcher else (matched[0] if matched.size() > 0 else -1)
    ctx.resolve(result)
    args["next"].call()
    return Callable()
)

ACTION

action block はゲーム内で副作用を発動します — アイテムの付与、サウンドの再生、フラグの設定。各アクションは開発者が自身のシステムにマッピングする actionId を参照します。handler はアクションリストを実行し、context.resolve() で "then" port を、context.reject(error) で "catch" port を辿ります("catch" 接続がない場合は "then" にフォールバック)。

ts
engine.onAction(({ block, context, next }) => {
  const { actions } = block;
  game
    .executeActionsList(actions)
    .then(() => context.resolve())
    .catch((err) => context.reject(err))
    .finally(() => next());
});
csharp
engine.OnAction(args => {
    var (scene, block, context, next) = args;
    Game.ExecuteActionsList(block.Actions, onComplete: () => {
        context.Resolve();
        next();
    }, onError: err => {
        context.Reject(err);
        next();
    });
    return null;
});
cpp
engine.onAction([&game](auto* scene, auto* block, auto* ctx, auto next) -> CleanupFn {
    auto* ab = dynamic_cast<const ActionBlock*>(block);
    game.executeActionsList(ab->actions, [ctx, next]() {
        ctx->resolve();
        next();
    }, [ctx, next](const auto& err) {
        ctx->reject(err);
        next();
    });
    return {};
});
gdscript
engine.on_action(func(args):
    var block = args["block"]
    var ctx = args["context"]
    var next_fn = args["next"]

    await game.execute_actions_list(block.get("actions", []))
    ctx.resolve()
    next_fn.call()
)

NOTE

note block はナラティブデザイナーのためのメモです — コメント、リマインダー、コンテキスト。走査中は自動的にスキップされます。onBeforeBlock で note block をインターセプトすることは技術的に可能ですが、推奨されません — action block がすべての副作用のニーズをカバーできます。

共通プロパティ

すべての block は以下の基本フィールドを共有します(BlueprintBlockBase):

フィールド説明
uuidstring一意識別子
typeBlockType判別タイプ
labelstring?人間可読な名前
parentLabelsstring[]?エディター内の親フォルダー階層
propertiesBlockProperty[]キー・バリュープロパティ
userPropertiesRecord?自由形式のユーザープロパティ
nativePropertiesNativeProperties?実行プロパティ
metadataBlockMetadata?表示メタデータ(キャラクター、タグ、カラー)
isStartBlockboolean?エントリー block を示す

NativeProperties

フィールド説明
isAsyncboolean?並列 async トラックで実行
delaynumber?実行前の遅延(onBeforeBlock で処理)
timeoutnumber?実行タイムアウト
portPerCharacterboolean?metadata 内のキャラクターごとに1つの出力 port
skipIfMissingActorboolean?参照アクターが不在の場合 block をスキップ
debugboolean?エディタ用デバッグフラグ
waitForBlocksstring[]?この block が進行する前に訪問済みでなければならない block UUID
waitInputboolean?プレイヤー入力制御用パッシブフラグ
enableDispatcherboolean?dispatcher モード:マッチしたすべての条件が async track として発火、false/default port は継続 track