VNLEWIKI物語をつくる。世界をつなぐ。
日本語
VNLEガイド

進行ルールとゲームアクション

← 学習ガイドとエンジン一覧

以下は、自分のスクリプトやコミュニティの連携機能でエクスポートを使うためのルールです。JSONに入っているのはデータであり、実行するソースコードではありません。エディターのレイアウト、選択状態、画面上の配置はゲームの動作を制御しません。

最初のステップの前に

format = vnle.story、containerVersion = 1を確認し、execution.contractVersionとrequiredCapabilitiesに対応しているか調べます。作例は1.1、core.v1、substory.call-return.v1を使用します。JSON Schemaによる構造の検証に加え、参照、型、対応コマンドの確認も必要です。未知のkindを黙って飛ばしてはいけません。

フィールド参照 · JSON スキーマ

各ノードの動作

kind動作
dialogue文章を表示してContinueを待ち、その後continuationへ進みます。
choiceoptionOrderの順に選択肢を並べ、option.conditionで絞り込みます。選ばれた回答のeffectsを一度だけ適用してcontinuationへ進みます。whenEmptyの指定にも従ってください。
branchconditionを評価し、whenTrueまたはwhenFalseへ進みます。
actioneffectsを順番に適用してからcontinuationへ進みます。
jump新しい戻り先を保存せず、指定された開始地点へ移ります。
callcontinuationを保存してから、指定された開始地点へ移ります。
return直近に保存したcontinuationから再開します。Callを経ていないReturnはエラーです。
command引数を評価してゲームアクションを要求します。結果を待ち、対応する結果の進行先へ進みます。
endサブストーリーの中でも、現在の会話全体を終了します。呼び出し元に戻りたい場合はReturnを使います。

条件と型付きの値

オペランドには、型付きの値を持つliteralか、variableRefを持つvariableを使います。compareはeq、ne、lt、lte、gt、gteに対応します。allはすべての子条件、anyは少なくとも一つの子条件の成立を求め、notは子条件を反転します。真偽値を文字列の"true"/"false"と混同しないでください。

変数の変更にはset、add、subtractを使います。add/subtractで扱う整数は−2147483648から2147483647です。変更は順番に評価しますが、無効な変更があれば一部だけを確定しないでください。会話のたびに保存済みの値を初期値defaultValueで上書きしてはいけません。

Jump · 購入後に倉庫へ進む

ジャンプ

鍵をすでに購入している場合は、Warehouseの開始地点へ直接ジャンプします。targetはflowRefとentryRefを持ち、execution.entries[entryRef].nodeRefから次のノードを取得します。Jumpがエンジンのシーンやマップを自動で開くわけではありません。それには対応するゲームアクションが必要です。

{
  "flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
  "id": "2e1710dc-c32f-418f-abca-e055d6fd28f3",
  "kind": "jump",
  "target": {
    "entryRef": "a4dd48dc-b0e2-47fc-b102-def52583a8fe",
    "flowRef": "2a0a259c-b76a-4de0-8cee-567713c2d320"
  }
}
Call Substory / Return · ミラが倉庫を説明する

サブストーリー呼び出し/復帰

この流れもJSONで表現されています。挨拶の後、鍵のかかった倉庫の説明を呼び出します。ゲームはcontinuationをスタックに積み、targetへ移ります。ミラの説明後にreturnへ到達したら、直近のcontinuationを取り出して「Already have the key?」へ進みます。入れ子の呼び出しも同じルールです。

{
  "continuation": {
    "kind": "node",
    "nodeRef": "95e9a55f-f6b1-4696-9c52-6ea3ea2ac7e5"
  },
  "flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
  "id": "85b3aadf-2b95-4150-bc44-c0c796337874",
  "kind": "call",
  "target": {
    "entryRef": "2bac8555-3cae-4a21-9b8c-b410ab207ebb",
    "flowRef": "ea13b52e-092a-44ff-b087-0ce65d040c9a"
  }
}
{
  "flowRef": "ea13b52e-092a-44ff-b087-0ce65d040c9a",
  "id": "f8b8673a-cb86-40ae-a4f6-d37a5c1563c3",
  "kind": "return"
}

関係するすべてのノードはstory.jsonに含まれます。Callは別のJSONやスクリプトを読み込む操作ではありません。入れ子の深さはmaxCallDepthで制限します。空のスタックでReturnするとエラーです。一方、Endは会話全体を終了し、スタックを空にします。

Command · 店を開く、鍵を渡す

コマンド

例えば店を開く場合、ゲーム側が店の画面を開いて会話を一時停止します。店を閉じてから、purchasedやcancelledなどの合意済みの結果を返します。結果の名前と引数はVNLEプロジェクトのゲームアクションで定義してください。open_shopはVNLEの組み込みコマンドではありません。

港の実際のエクスポートではgive_itemを使います。コマンドは定義を参照し、アイテムIDと2つの結果を持ちます。ゲームは鍵をインベントリに追加するか、受け取りを拒否します。どちらの結果にも、その後の進行先が作成者によって設定されています。

{
  "arguments": {
    "3cac6185-4404-4a2a-be5f-14afa0f97137": {
      "kind": "literal",
      "value": {
        "entityType": "item",
        "type": "reference",
        "value": "2d71e57e-9b58-4fdb-a33f-42b89bd8bf04"
      }
    }
  },
  "commandRef": "0723beb0-daba-4602-a665-4efc27511bc3",
  "flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
  "id": "500c28a1-ed85-4c36-9a38-bedacce1c6b7",
  "kind": "command",
  "outcomes": {
    "12bc86a9-b2eb-4eb4-836c-a9efd936bf1b": {
      "continuation": {
        "kind": "node",
        "nodeRef": "382640aa-5b50-4715-9148-b470bc2f7cd6"
      },
      "resultBindings": {}
    },
    "1c527580-69a6-4e99-8503-1b0de3fc8e78": {
      "continuation": {
        "kind": "node",
        "nodeRef": "221d6b0c-99e1-45a8-bc73-768c4ee679eb"
      },
      "resultBindings": {}
    }
  }
}
{
  "id": "0723beb0-daba-4602-a665-4efc27511bc3",
  "key": "give_item",
  "name": "Give item",
  "category": "custom",
  "inputs": [
    {
      "id": "3cac6185-4404-4a2a-be5f-14afa0f97137",
      "name": "Item",
      "valueType": {
        "entityType": "item",
        "kind": "reference"
      }
    }
  ],
  "outcomes": [
    {
      "id": "12bc86a9-b2eb-4eb4-836c-a9efd936bf1b",
      "name": "Received",
      "results": []
    },
    {
      "id": "1c527580-69a6-4e99-8503-1b0de3fc8e78",
      "name": "Declined",
      "results": []
    }
  ]
}
  1. commandRefをexecution.commandsで調べてシグネチャを取得し、content.commandDefinitionsで読みやすいキーを取得します。
  2. 入力IDを使って引数を評価します。省略可能な引数がなければ、定義されたdefaultValueを使います。
  3. 明示的に対応しているゲームアクションを一度だけ要求します。結果を待つ間は、Continueや二重実行を許可しないでください。
  4. 結果IDと型付きの戻り値を検証します。outcome.resultBindingsに従って値を物語の変数へ代入し、outcome.continuationへ進みます。
  5. 技術的な失敗を、自動的に作成者のキャンセル結果として扱ってはいけません。エラーを表示し、ゲーム側で再試行やキャンセルの手順を用意します。

このプロジェクトでは、鍵を受け取れないと5ゴールドを返金します。これはエクスポート内の進行先による動作で、すべてのCommandに自動で付く処理ではありません。インベントリ、クエストログ、マップはゲーム側の仕組みです。JSONの項目だけでは作られません。

プレースホルダー:「残りは5ゴールドです」

テキストとプレースホルダー

{
  "continuation": {
    "kind": "node",
    "nodeRef": "2e1710dc-c32f-418f-abca-e055d6fd28f3"
  },
  "flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
  "id": "0d7f959e-8fba-4882-ae98-b66d8416d4d2",
  "kind": "dialogue",
  "speakerRef": "807b2957-7bab-4a28-83a4-1f3d1589bbc2",
  "text": {
    "bindings": {
      "1084cf8e-515e-4b33-92f8-b5c260e88a9e": {
        "kind": "variable",
        "variableRef": "a547610e-661f-4b8d-a25b-21d042f1a2d5"
      }
    },
    "textRef": "fcc775ff-558e-4a41-823d-183aaff03e4b"
  }
}

kind: textのセグメントはそのまま連結します。kind: placeholderではplaceholderRefを使い、テキスト項目内の定義とnode.text.bindings内のバインディングを探します。バインディングはリテラル値か変数値を提供します。セリフを表示する時点の値を保持し、型に合った形式で表示してください。型と書式はフィールドリファレンスで確認できます。

PlaceholderDefinition · メッセージ

進捗、制限、エラー

現在位置、物語の変数、戻り先のスタック、応答待ちのゲームアクションをゲームの状態と一緒に保存します。言語は独立した設定です。storyBuildIdでセーブと実行データのバージョンを対応付けられます。互換性のない物語で進行先を推測してはいけません。

maxAutomaticTransitions、maxConditionDepth、maxConditionTerms、maxCallDepthで、自動進行と深い入れ子を制限します。参照先がない、whenEmpty: faultなのに回答がない、未知のコマンド、無効な型などの場合は、明確なエラーを表示します。UIの再描画で購入や変数の変更を繰り返してはいけません。

開発担当者やAIに実装を依頼する

実際のエクスポート、エンジンのバージョン、対象プラットフォーム、この進行ルール、エンジンのガイドを渡します。UI要素と既存のインベントリ・店舗機能を説明し、実際のJSONフィールドと公式APIに基づく実装を依頼します。以下のケースもテストしてください。

  1. 10ゴールドなら鍵を受け取り、残りは5。2ゴールドなら鍵を受け取れず、所持金も減らないこと。
  2. インベントリが受け取りを拒否したら、物語の指定どおり返金すること。成功したものとして扱わないこと。
  3. Call/Returnで意図したノードへ戻り、Call内のEndでは会話全体が終了すること。
  4. 言語を変えても進行状況が保たれ、未翻訳や意図的に空にした文章も正しく扱えること。
  5. 同梱したJSON・画像・フォントが開発フォルダーの外でも使えること。画像がないときに前のポートレートが残らないこと。