コンテンツをゲームに組み込む
VNLEは文章、翻訳、作成した進行をエクスポートします。ゲーム側でデータを読み、会話ボックスを表示し、ゲームアクションを各システムへつなぎます。自分のスクリプトやコミュニティの連携機能を使えます。以下の例では、VNLE専用のハンドラーを必要とせずにデータを扱う方法を説明します。
ファイル、JSON およびエントリ ポイント
エクスポート → ファイルとJSONを理解する → 進行ルールを調べる
すべての例は「港の鍵」の実際のエクスポートを使います。長いIDはそのプロジェクト専用なので、自分のエクスポートのIDに置き換えてください。名前や翻訳文を参照キーとして使わないでください。
story.json · story-index.json · 編集できるサンプルプロジェクト
エンジンを選ぶ
各エンジンのページで読み込みと表示を説明し、以下の学習ガイドでは共通のデータの扱い方を紹介します。他のエンジンでも同じJSONの仕様を利用できますが、完成済みのプラグインを提供するという意味ではありません。
1 · JSON から 1 つのテキストを表示
まず「Welcome to the harbour. I am Mira.」を表示します。会話ノードは文章を直接持たず、textRefを参照します。テキストの表にはセグメントが入ります。このJavaScript例では、story.jsonを解析してstoryに格納済みとします。読み込みと表示は各エンジンのページを参照してください。
// story is the parsed story.json. This excerpt reads text-only messages.
const locale = 'en';
const textRef = 'fd7b61a3-8233-4ba7-8f77-b88b119a91bf';
const record = story.content.texts[textRef];
const translation = story.content.translations[locale]?.[textRef];
const message = translation ? translation.message : record.sourceMessage;
const text = message.segments.map(segment => {
if (segment.kind !== 'text') throw new Error('This example needs text-only segments');
return segment.text;
}).join('');
console.log(text);最初のテストでは固定のテキストIDを使います。空の翻訳と未翻訳は異なります。存在する有効な空の翻訳は、空のまま表示して構いません。
2 ・ 会話の開始、続行、終了
プレイヤーがミラに話しかけたら、ゲーム側でstory-index.jsonから対応する開始地点を選び、会話ボックスを開いて最初のノードを取得します。JSONのプロパティ順ではなく、参照をたどってください。
const entryRef = '5e6b2e12-1ac1-4c46-8221-1a051e87370a';
const entry = story.execution.entries[entryRef];
const node = story.execution.nodes[entry.nodeRef];
console.log(node.kind, node.text.textRef);
// Read this dialogue's text; wait for the player's Continue action.
// node.continuation describes the next step, not the next array element.Continueの後はcontinuationに従います。kind: nodeならnodeRefのノードへ進み、kind: endなら終了します。kindがendのノードも同じ動作です。ゲーム側で会話ボックスを閉じ、操作を戻してください。VNLEがアプリやレベルを自動終了することはありません。
{
"continuation": {
"kind": "node",
"nodeRef": "7c804744-b002-4136-9a7c-703fcf2d0880"
},
"flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
"id": "d789af84-d51d-4b97-b564-86a6756e2f2e",
"kind": "dialogue",
"speakerRef": "807b2957-7bab-4a28-83a4-1f3d1589bbc2",
"text": {
"bindings": {},
"textRef": "cbe1a637-991f-4857-81d1-6cc053d68927"
}
}この抜粋ではMaybe laterからEndへ進みます。一方、最初の挨拶はCall Substoryへ進みます。会話と終了だけに対応したスクリプトでは、Call/Returnを実装するまで、その地点で説明を表示して止めてください。
3 ・ 回答の提供
ミラはBuy the key (5 gold)とMaybe laterを提示します。optionOrderが表示順、optionsが各選択肢の内容を持ちます。各ボタンに選択肢IDを保持し、クリックされた回答のeffectsとcontinuationだけを適用します。
// Read this exact choice from the Harbour story. No condition on these two options.
const choice = story.execution.nodes['42edfff8-b8fe-46bf-ae0d-9e2347b6e726'];
for (const optionId of choice.optionOrder) {
const option = choice.options[optionId];
console.log(optionId, option.text.textRef, option.continuation);
}
// Each answer button must retain its optionId.
// On click: check availability, apply that option's effects once,
// then follow that option's continuation.option.conditionがあれば、その回答を表示できるか判定します。港のこの2つの回答には個別の条件はなく、所持金は次のbranchノードで判定します。回答が一つもなければwhenEmptyに従い、指定の進行先へ進むか、理由を示してエラーにします。
4 ・条件を確認し、値を変更
10ゴールドなら鍵を買えますが、2ゴールドでは買えません。条件はJSONに含まれ、ゲームが現在の所持金を提供します。この例はgteによる比較を扱います。他の演算子や入れ子の条件は進行ルールを参照してください。
// This excerpt evaluates the Harbour purchase check: Gold >= 5.
const branch = story.execution.nodes['c1c3c372-0a73-4cd0-851d-ede59d5709bb'];
const condition = branch.condition;
const gold = 10; // Supply the current value from your game's saved state.
const minimum = condition.right.value.value; // 5, from the exported typed literal.
const continuation = gold >= minimum ? branch.whenTrue : branch.whenFalse;
console.log(story.execution.nodes[continuation.nodeRef].kind);
// Repeat with gold = 2: the next node is a dialogue explaining the price.
// The full condition grammar is documented in Flow rules.条件と変更のための実際のJSONフィールド
{
"condition": {
"kind": "compare",
"left": {
"kind": "variable",
"variableRef": "a547610e-661f-4b8d-a25b-21d042f1a2d5"
},
"operator": "gte",
"right": {
"kind": "literal",
"value": {
"type": "integer",
"value": 5
}
}
},
"flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
"id": "c1c3c372-0a73-4cd0-851d-ede59d5709bb",
"kind": "branch",
"whenFalse": {
"kind": "node",
"nodeRef": "eaffd4b0-397d-40b7-abc6-404a0a801e0e"
},
"whenTrue": {
"kind": "node",
"nodeRef": "242b2fd4-8101-4a7f-9306-17b789508bdc"
}
}{
"continuation": {
"kind": "node",
"nodeRef": "500c28a1-ed85-4c36-9a38-bedacce1c6b7"
},
"effects": [
{
"kind": "subtract",
"value": {
"kind": "literal",
"value": {
"type": "integer",
"value": 5
}
},
"variableRef": "a547610e-661f-4b8d-a25b-21d042f1a2d5"
},
{
"kind": "set",
"value": {
"kind": "literal",
"value": {
"type": "boolean",
"value": true
}
},
"variableRef": "76407a53-5ce1-408e-a8c6-a31c10f69ca5"
}
],
"flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
"id": "242b2fd4-8101-4a7f-9306-17b789508bdc",
"kind": "action"
}Change Variableはeffectsを持つactionとして出力されます。ここではGoldから5を引き、QuestAcceptedをtrueにします。execution.variablesから初期化するのは新規ゲームの開始時だけです。セーブを読み込むときは保存値を復元してください。インベントリにも所持金がある場合は、基準となるデータを一つに決め、更新を連携させます。
5・言語変更
完全なエクスポートでは、翻訳はcontent.translationsに入っています。言語ごとに別のJSONを読む必要はありません。content.translations[locale][textRef].messageがあれば使い、なければcontent.texts[textRef].sourceMessageを使います。localeはゲーム設定から取得します。
言語を変えたら、現在の会話、話者名、回答を読み直します。現在位置、選択済みの回答、変数は保ちます。コマンドや変数変更を再実行しないでください。プレースホルダーは、そのセリフの表示時に取得した値を再利用します。
プレースホルダー:「残りは5ゴールドです」 · VNLEで翻訳を管理する
6 · 名前・セリフ・ポートレートを一つの会話欄に表示する
- 会話文:node.text.textRefから取得します。
- 話者:content.characters[node.speakerRef]を調べ、nameTextRefを翻訳します。speakerRefがなければ、話者のいない地の文として扱えます。
- 完全なエクスポートのポートレート:指定された感情の画像を優先し、なければdefaultPortraitRefを使います。story.jsonのあるフォルダーを基準にcontent.assets[assetRef].relativePathを読み込みます。JSONに入るのはパスで、PNGの画像データではありません。
- 回答:利用可能な選択肢IDごとにボタンを作ります。通常の会話にはContinueを表示します。
- Endでは会話欄全体を閉じ、回答ボタンを無効にし、必要に応じてプレイヤーの操作を戻します。
エンジン側の画像を使う場合やエクスポートに画像の対応がない場合は、ゲーム側でキャラクターIDと画像を対応付けます。名前や言語を変えてもIDは変わりません。感情の画像には感情IDも使います。
// The game owns this mapping. These are real Harbour character IDs.
const portraits = {
'807b2957-7bab-4a28-83a4-1f3d1589bbc2': 'assets/portraits/mira.png'
};
const node = story.execution.nodes['ffa7141b-97ed-44c1-a346-8bba7e26d94f'];
const file = portraits[node.speakerRef];
// Load file using your engine's image loader; show a placeholder if absent.
// Optional emotion variants: portraits[characterId][emotionId].
// Do not index by the translated character name.エンジンのページでは、テクスチャ、スプライト、アトラスを使った対応付けを紹介します。VNLEのエクスポートには言語別の画像選択はありません。画像がなければ前のポートレートを消し、必要に応じて代替画像を表示します。
より高度な進行を実装する
ジャンプ · サブストーリー呼び出し/復帰 · コマンド · セーブとエラーへの対処
物語の作成者が進行を組み立て、開発担当者が各ノードの動作を実装し、決めたコマンドをゲームへ接続します。その後の物語の変更は、エクスポートされた接続に従って反映されます。