Block タイプ
block は dialogue scene の構成要素です — エディターグラフの各ノードが block です。engine は block から block へフローをルーティングし、各タイプに対応する handler を呼び出します。
タイプは5種類あります:Dialog、Choice、Condition、Action、Note。最初の4つは専用の handler(onDialog、onChoice、onCondition、onAction)を持つコンテンツ block です — 4つとも必須で、start() 呼び出し時に検証されます。Note block は自動的にスキップされます。
handler は2つのレベルで構成されます:global handler(engine に登録)はすべての scene をカバーし、ほとんどのゲームではこれだけで十分です。scene handler(SceneHandle に登録)は、特定の scene で global を補完または上書きできます。詳細は Handlers を参照してください。
DIALOG
dialog block はセリフを表します — キャラクターの会話、ナレーター、画面上のテキスト。engine は onResolveCharacter callback で話しているキャラクターを解決し、context.character として公開します。典型的な dialog handler はゲーム内でテキストインスタンス(テキストボックス、吹き出し、字幕…)を作成し、プレイヤーやアニメーションの完了を待ち、next() を呼び出して engine を進めます。オプションの cleanup 関数で、engine が次の block に移る際に副作用をクリーンアップできます。
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);
};
});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);
};
});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);
};
});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() でフローを進めます。
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();
});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();
});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(); };
});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/defaultport に従います。 - dispatcher モード(
enableDispatcher= true):マッチしたすべてのグループが async track として同時に発火します。false/defaultport はメインの継続 track("Continue")となり、常に実行されます。条件 port に接続される block は async でなければなりません。
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();
});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;
});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 {};
});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" にフォールバック)。
engine.onAction(({ block, context, next }) => {
const { actions } = block;
game
.executeActionsList(actions)
.then(() => context.resolve())
.catch((err) => context.reject(err))
.finally(() => next());
});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;
});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 {};
});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):
| フィールド | 型 | 説明 |
|---|---|---|
uuid | string | 一意識別子 |
type | BlockType | 判別タイプ |
label | string? | 人間可読な名前 |
parentLabels | string[]? | エディター内の親フォルダー階層 |
properties | BlockProperty[] | キー・バリュープロパティ |
userProperties | Record? | 自由形式のユーザープロパティ |
nativeProperties | NativeProperties? | 実行プロパティ |
metadata | BlockMetadata? | 表示メタデータ(キャラクター、タグ、カラー) |
isStartBlock | boolean? | エントリー block を示す |
NativeProperties
| フィールド | 型 | 説明 |
|---|---|---|
isAsync | boolean? | 並列 async トラックで実行 |
delay | number? | 実行前の遅延(onBeforeBlock で処理) |
timeout | number? | 実行タイムアウト |
portPerCharacter | boolean? | metadata 内のキャラクターごとに1つの出力 port |
skipIfMissingActor | boolean? | 参照アクターが不在の場合 block をスキップ |
debug | boolean? | エディタ用デバッグフラグ |
waitForBlocks | string[]? | この block が進行する前に訪問済みでなければならない block UUID |
waitInput | boolean? | プレイヤー入力制御用パッシブフラグ |
enableDispatcher | boolean? | dispatcher モード:マッチしたすべての条件が async track として発火、false/default port は継続 track |
