コンテンツにスキップ

ダイアログ

ダイアログについて

吉里吉里Z は、JSON / Dictionary で記述したレイアウトからボタン・チェックボックス・テキスト入力などを含むダイアログを表示する機構を内蔵しています。TJS からは ElementsDialog クラスで利用します ( 旧名 Dialog。ゲーム側スクリプトのクラス名と衝突しやすい汎用名だったため改名されました )。

このダイアログは、Elements ( ThorVG ベースの C++ GUI ライブラリ ) を組み込んで実現されています。プラットフォーム依存のネイティブダイアログ ( Win32 の WIN32Dialog など ) とは異なり、SDL3 ビルドと Windows ネイティブ ( D3D11 ) ビルドの両方で同じ JSON 定義から同じ見た目のダイアログを表示できるのが特徴です。JSON / JSONC ( コメント付き JSON ) で記述したレイアウトを渡すだけで、レイアウト計算・描画・入力処理はエンジン側が行います。

Windows 標準の GUI に依存しないクロスプラットフォーム UI を目的とした機構のため、SDL3 ビルドでの利用を主眼に設計されていますが、WINVER ( Windows ネイティブ ) ビルドでも同じように動作します。

表示モード

ダイアログには、1 画面だけを表示する「単発ダイアログ」と、複数画面の遷移を含む「フロー」があります。それぞれにモーダル ( 閉じるまで TJS の処理をブロックする ) と非モーダル ( ゲーム画面に重ねて表示しメインループを止めない ) の別があります。

単発ダイアログ

表示モード TJS API 挙動
非モーダル ( オーバーレイ ) ElementsDialog.showJson / showFile 既存ゲーム画面の上にダイアログを描き、メインループを止めません。値の変化・ボタンのクリックは onAction で逐次通知されます。close で終了します。
ブロッキングモーダル ( 独立ウィンドウ ) ElementsDialog.showModalJson / showModalFile ( title / w / h を渡す ) 新しいネイティブウィンドウを開き、閉じるまでブロッキングします。閉じると %[ action, values ] 形式の Dictionary を返します。
ブロッキングモーダル ( オーバーレイ ) ElementsDialog.showModalJson / showModalFile ( JSON 1 引数のみ ) 独立ウィンドウを作らずに既存ゲーム画面に重ねて表示し、閉じるまでブロッキングします。戻り値は独立ウィンドウ版と同じです。
ホストレイヤ描画 ( パネル ) ElementsPanel ( new ElementsPanel(layer) + showFile 等 ) オーバーレイではなく指定した Layer のビットマップへ描画します。z 順・トランジション・スクリーンショットへの写り込み・入力の帰属がレイヤの仕組みに従うので、HUD やゲーム画面の一部としての UI に使います。イベント / 変数 API は ElementsDialog と同形です。

showModalJson / showModalFile の戻り値は次の形式です。

%[
    action: <閉じた button の id (Esc / × は "")>,
    values: %[ <id>: <値>, ... ]   // state widget の最終値マップ
]

モーダル中も onAction は発火しますが、ダイアログを閉じるのは JSON 側で "close_on_click": true を指定したボタン ( および Esc / × による中断 ) だけです。

複数画面フロー

複数の画面を順に切り替えていく「フロー」も定義できます。マニフェスト ( または Dictionary ) で複数画面と画面間の遷移を宣言します。

表示モード TJS API 挙動
ブロッキング ( オーバーレイ ) ElementsDialog.showFlow / showFlowScreens 複数画面の遷移をオーバーレイで実行します。フロー終了まで TJS をブロックし、最後に閉じた画面の %[ action, values ] を返します。
非モーダル ( 常駐 ) ElementsDialog.startFlow / startFlowScreens 同じフロー定義を非ブロッキングで開始します ( ゲーム画面に常駐 )。close で閉じ、teardown ( 後始末 ) の完了は active で判別します。

画面が切り替わるタイミングで onScreen / onScreenLeave、widget の操作で onAction が発火します。

画面切替エフェクト ( fade / universal )

画面 JSON の transitions エントリを object 形式にすると、画面切替時の遷移エフェクトを宣言できます。CPU 合成のため、SDL / WINVER / GL のすべての DrawDevice で同一に動作します。

"transitions": {
    "next": { "target": "s2", "effect": "fade", "duration": 300 },
    "back": { "target": "<back>", "effect": "universal",
              "rule": "rule.png", "vague": 64, "duration": 500 }
}
キー 意味
effect "fade" = クロスフェード / "universal" = rule 画像によるユニバーサルトランジション。未対応名は警告ログ + 即切替
duration 所要時間 ms。省略 / 0 で 200ms
rule universal の rule 画像 ( グレースケール。値が小さい画素ほど早く次画面へ切り替わる )。解決順 = 遷移を宣言した画面からの相対 → Storages パス → autopath 検索
vague 境界ぼかし幅 ( rule 値スケール 0〜255、既定 64 )

退場 ( exit ) 演出との協調

要素の "animate""on": "exit" を付けると、画面が閉じる / 遷移するときに退場演出を再生してから遷移します。close_on_click / Esc などの画面内トリガに加えて、TJS からの close でも発火します ( 演出完了後に閉じ、フロー実行中は transitions を解決せずフローごと終了します )。

GPU ( OpenGL 描画 ) 側で同等のトランジションを行いたい場合は、3D グラフィックシステム の Canvas トランジション描画を参照してください。

レイアウト密度の指定 ( gap / style )

既定は fit-to-content ( 内容に合わせたサイズ ) + 密着積みのため、明示指定を省略すると詰まった見た目になります。spacer を並べる代わりに、以下で密度をまとめて指定できます。

{
    "style": { "font_scale": 1.25, "row_height": 44, "tile_gap": 8, "padding": 24 },
    "content": { "type": "vtile", "gap": 12, "children": [ ... ] }
}
  • vtile / htile"gap" — 子要素間の隙間 px ( spacer 自動挿入と等価 )
  • top-level "style"font_scale ( 既定フォント倍率 ) / tile_gap ( gap 未指定タイルの既定 ) / row_height ( button 系 / input_box の既定最小高 ) / padding ( content 全体の外側余白 ) を、未指定値の既定として適用します

いずれも省略すれば従来と完全に一致します。

矩形テキスト ( text_area )

字幕やセリフ窓のように「決まった矩形に本文を流し込み、文字送りする」用途には text_area を使います。

{ "type": "text_area",
  "text": "…本文…",
  "size": 36,                    // px
  "font": "Noto Sans JP",        // 省略時はテーマ既定
  "align": "left",               // left / center / right
  "line_spacing": 12,            // 行間追加 px
  "count_var": "sub_count" }     // 文字送り ( -1 = 全部 )
  • 折り返し・行頭行末禁則・文字送りの単位が Layer.drawShapedTextArea同じロジックです。同じ本文・同じ幅・同じフォント / サイズなら改行位置が一致します ( レイヤ描画と Elements で字幕を出し分けても行組みがずれません )。
  • "count_var" に変数名を与えると、ホストが setVar で数値を書くだけで文字送りが進みます。折り返しは全文で確定してから count を適用するので、送っている途中でリフローしません。数える単位は Layer.shapedTextCount と同じクラスタ ( 合字・結合文字・絵文字 ZWJ シーケンスで 1 ) です。
  • 本文の差し替えは "text_var" / "text_id"、リストからの指定番号表示は "text_list_id" + "index_var" で、いずれも label と同じ規約です。
  • 行ごとに固定の "index" と、行で共有する "index_offset_var" ( 先頭位置 ) を組み合わせると「N 行の窓」になります。ホストは setVar先頭位置の変数 1 個を動かすだけで一覧が送れるので、行ごとに変数を用意する必要がありません ( 一覧データ自体の差し替えは "text_list_var" )。
  • 従来からある text_box は互換のためそのまま残っています ( 素朴なワード折り返し・禁則なし )。既存画面の改行位置は変わりません。

⚠ 絶対座標で置く ( floating"at" を使う ) 場合は、top-level に "size": [w, h] を明示してください。省略するとダイアログが内容の最小サイズまで縮み、絶対座標がその外に出て何も表示されません。

ドラッグ操作 ( drag_at_var / onDrag )

widget に "drag_at_var" を書くと、ドラッグ中の位置が "x,y" 形式でその変数へ書き込まれます。canvas の子の "at_var" に同じ変数を挿せば、TJS を介さずに絵がドラッグへ追従します ( エンジン内で完結するのでイベント配送の遅延を受けません )。可動域は "drag_bounds": [x, y, w, h] で制限できます。

「どこで離したか」のような判断を TJS 側で行いたい場合は、その widget に "drag_events": true を指定して onDrag を実装します。

class DragDialog extends ElementsDialog {
    // 明示コンストラクタで super を呼ぶこと ( 省略するとイベントが届かない )
    function DragDialog() { super.ElementsDialog(); }
    function onDrag(e) {
        // e.id / e.phase ( "begin" | "move" | "end" ) / e.x / e.y
        // e.dx / e.dy ( 前回からの差分 ) / e.startX / e.startY / e.modifiers
        if (e.phase == "end") {
            // 離した位置で当たり判定を取る、といった判断はここで
        }
    }
}

座標は画面 JSON に書いた座標系です。溜まった "move" は最新の 1 件へ畳まれます ( "begin" / "end" は畳まれません )。見た目を追従させるだけなら onDrag は不要で、drag_at_var + at_var の組み合わせで済みます。

一覧とスクロールバー ( list / atlas_scrollbar )

行が並ぶ画面は "type": "list" で組みます。1 行分のテンプレートを行数ぶん複製する仕組みで、行の位置・当たり判定・hover・選択・「データが足りない行の後始末」までウィジェット側が持ちます。TJS 側はデータを流し込むだけです。

var layout = %[
  "content" => %[
    "type" => "list", "id" => "files",
    "rows" => 6, "row_size" => [676, 36], "pitch" => [0, 40],
    "index_offset_var" => "top", "count_var" => "n",
    "select_var" => "sel", "row_hover_var" => "rhov#index",
    "row" => %[ "type" => "label", "id" => "row#index",
                "text_list_var" => "items" ]
  ]
];
dlg.showDict(layout);
dlg.setVar("items", "file 0\nfile 1\nfile 2");   // 一覧データ
dlg.setVar("n", "3");                             // 総件数
  • 文字列の中の #index が行番号へ置換されます ( "id": "row#index"row0 / row1 … )。text_list_var を持つ要素には行番号と先頭位置が自動で挿さるので、テンプレートに 1 行書くだけで一覧になります
  • 行をクリックすると onActionpayload = データ index で発火します ( 行の中にボタンがあればそちらが優先 )
  • count_var を渡すと、データが無い行は描画も当たり判定も消えます
  • hover / 選択の色は、行ごとのフラグ変数 ( row_hover_var / row_select_var"visible_var" で受ける ) か、onVarhover_var / select_var を拾ってホスト側のレイヤを差し替える形のどちらでも組めます

スクロールバーは "type": "atlas_scrollbar"同じ index_offset_var を挿すだけです。つまみの長さは「見えている行数 ÷ 総件数」に比例し、つまみのドラッグ・溝クリックでのページ送り・ホイールまで内蔵しています ( 本文が scroller に載っている画面なら scrollerpos_var で足ります )。

変数の読み書きと変化通知 ( setVar / getVar / onVar )

画面 JSON の変数は 1 本の store にぶら下がっていて、setVar で書けるだけでなく getVar で読み出せます。読めるのは自分が書いた値だけではありません — "vars_on_hover" / "vars_on_focus"、slider の "value_var""drag_at_var"、一覧の "index_offset_var" のように画面側が書いた値も同じ storeなので、そのまま読めます。

変化した時点で知りたい場合は onVar を実装します。

class ListDialog extends ElementsDialog {
    // 明示コンストラクタで super を呼ぶこと ( 省略するとイベントが届かない )
    function ListDialog() { super.ElementsDialog(); }
    function onVar(name, value) {
        if (name == "row_hover") {
            // カーソルが乗っている行が変わった → ホスト側のレイヤを差し替える
            highlightRow(+value);
        }
    }
}

これで「絵はホスト側のレイヤ、当たり判定だけダイアログ」という構成が組めます。1 枚絵が大きすぎて atlas に積めない一覧画面などで、ダイアログには透明なボタンだけを並べて hover を受け取り、表示はゲーム側のレイヤで行う、という分担です。

  • 通知は 1 フレーム遅延し、同じ変数の連続変化は最新の 1 件へ畳まれます。「いまの値」が要るときは getVar を読みます
  • onVar を実装したダイアログだけが観測対象になります ( 実装していなければコストはかかりません )。受け取る変数を絞りたいときは watchVars に名前を並べます — hover 連動変数やドラッグ位置は毎フレーム書き換わるためです
  • 画面にどんな変数があるかは listVars で一覧できます ( 変数名・現在値・参照している widget の id と種類 )。デバッグパネルや画面 JSON の検証に使えます

画面をまたいで値を保つ ( shared_vars )

変数 store は画面ごとに作り直されるので、設定をタブで渡り歩くと「さっき動かしたスライダー」が既定値へ戻ります。引き継ぎたい値は画面 JSON の top-level に宣言します。

"shared_vars": ["cfg_*", "ui_lang"]

一致した変数はセッション共有ストアと双方向になり、画面を組むときは共有側の値で初期化され ( 画面の "vars" 既定より共有側が優先 )、以後変わるたび共有側へ書き戻されます。パターンは完全一致か末尾 * の前方一致です。どの値を持ち回るかは画面側が決めるので、ホスト側の実装は要りません

ゲームのセーブデータへ落とす / ロード後に流し込むのはホストの仕事なので、その口だけ用意してあります。

// セーブ
var cfg = ElementsDialog.getSharedVars();   // %[ "cfg_bgm" => "70", ... ]
// ロード
ElementsDialog.clearSharedVars();
foreach_dict_of(cfg, function(k, v) { ElementsDialog.setSharedVar(k, v); });

値は文字列で出入りします。共有側へ行くのは「変化として書かれた」値だけで、ウィジェットの初期値 ( "initial" / "value" ) は出ません — 誰も触っていない項目が最初に開いた画面の既定で固定されるのを避けるためです。

値と絵を変数で差し替える ( value_var / image_var / 差し替え可能アトラス )

  • 2 値トグル ( checkbox / toggle_button / slide_switch ) は "value_var" で変数 store と双方向になります ( "" / "0" / "false" = off )。クリックで書き戻り、setVar で状態が追従します ( 追従では onAction は発火しません )。設定画面の ON/OFF をホストのコールバック無しで扱えます。
  • image ウィジェット"image_var" で絵そのものを差し替えられます。変数の値がそのまま画像パス ( "resources/x.png" / "mem://thumb_3" / 空 = 無描画 ) になるので、セーブ一覧のページ送りでサムネイルが変わる、CG ビュワーの絵を送る、といった画面が再構築なしで書けます。
  • アトラスごと入れ替えたい場合は、画面 JSON で "atlases": { "cg": { "path": ..., "swappable": true } } と宣言し、setAtlasImage で差し替えます。ウィジェットは作り直さないのでレイアウトもフォーカスも保たれます。差し替え先は同じ矩形割りであること ( frames / rect は変わらないので、絵の位置がずれると別の絵が出ます )。差し替えられるアトラス名は swappableAtlases で確認できます。
  • アトラスは画面を閉じても解放されません。 デコード済みの絵は「パス + 倍率」をキーにキャッシュされ、画面を切り替えても抱えたままになります ( 長時間プレイでヒープが断片化したあと大きな連続領域が取れずデコードに失敗し、絵の無い画面が組まれるのを避けるため )。抱え込み量は atlasCacheStats で読め、場面の切れ目 ( タイトル → 本編など ) で trimAtlasCache を呼べば落とせます。⚠ 表示中の画面が使っているアトラスは参照が残るので落ちません — 画面を閉じた後に呼んでください。予算そのものを変えるなら atlasCacheBudget です。

非モーダルの複数同時表示とフォーカス

非モーダル ( オーバーレイ ) パネルの配置は画面 JSON の top-level "align" / "margin" で指定し、配置と拡縮の基準領域は top-level "base" で選べます — "window" ( 既定、ウィンドウ全面基準 ) / "content" ( ゲーム画像の表示領域基準。字幕窓のようにゲーム画像へ追従させたい場合 )。拡縮はゲームの基準面に対するウィンドウ ( または表示領域 ) の比率に追従するため、フルスクリーン等ではゲームと同率で拡大されます。ゲーム画面と別解像度で UI を author しているタイトル ( ゲーム画面 640x400 / UI 1920x1080 等 ) では、ElementsDialog.baseSize に author 基準面のサイズを設定すると拡縮の分母がそちらになり、ゲーム側の基準面サイズの変更にも巻き込まれません。

非モーダルダイアログ ( showJson / startFlow 系 ) は z-order 付きのインスタンスリストとして管理され、複数同時に表示できます。マウスは最前面からヒットテストし、キーボード / ゲームパッドはフォーカスを保持しているインスタンス ( z-order 末尾優先 ) に届きます。モーダルダイアログを重ねた場合、下のインスタンスは描画は維持されたまま入力だけがブロックされます。

非モーダル開始系 ( startFlow / startFlowScreens ) には grabFocus 引数があり、偽を指定すると「フォーカスを取らない常駐 HUD」として動きます。常駐 UI がゲームのホットキーまで食ってしまうのを防ぎたい場合に利用します。

非モーダルでは、Elements 側で実際に処理されたキーだけを消費し、未処理キーはゲームへ通過させる ( handled pass-through ) ため、メニューを開いたままゲーム本体のホットキーで別のダイアログを開く、といった共存も可能です。

入力の配送優先順位とホストホットキー

入力は次の優先順位で配送されます。

  1. 最上位ホットキー ( System.registerHotKey ) — イベントポンプの入口。モーダル表示中でも効く唯一の層です ( フックは SDL3 系ビルドのみ配線されており、WINVER ビルドでは発火しません )
  2. モーダルダイアログ — 全入力を独占 ( 下にもゲームにも通しません )
  3. ホストホットキー ( registerHotKey ) — 登録キーはダイアログへ渡らず Window.onKeyDown 等へ直行
  4. フォーカスを持つ非モーダルパネル — キー / パッドを受け、未処理分のみ素通し
  5. ゲーム / レイヤ — 未消費の落ち先

単発表示系 ( showJson / showFile / showDict ) は第 3 引数 modal で「非モーダル + フォーカスあり」( showJson(json, true, false) ) を指定できます。slider や picker を含む操作パネルはこの形で出すと、パッドの十字 / A ボタンやキーボードでウィジェットを操作しつつ、パネルが使わないキーはゲームへ流れます。その上で ESC ( シーン復帰 ) や PageUp/Down ( 画面切替 ) のような「必ずホストが受けたいキー」を registerHotKey で確保するのが定石です ( 実例: data/demolib/demo_common.tjs の DemoShell )。

  • ホットキーはテキスト入力ウィジェットにキャレットがある間は既定で抑止されます ( duringTextInput = true で入力中も有効化 )
  • モーダル表示中はホットキーも無効です ( 確認ダイアログの ESC = cancel を奪いません )
  • マウスボタン ( VK_RBUTTON 等 ) も登録でき、全画面透過 HUD が右クリックを拾って閉じてしまう問題の回避にも使えます
  • 最上位ホットキー側のコールバックで「モーダルが出ている間は何もしない」と分岐したい場合は ElementsDialog.modalActive を見ます ( モーダルインスタンスの有無。フォーカスを取らない常駐オーバレイは含みません )

入力バインドと named action

"input" ブロックの "bindings" で、キー / パッド / マウス / ホイールに名前付きアクション ( named action ) を割り当てられます。"accept" / "cancel" / "nav_up" … のような組込名はダイアログ内で処理されるため TJS へは届きません。組込以外の任意の名前を書いた場合だけ、その入力がホストへ通知されます。

"input": { "bindings": [
    { "wheel": "up",   "action": "prev_page" },   // 任意名 = ホストへ通知
    { "key": "escape", "action": "cancel" } ] }   // 組込名 = ダイアログ内で処理

通知は onAction で受けますが、id"<action>" 固定で、付けた名前は payload に入ります

function onAction(id, payload) {
    if (id == "<action>") {
        switch (payload) {
        case "prev_page": /* ホイール上で前ページ */ break;
        }
    }
}

id に自分で付けた名前が来るものと思って書くと、その入力だけが黙って無反応になり原因に気づきにくいので注意してください。

ミニマルな利用例

Dialog を継承したクラスで onAction を実装し、JSON レイアウトを渡して表示します。

class TestDialog extends ElementsDialog {
    // 明示コンストラクタで super を呼ぶこと ( 省略するとイベントが届かない )
    function TestDialog() { super.ElementsDialog(); }
    function onAction(id, payload) {
        switch (id) {
        case "ok":     System.inform("OK!");  close(); break;
        case "cancel": close(); break;
        }
    }
}

var json = @"
{
    // hspacer で 560 幅を確保 (JSONC コメント可)
    'size': [ 560, 200 ],
    'content': {
        'type': 'vtile',
        'children': [
            { 'type': 'hspacer', 'size': 560 },
            { 'type': 'label', 'text': 'Hello, Elements!' },
            { 'type': 'htile', 'children': [
                { 'type': 'button', 'id': 'ok',     'text': 'OK',     'close_on_click': true },
                { 'type': 'button', 'id': 'cancel', 'text': 'Cancel', 'close_on_click': true }
            ]}
        ]
    }
}";

var dlg = new TestDialog();
dlg.showJson(json);             // 非モーダル
// var r = dlg.showModalJson(json, "Title", 560, 200);   // 独立ウィンドウ
// var r = dlg.showModalJson(json);                       // ゲーム画面オーバーレイ

ボタン・入力欄などの widget を JSON でどう記述するかの一覧は、Dialog クラスリファレンスを参照してください。

フォントの登録

ダイアログ描画に使うフォントは krkrz の Storages ( XP3 含む ) を経由して読み込まれます。スクリプトから明示的に登録するには ElementsDialog.registerFont / ElementsDialog.registerFontDir を、既定フォントファミリの確認・上書きには ElementsDialog.defaultFontFamily プロパティを使います。

エンジン側でも、起動時にリソースパス下の .ttf / .otf を自動スキャンして登録しています ( ファイル名から family / weight / slant / stretch を推定 )。

チェックボックスの ✓ や selection_menu の ▼ などのアイコングリフは、本文フォントではなくアイコンフォント elements_basic.ttf ( resource/ に同梱 ) で描画されます。エンジンが自動登録するため通常は意識不要ですが、リソースを差し替える構成でこのフォントを外すと「枠は出るが ✓ が出ない」状態になります。

可変フォント ( ウェイト指定つき登録 )

registerFont のパスに "#tag=val" サフィックスを付けると、可変フォントの軸インスタンスを別名として登録できます。画面 JSON 側は "font": "MyFont-Medium" のような名前だけで、実体は 1 つの可変フォントに集約できます。widget の "font" に直接 "MyFont#wght=700" と書く指定も同じ表記です。

ElementsDialog.registerFont("MyFont", "fonts/MyFont-VF.ttf");                   // 素の VF ( 無指定 = wght=400 )
ElementsDialog.registerFont("MyFont-Medium", "fonts/MyFont-VF.ttf#wght=500");   // 別名 = 軸インスタンス

言語連動フォント置換 ( 多言語 UI )

日本語 / 繁体字 / 簡体字のように文字体系ごとの別フォントを持つ UI では、ElementsDialog.fontLanguages に言語→ファミリの置換表を設定しておくと、ElementsDialog.language の切替に連動してフォント解決時にファミリが差し替わります ( 共有コードポイントの漢字を表示言語に合った地域字形で描画できます )。表は画面 JSON の top-level "font_languages" でも宣言でき、特定 widget だけ言語を固定したい場合は widget の "locale" を指定します。

ビルド構成

ダイアログ機能は KRKRZ_USE_ELEMENTS=ON ( デフォルト ) でビルドされたエンジンで利用できます。SDL3 ビルドと WINVER ( Windows ネイティブ / D3D11 ) ビルドの両方に対応します。KRKRZ_USE_ELEMENTS=OFF でビルドした場合は ElementsDialog / ElementsPanel クラスは登録されず、ダイアログ関連のコードはリンクから除外されて実行ファイルサイズが削減されます。

全体像と設計方針

層構造 ( elements / elements_modal / krkrz 本体 / ゲーム側 ) と、 「UI の処理は画面データ側で完結させ、ホストは繋ぐだけ」という方針の 説明は Elements UI 機構の全体像 にあります。 どの層を直せばよいか迷ったときはそちらを先に読んでください。

関連 API