クエストのアーキテクチャ

スクリプトなし、ロジックあり:Romergo のノードと変数の仕組み

ゲーム エディターでロジックについて話すとき、通常はすぐにスクリプトに行き着きます。著者はどこかで JavaScript を書き、どこかで Python を書き、どこかでエンジン独自の言語を研究しています。これにより、ほぼ無制限の自由が得られますが、それに伴い、構文エラー、無限ループ、互換性のないプラグイン、6 か月後には作成者ですら開くのが怖いコードが発生します。

Romergo では、別の道を選びました。クエスト内に任意のスクリプトはありません。機能を挿入したり、ネットワーク要求を行ったり、プレイヤーに直接アクセスしたりすることはできません。代わりに、ストーリーは、論理ノード、変数、条件、効果など、限定的ではあるが十分に表現力豊かなセットから組み立てられます。

これは、Romergo では単純な分岐しか作成できないという意味ではありません。プレイヤーの決定を記憶し、ポイントを数え、オプションを開いたり隠したり、後続の章で異なるルートを選択したり、ランダムイベントを作成したり、インタラクティブなインターフェイスを構築したりできます。ただ、「このコードを実行してください」というコマンドの代わりに、作者は「この状態では、これが起こるはずです」と説明しています。

グラフはすでにプログラムになっています

Romergo の最も基本的なロジックは、チャプター マップ上にあります。シーンはトランジションによって接続されており、特別なノードが進行が次にどこに進むかを決定します。

非常に単純化すると、小さなクエストは次のようになります。

Start
  ↓
Conversation with the guard
  ↓
Is there a pass?
  ├─ yes → Closed archive
  └─ no → Bypass

これはすでに実行可能なロジックです。 Player は開始ノードに入り、シーンを表示し、履歴状態を読み取り、適切な遷移を選択します。著者は、同じパスを ifgoto、およびサービス ID のセットとしてではなく、わかりやすいマップとして捉えています。

![フロー Romergo: Final Accusation は Switch と 3 つの If / Else を通過し、下部のロジック パネルが開いた状態で異なるエンディングに進みます](../assets/logic-without-scripts-flow-nodes.webp)

*Builder の実際のフローの断片: 最終告発シーンでスイッチに決定が伝えられ、その後、いくつかの If / Else で状態がチェックされ、ストーリーが異なるエンディングに分岐します。下部にある開いているロジック パネルには、利用可能なセット全体が表示されます: Dead End、Checkpoint、If / Else、Switch、Random、Teleport。*

現在、ロジック パネルには 6 つのメイン ノードがあります。

開始と終了でチャプターのフレームが構成され、通常のシーンでコンテンツが表示され、論理ノードがそれらをルートに変換します。このレベルでも、スクリプトを 1 行も書かずに、直線的なストーリー、分岐するパス、複数のエンディング、ランダムなイベント、安全な帰還ポイントを組み立てることができます。

変数は歴史の記憶です

履歴が以前に何が起こったかを記憶する必要がある場合、1 つのグラフでは十分ではありません。このため、クエストには 3 つの単純なタイプの変数があります。

各変数にはキー、作成者が理解できるラベル、および初期値があります。数値変数には最小値と最大値を記述することもでき、サービス変数はプレイヤーのステータス パネルから非表示にしたり、条件が満たされた場合にのみ表示したりすることができます。

ここで重要なことは、変数は単一のシーンだけではなく、クエスト パッセージ全体に属するということです。第 1 章でプレイヤーがパスを獲得した場合、第 4 章では同じフラグを確認できます。終了から次の章に移動する場合、状態はリセットされません。これは、選択したオプション、適用された効果、チェックポイント、およびランダム性の現在のステップとともに保存されます。

これにより、単純な作成ループが発生します。

the player's choice writes a value
              ↓
the variable stores the state
              ↓
the condition reads it later
              ↓
the graph, scene, or interface reacts

たとえば、最初のシーンでは、「見つかった写真を表示する」を選択すると、portrait_revealed = true が設定される可能性があります。その後、**If / Else** ノードがこのフラグをチェックし、新しい会話シーンを開きます。その後でも、端末インターフェイスには、写真を公開したプレイヤーにのみ追加のエントリが表示されます。

1 つの変数がクエストの 3 つの異なる部分を接続します。シーンまたはチャプター間で値を手動で渡す必要はありません。それらはすべて同じ一般的な進行状態を読み取ります。

![変数ページ ルートは、状態のキー、名前、タイプ、および可視性を示します。](../assets/logic-without-scripts-variables.webp)

*1 つのカード内の変数 route には、安定したキー、明確な名前、値のタイプ、および状態の可視性が設定されています。選択肢と分岐との接続は、同じページの下にあります。*

選択肢は書き込み、分岐は読み取り

一般的なシーンでは、状態を変更する最もわかりやすい方法は、選択オプションにエフェクトを追加することです。ビジュアル エディターでは、これは割り当てのように見えます。

"Trust Mary" → ally = "mary"
"Go alone"     → ally = "none"

その後、Switch は、ally の値に基づいてプレイヤーを目的のシーンに誘導できます。変数ページには、どの選択肢が変数を設定するのか、どのブランチがそれを読み取るのかが個別に表示されます。これは細かいことですが、大規模なクエストでは、何十ものシーンにわたる退屈な検索の代わりになります。

![設定は、選択したルートの変数を比較する、Laboratory および Habitat ブランチに切り替えます](../assets/logic-without-scripts-switch-rules.webp)

※スイッチ分岐は上から順にチェックされます。この例では、実験室ルートは route = lab でトリガーされ、生息地は route = habitat でトリガーされます。

条件は一般的な比較をサポートします。

単純なケースでは、作成者は If / Else または Switch 設定で変数、演算子、値を直接選択します。より複雑なロジックの場合、一般的なランタイムは、**すべての条件**、**任意の条件**、および否定のグループを理解します。

このようなルールは構造的に説明できます。

{
  "op": "all",
  "conditions": [
    { "op": "gte", "key": "evidence", "value": 3 },
    { "op": "eq", "key": "alarm_active", "value": false }
  ]
}

これは小さなスクリプトのように見えますが、基本的にはスクリプトではありません。ここで関数を呼び出したり、何かをランダムに変更したりすることはできません。 Romergo は、許可されるすべての操作をすでに認識しており、構造をチェックして、Builder プレビューと公開された Player で同じ方法で実行します。

効果: プログラミング言語を使用しない小さなコマンド

条件は状態を読み取り、効果はそれを変更します。 Runtime Romergo は、いくつかのアトミック操作をサポートしています。

通常の選択インスペクターでは、主なシナリオは意図的に単純になっています。つまり、変数を選択し、set を通じて新しい値を記録します。高度なインターフェイス ロジックでは、条件と効果をチェック可能な構造として設定できます。たとえば、ターミナル状態間の遷移では、決定を記憶し、証拠を追加し、アラームをトリガーすることができます。

![シーンエディター Romergo: 3 つの選択肢のオプションは、ラボ、生息地、およびサーバーを変数ルート ](../assets/logic-without-scripts-choice-effects.webp) に記録します。

*効果は回答選択肢に直接付加されます。3 つの選択肢は、labhabitatserverroute に記録します。変数フィールドと新しい値は、狭いインスペクターでも分離されたままになります。*

[
  { "op": "set", "key": "terminal_decision", "value": "cut-power" },
  { "op": "inc", "key": "evidence", "amount": 1 },
  { "op": "toggle", "key": "alarm_active" }
]

このアプローチには重要な制限があります。それは、任意の数式を計算するための計算機ではないということです。 evidence * trust / 2 を記述したり、ループを開始したり、独自の関数を作成したりすることはできません。複雑な動作はいくつかの明示的なステップから組み立てられ、ロジックの拡張は、未知のコードを実行するのではなく、新しいチェック可能な操作のランタイムを通じて行われます。

著者にとって、これは無制限ではありませんが、ストーリーは依然として予測可能です。同じエフェクトを公開前にテストし、保存してプレイヤー間で同期し、ロードの進行後に再現することができます。

論理ノード内に別のフローが存在しないのはなぜですか

あらゆる予測可能性にもかかわらず、現在のアプローチには正直な欠点があります。開発経験のない人にとって、「変数 - 演算子 - 値」という形式はすでにプログラミングのように見える可能性があります。また、条件のグループといくつかの効果が近くに現れる場合、宣言構造は Player にとって安全なままですが、作成者にとっては必ずしも単純になるわけではありません。

最終的にはビジュアル ノードやロジック自体の中に行き着くのは自然なことのように思えます。条件のリストの代わりに、ブロック **AND**、**OR**、**NOT**、変数の比較、および値の変更を接続できます。紙の上では、これはテキストや JSON よりも親しみやすいように見えます。

しかし、私は実際のシナリオを想像します。作成者がチャプタ マップ上でロジック ノードを開き、その中に独自のノードと接続を持つ別のフローを見つけます。それはグラフ内のグラフであることがわかります。現在どこにいるのか、ストーリー レベルに戻る方法、2 つのフローのどちらでエラーを探すのかを理解する必要があります。初心者にとって、このようなネストは現在の形式よりも怖いかもしれません。

したがって、現時点では妥協案が使用されます。ストーリー ルートは視覚的なままですが、ノード内の条件と効果はコンパクトな明示的なフィールドとして表示されます。動作しますが、インターフェースが最終的に解決されたとは思えません。おそらく後で、既製のテンプレート、段階的なセットアップ、および視覚的なブロックのよりわかりやすい組み合わせが登場するでしょう。これは、単に別のエディターに移行するのではなく、実際に複雑さを軽減するものです。

ロジックは地図上だけで生きているわけではない

インターフェイス シーンは同じステートを使用します。ウィジェット、メッセージ、回答オプション、タスク、通知、またはメディア ストリームは、showWhen が一致する場合にのみ表示されます。メトリック値は変数にリンクでき、ウィジェットの状態間の遷移は、遅延後、条件に基づいて、またはプレイヤーのアクション後にトリガーできます。

たとえば、オンボード コンピューターの画面は次のように動作します。

1. 最初は、ロックされた端末のみが表示されます。

2. プレイヤーの選択後、エフェクトは terminal_unlocked = true を設定します。

3. この条件により、船の回路図と新しいボタンが開きます。

4. ボタンを押すとウィジェットの状態が変化し、evidence が増加します。

5. evidence >= 3 の場合、非表示の通知が表示され、新しいクエスト分岐が利用可能になります。

チャプター マップ、通常の選択肢、および対話型インターフェイスによって、3 つの個別のシステムが作成されるわけではありません。これらは単一の状態オブジェクトを読み取り、変更します。したがって、ダイアログ内で見つかった手がかりによってインターフェイス要素のロックが解除され、インターフェイス内のアクションによって後続のシーンが変更される可能性があります。

ここで、宣言型ロジックが実際のプログラミングのように感じられ始めます。ファイル、関数、イベント バスの代わりにのみ、作者は事実、選択、条件、遷移といった歴史の理解可能なエンティティを扱います。

意図的に任意のJavaScriptを追加してみてはいかがでしょうか

スクリプトを挿入する機能は、新しいメカニズムへの最短の道のようです。しかし、公開されたクエストが任意のコードを実行する権利を取得するとすぐに、製品モデル全体が変更されます。

このコードでどのブラウザ API を使用できるか、ネットワークとストレージを制限する方法、無限ループをどうするか、クエストをオフラインで転送する方法、マルチプレイヤーで同期する方法、Player を更新した後も古いプロジェクトが動作し続けることを確認する方法を決定する必要があります。

宣言ルールは任意の関数よりもはるかに退屈です。だからこそ、宣言ルールの方が信頼性が高いのです。実行前に検証することができます。どの変数を読み書きするのかを理解できます。進行状況はチェーンの途中で安全に保存できます。クエストは、ブラウザ、スタンドアロン ビルド、および埋め込み Player で同じ方法で実行できます。

ここでの語彙の制限は論理を妨げるものではありませんが、論理の境界を設定します。本当に有用な操作が現れた場合は、各プロジェクトに独自のエンジンを発明させるよりも、それを共通のランタイムに追加して、すべての作成者に同じ検証済みの動作を提供する方が良いでしょう。

デザートには、自分だけの HTML と CSS

同時に、Romergo は、最も安全な場所、つまり通常のシーンのデザインに実際のコードを入れる余地を残しています。

プロジェクトでは、HTML および CSS で独自の外観テーマを作成し、さまざまなチャプターで使用できます。 HTML は、ダイアログ ウィンドウ、ポートレート、キャラクター名、テキスト、質問、選択ボタンなど、準備された領域のレイアウトを設定します。 CSS を使用すると、境界線、背景、カードの形状、間隔、タイポグラフィ、画面サイズへの対応などの領域を大幅に変更できます。

既製のテーマ **スタンダード**、**ミニマリズム**、**ブルータリズム**、**ロマンス**、**ネオン**は、手動レイアウトなしで確認済みの範囲を表示します。カスタム テーマを作成するとき、Romergo は選択したテンプレートをコピーします。その後、その HTML および CSS を編集して、ライブ Player プレビューですぐに確認できます。

![実際の書式設定エディター Romergo: ブルータリズム テーマの CSS とモバイル ダイアログ プレビュー ](../assets/logic-without-scripts-custom-html-css.webp)

*ここにはコンセプト アートはありません。左側は組み込みテーマ Brutalism の実際の CSS、右側は携帯電話のプレビューで同じコードを実行した結果です。デスクトップ/モバイルの切り替えにより、レスポンシブ ルールをすぐに確認でき、カスタム テーマは選択したテンプレートのコピーで動作を開始します。*

しかし、国境はそのままです。カスタム HTML には、<script>、イベント ハンドラー、フォーム、iframe、および外部リソースはありません。 CSS は、外部 url() または @import をロードできません。必要なシーン領域は所定の位置に維持する必要があります。テンプレートでそれらが失われると、Player は安全な標準デザインに戻ります。

つまり、HTML と CSS は **ストーリーの見た目**を担当し、ノード、変数、条件、エフェクトは **ストーリーの動作**を担当します。

こういう別れ方が好きです。作成者は、他のクエストとは視覚的に異なるクエストを作成できますが、その完了はテスト可能なランタイム全体の一部のままです。各テーマを別個のプログラムにすることなく、外側のレイヤーを根本的に色変更して再構築することができます。

コードのないコードはロジックの欠如を意味するものではありません

Romergo は、プログラミング言語を画像に置き換えることを目的としたものではありません。グラフは複雑な数学にはあまり適しておらず、一連のアトミック効果は万能の自動化エンジンにはなりません。

ただし、インタラクティブなストーリーの場合は、他のことが必要になることがよくあります。つまり、事実を記憶する、カウンターを変更する、選択肢を開く、ルートを選択する、インターフェイスの状態を表示する、チェックポイントを保存する、これらすべての決定を次の章に伝えるなどです。

このため、ノードと変数はスクリプトの簡略化されたバージョンではなく、ストーリー自体のより直接的な言語であることがわかります。著者はコンピュータへのコマンドではなく、世界の因果関係を説明します。つまり、プレイヤーが選択を行い、状態が変化し、ストーリーが反応します。

実際のコードが少し必要な場合は、HTML と CSS がまだデザートを待っています。

開発ブログに戻る