ゲームエンジン統合
LSDE はエンジン非依存です — ゲームエンジン、UIフレームワーク、オーディオシステムへの依存はありません。グラフを走査し、登録された handler を呼び出します。このページでは、主要なゲームエンジンへの組み込み方法を示します。
handler の詳細な実装については、Block Types と Handlers を参照してください。
完全な統合例
以下の例は、各エンジンに LSDE を統合する一つの方法を示しています。必須の 4 つの handler — dialog、choice、condition、action — を一つのクラスにまとめた、出発点となるコードです。
ゲームごとにニーズは異なります。構造、レイアウト、UI をプロジェクトに合わせて調整してください。
// Phaser 3.80+ — overlay scene that handles all LSDE block types
import Phaser from 'phaser';
import { type DialogueEngine, LsdeUtils } from '@lsde/dialog-engine';
export class DialogueUI extends Phaser.Scene {
private pendingNext: (() => void) | null = null;
constructor() {
super({ key: 'DialogueUI' });
}
create(params: { engine: DialogueEngine }) {
const dialog = this.createDialogPanel();
const choices = this.createChoicePanel();
// player advances dialogue via keyboard or touch
this.input.keyboard?.addKey('SPACE').on('down', () => this.advance());
this.input.on('pointerdown', () => this.advance());
// ── DIALOG ────────────────────────────────────────────────
params.engine.onDialog(({ block, context, next }) => {
const { dialogueText, nativeProperties } = block;
const { character, resolveCharacterPort } = context;
const text = LsdeUtils.getLocalizedText(dialogueText);
character && resolveCharacterPort(character.uuid);
dialog.show(text, character?.name);
this.pendingNext = next;
// auto-advance after a delay (cinematics, tutorials)
let timer: Phaser.Time.TimerEvent | null = null;
if (nativeProperties?.timeout) {
timer = this.time.delayedCall(nativeProperties.timeout * 1000, () => this.advance());
}
return () => { dialog.hide(); this.pendingNext = null; timer?.destroy(); };
});
// ── CHOICE ────────────────────────────────────────────────
params.engine.onChoice(({ block, context, next }) => {
const { nativeProperties } = block;
const { choices: items, selectChoice } = context;
const visible = items.filter(c => c.visible !== false);
const buttons = choices.show(visible, (uuid) => {
selectChoice(uuid);
next();
});
let timer: Phaser.Time.TimerEvent | null = null;
if (nativeProperties?.timeout) {
timer = this.time.delayedCall(nativeProperties.timeout * 1000, () => next());
}
return () => { choices.hide(buttons); timer?.destroy(); };
});
// ── CONDITION ─────────────────────────────────────────────
params.engine.onCondition(({ block, context, next }) => {
// evaluate your game state — return true or false
const result = true; // ← replace with your game logic
context.resolve(result);
next();
});
// ── ACTION ────────────────────────────────────────────────
params.engine.onAction(({ block, context, next }) => {
// execute your game effects — set flags, play sounds, give items
// context.resolve() on success, context.reject(err) on failure
context.resolve();
next();
});
}
private advance() {
if (this.pendingNext) { this.pendingNext(); this.pendingNext = null; }
}
// ── UI factories ──────────────────────────────────────────────
private createDialogPanel() {
const bg = this.add
.graphics()
.fillStyle(0x000000, 0.8)
.fillRoundedRect(0, 0, 700, 150, 12);
const nameText = this.add.text(16, 12, '', {
fontSize: '16px', color: '#ffcc00', fontStyle: 'bold',
});
const bodyText = this.add.text(16, 38, '', {
fontSize: '14px', color: '#ffffff', wordWrap: { width: 668 },
});
const container = this.add
.container(50, 430, [bg, nameText, bodyText])
.setScrollFactor(0)
.setDepth(1000)
.setVisible(false);
return {
show: (text?: string, speaker?: string) => {
nameText.setText(speaker ?? '');
bodyText.setText(text ?? '');
container.setVisible(true);
},
hide: () => container.setVisible(false),
};
}
private createChoicePanel() {
return {
show: (
visible: { uuid: string; dialogueText?: Record<string, string>; label?: string }[],
onSelect: (uuid: string) => void,
) => {
return visible.map((choice, i) => {
const text = LsdeUtils.getLocalizedText(choice.dialogueText) ?? choice.label ?? '';
return this.add
.text(80, 440 + i * 40, text, { fontSize: '15px', color: '#ffffff' })
.setScrollFactor(0)
.setDepth(1001)
.setInteractive({ useHandCursor: true })
.on('pointerdown', () => onSelect(choice.uuid));
});
},
hide: (buttons: Phaser.GameObjects.Text[]) => buttons.forEach(b => b.destroy()),
};
}
}
// start a dialogue scene from your game:
// this.scene.launch('DialogueUI', { engine });
// engine.scene(sceneId).start();// Unity 2021+ — MonoBehaviour that handles all LSDE block types
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using LsdeDialogEngine;
public class DialogueUI : MonoBehaviour
{
[Header("Dialog UI — assign in Inspector")]
[SerializeField] private GameObject dialogPanel;
[SerializeField] private TMP_Text speakerName;
[SerializeField] private TMP_Text dialogText;
[Header("Choice UI")]
[SerializeField] private Transform choiceContainer;
[SerializeField] private GameObject choiceButtonPrefab;
private DialogueEngine _engine;
private System.Action _pendingNext;
/// <summary>
/// Call once to wire all LSDE handlers to this UI.
/// The engine must be initialized before calling this method.
/// </summary>
public void Register(DialogueEngine engine)
{
_engine = engine;
// ── DIALOG ────────────────────────────────────────────
engine.OnDialog(args => {
var (_, block, context, next) = args;
var text = LsdeUtils.GetLocalizedText(block.DialogueText);
var ch = context.Character;
if (ch != null) context.ResolveCharacterPort(ch.Uuid);
speakerName.text = ch?.Name ?? "";
dialogText.text = text ?? "";
dialogPanel.SetActive(true);
_pendingNext = next;
return () => { dialogPanel.SetActive(false); _pendingNext = null; };
});
// ── CHOICE ────────────────────────────────────────────
engine.OnChoice(args => {
var (_, block, context, next) = args;
var visible = context.Choices
.Where(c => c.Visible != false).ToList();
foreach (var choice in visible)
{
var btn = Instantiate(choiceButtonPrefab, choiceContainer);
btn.GetComponentInChildren<TMP_Text>().text =
LsdeUtils.GetLocalizedText(choice.DialogueText) ?? choice.Label ?? "";
var uuid = choice.Uuid;
btn.GetComponent<Button>().onClick.AddListener(() => {
context.SelectChoice(uuid);
next();
});
}
return () => {
foreach (Transform child in choiceContainer)
Destroy(child.gameObject);
};
});
// ── CONDITION ─────────────────────────────────────────
engine.OnCondition(args => {
var (_, block, context, next) = args;
// evaluate your game state — return true or false
var result = true; // ← replace with your game logic
context.Resolve(result);
next();
return null;
});
// ── ACTION ────────────────────────────────────────────
engine.OnAction(args => {
var (_, block, context, next) = args;
// execute your game effects — set flags, play sounds, give items
// context.Resolve() on success, context.Reject(err) on failure
context.Resolve();
next();
return null;
});
}
/// <summary>Wire this to your Continue button's OnClick in the Inspector.</summary>
public void OnContinueClick()
{
if (_pendingNext == null) return;
_pendingNext();
_pendingNext = null;
}
/// <summary>Start a dialogue scene from your game code.</summary>
public void StartScene(string sceneId)
{
var handle = _engine.Scene(sceneId);
handle.Start();
}
}// UE5 — GameInstanceSubsystem that handles all LSDE block types
// Engine is a lsde::DialogueEngine member initialized in Initialize()
#include "lsde/engine.h"
#include "lsde/utils.h"
#include "DialogueSubsystem.h"
#include "DialogueWidget.h"
#include "ChoiceWidget.h"
void UDialogueSubsystem::RegisterHandlers()
{
// ── DIALOG ────────────────────────────────────────────────
Engine.onDialog([this](auto* scene, const auto* block, auto* ctx, auto next) -> lsde::CleanupFn {
auto localized = lsde::LsdeUtils::GetLocalizedText(block->dialogueText);
auto* ch = ctx->character();
if (ch) ctx->resolveCharacterPort(ch->uuid);
DialogWidget->SetDialogue(
FString(UTF8_TO_TCHAR(localized.value_or("").c_str())),
FString(UTF8_TO_TCHAR(ch ? ch->name.c_str() : "")));
DialogWidget->SetVisibility(ESlateVisibility::Visible);
PendingNext = std::move(next);
return [this]() {
DialogWidget->SetVisibility(ESlateVisibility::Collapsed);
PendingNext = nullptr;
};
});
// ── CHOICE ────────────────────────────────────────────────
// PendingChoiceCtx is valid only during the current block — the engine
// invalidates it once the cleanup runs or the scene advances.
Engine.onChoice([this](auto* scene, const auto* block, auto* ctx, auto next) -> lsde::CleanupFn {
const auto& choices = ctx->choices();
for (const auto& c : choices) {
if (!c.visible.has_value() || c.visible.value()) {
auto text = lsde::LsdeUtils::GetLocalizedText(c.dialogueText);
ChoiceWidget->AddOption(
FString(UTF8_TO_TCHAR(c.uuid.c_str())),
FString(UTF8_TO_TCHAR(text.value_or("").c_str())));
}
}
PendingChoiceCtx = ctx;
PendingChoiceNext = std::move(next);
return [this]() {
ChoiceWidget->ClearOptions();
PendingChoiceCtx = nullptr;
};
});
// ── CONDITION ─────────────────────────────────────────────
Engine.onCondition([](auto*, const auto* block, auto* ctx, auto next) -> lsde::CleanupFn {
// evaluate your game state — return true or false
bool result = true; // ← replace with your game logic
ctx->resolve(result);
next();
return {};
});
// ── ACTION ────────────────────────────────────────────────
Engine.onAction([](auto*, const auto* block, auto* ctx, auto next) -> lsde::CleanupFn {
// execute your game effects — set flags, play sounds, give items
// ctx->resolve() on success, ctx->reject(err) on failure
ctx->resolve();
next();
return {};
});
}
// UFUNCTION(BlueprintCallable) — call from your Continue button
void UDialogueSubsystem::AdvanceDialogue()
{
if (PendingNext) { PendingNext(); PendingNext = nullptr; }
}
// UFUNCTION(BlueprintCallable) — call from your choice button delegate
void UDialogueSubsystem::OnChoiceSelected(const FString& Uuid)
{
if (PendingChoiceCtx) {
PendingChoiceCtx->selectChoice(TCHAR_TO_UTF8(*Uuid));
if (PendingChoiceNext) { PendingChoiceNext(); PendingChoiceNext = nullptr; }
PendingChoiceCtx = nullptr;
}
}
// UFUNCTION(BlueprintCallable) — start a dialogue scene
void UDialogueSubsystem::StartScene(const FString& SceneId)
{
auto handle = Engine.scene(TCHAR_TO_UTF8(*SceneId));
handle->start();
}# Godot 4.3+ — autoload node that handles all LSDE block types
# Register as autoload in Project > Settings > Autoload
extends Node
@onready var dialogue_panel: PanelContainer = %DialoguePanel
@onready var speaker_label: Label = %SpeakerLabel
@onready var dialogue_label: RichTextLabel = %DialogueLabel
@onready var choice_container: VBoxContainer = %ChoiceContainer
var _engine: LsdeDialogueEngine
var _pending_next: Callable
func _ready() -> void:
var json = JSON.parse_string(
FileAccess.open("res://data/blueprint.json", FileAccess.READ).get_as_text())
_engine = LsdeDialogueEngine.new()
_engine.init({"data": json})
_engine.set_locale("en")
_register_handlers()
func _register_handlers() -> void:
# ── DIALOG ────────────────────────────────────────────────
_engine.on_dialog(func(args):
var block = args["block"]
var ctx = args["context"]
var next_fn = args["next"]
var ch = ctx.character
var text = LsdeUtils.get_localized_text(block.get("dialogueText"))
if ch:
ctx.resolve_character_port(ch.get("uuid", ""))
speaker_label.text = ch.get("name", "") if ch else ""
dialogue_label.text = text if text else ""
dialogue_panel.visible = true
_pending_next = next_fn
return func():
dialogue_panel.visible = false
_pending_next = Callable()
)
# ── CHOICE ────────────────────────────────────────────────
_engine.on_choice(func(args):
var ctx = args["context"]
var next_fn = args["next"]
var choices = ctx.choices
var visible: Array = []
for c in choices:
if c.get("visible") != false:
visible.append(c)
for c in visible:
var btn = Button.new()
var label = LsdeUtils.get_localized_text(c.get("dialogueText"))
btn.text = label if label else c.get("label", "")
btn.pressed.connect(func():
ctx.select_choice(c["uuid"])
next_fn.call()
)
choice_container.add_child(btn)
return func():
for child in choice_container.get_children():
child.queue_free()
)
# ── CONDITION ─────────────────────────────────────────────
_engine.on_condition(func(args):
var ctx = args["context"]
var next_fn = args["next"]
# evaluate your game state — return true or false
var result = true # ← replace with your game logic
ctx.resolve(result)
next_fn.call()
)
# ── ACTION ────────────────────────────────────────────────
_engine.on_action(func(args):
var ctx = args["context"]
var next_fn = args["next"]
# execute your game effects — set flags, play sounds, give items
# ctx.resolve() on success, ctx.reject(err) on failure
ctx.resolve()
next_fn.call()
)
## Player input — advance dialogue on ui_accept (Space, Enter, gamepad A)
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept") and _pending_next.is_valid():
_pending_next.call()
_pending_next = Callable()
get_viewport().set_input_as_handled()
## Start a dialogue scene from your game code
func start_scene(scene_id: String) -> void:
var handle = _engine.scene(scene_id)
handle.start()4つの Handler
各 handler は block データと next() コールバックを受け取ります。開発者がエンジン内でデータを処理し、block の処理が完了したら next() を呼び出します。その呼び出しのタイミングはゲーム側に委ねられています。
Dialog — テキスト、キャラクター、ネイティブプロパティ。UI にダイアログを表示し、プレイヤーの入力またはディレイを待ってから
next()を呼び出します。engine が次の block に移る際に UI を非表示にするクリーンアップ関数を返します。Choice —
choiceFilterが設定されている場合、visibleタグ付きの選択肢リスト。対応する UI 要素を作成します — ボタン、リスト、ラジアルメニュー。プレイヤーが選択したら、selectChoice(uuid)で分岐先を engine に伝え、next()でフローを進めます。Condition — block に定義された条件。ゲームロジックで評価します — フラグ、クエスト、インベントリのチェック。
context.resolve(true)はポート 0 へ、context.resolve(false)はポート 1 へフローを送ります。Action — block に定義されたアクション。エンジンで実行します — サウンド再生、アイテム付与、シネマティックのトリガー。
context.resolve()は成功を確認、context.reject(err)は失敗を通知します。
Tips
next()はリモコンです。 高速ダイアログのために即座に呼び出すか、アニメーションが完了するまで保持します。engine は待機します — 時間の概念を持ちません。- クリーンアップ関数が後片付けします。 どの handler からでも関数を返せば、engine が次の block に移る時に呼び出します。UI の非表示、オーディオの停止、ノードの解放に最適です。
onBeforeBlockが delay を処理します。 engine はnativeProperties.delayを強制しません —onBeforeBlockがそれを読み取り、タイマー後にresolve()を呼び出します。完全な制御権があります。- async track は並列フローです。 カットシーンでダイアログとカメラ移動を同時に行う場合、エディタで
isAsyncマークされた block は独立した track で実行されます。
