Skip to content

UIによるシーンジャンプとエントリ切り替え

ボタンをクリック → 異なる事前作成のオープニングにジャンプ。テキストボックスに入力 → AIにエントリが伝える内容を変更。このレシピでは両方を示します。


パート1:ボタンでオープニングを切り替え

これから作るもの

複数の事前作成されたオープニングシーンを持つワールド。プレイヤーはまず「メイン」オープニングをクリック可能なボタン付きで見ます。クリックすると、チャット内の最初のメッセージが瞬時に別の事前作成オープニングに切り替わる — AI生成なし、あなたが書いたテキストそのままです。

仕組み

Yuminaでは、エディタのFirst Messageタブで複数のグリーティングを作成できます。プレイヤーが新規セッションを開始すると、すべてのグリーティングが最初のメッセージのスワイプ(左右で切り替え)としてパックされます。プレイヤーはすでに手動でスワイプできます — しかし私たちが欲しいのは:ワンクリックでプレイヤーを特定のグリーティングにジャンプさせる

それがswitchGreeting(index)APIの役割です — カスタムコンポーネントがコードでN番目のグリーティングに直接ジャンプできるようにします。

プレイヤーが「Enter the Dark Cave」をクリック
  → コードが api.switchGreeting(1) を呼ぶ
  → 最初のメッセージがグリーティング#2に切り替わる(インデックスは0から開始、1 = 2番目)
  → プレイヤーが事前作成された暗い洞窟のオープニングを即座に見る

ステップバイステップ

ステップ1:First Messageタブで複数のグリーティングを作成

エディタを開き、左サイドバーのFirst Messageタブをクリックします。

このタブはオープニングの管理専用です。複数のグリーティングを作成でき — それぞれがスワイプになります。

最初のグリーティング(メインオープニング — ルート選択を提示)を作成:

「Create First Message」をクリック。テキストボックスにメインオープニングを書きます。これはプレイヤーがセッションを開いたときに最初に見るもの — シーンを記述し選択へと導きます:

*You wake up in a mysterious forest. Morning mist swirls between ancient trees.*

Two paths diverge before you:

**To the left** — a narrow trail into darkness. Cold air and distant echoes.

**To the right** — a sun-dappled path with wildflowers and birdsong.

Which way will you go?

なぜシーンを記述するだけでAIに応答を求めないのか?グリーティングはあなたが事前に書いた固定テキストであり、AI生成ではないからです。プレイヤーが見るすべての単語を正確に制御できます。

2番目のグリーティング(暗い洞窟のオープニング)を作成:

下の「Add Greeting」をクリック。番号付きタブ12が表示されるはず。2をクリックして2番目のグリーティングの編集ボックスに切り替えます。暗い洞窟ルートのオープニングを書きます:

*You step onto the left path. The canopy thickens overhead, swallowing the light. Within minutes, the trail narrows to a crack in a rock face — the entrance to a cave.*

*Cold air rushes out, carrying the smell of damp stone and something metallic. Faint blue-green light flickers deep inside — bioluminescent fungi clinging to the walls.*

*You take a breath and step in. Behind you, the last sliver of daylight shrinks to a pale line, then vanishes.*

You are alone in the dark.

このテキストはプレイヤーが「Enter the Dark Cave」をクリックした後にのみ表示されます。それまではプレイヤーは最初のグリーティング(メインオープニング)を見ます。

3番目のグリーティング(日当たりの良い草原のオープニング)を作成:

「Add Greeting」を再度クリック。タブ3に切り替え、日当たりの良い草原ルートのオープニングを書きます:

*You choose the right path. The trees thin out, and warm sunlight floods through the canopy. Within minutes, the forest opens into a vast meadow stretching to the horizon.*

*Wildflowers in every color sway gently in the breeze. A stream glitters in the distance. Somewhere nearby, a bird sings a melody you've never heard before.*

*You feel the tension in your shoulders melt away. Whatever this place is, it feels safe.*

Welcome to the Everbloom Meadow.

グリーティングの順序がインデックス

下部の番号付きタブの順序がswitchGreeting()indexパラメータです。タブ1 = インデックス0(デフォルト表示)、タブ2 = インデックス1、タブ3 = インデックス2。後でボタンコードを書くときにこのインデックスを使用します。

これで3つのグリーティングがあります。ワールドを保存後、新規セッションはデフォルトで最初(メインオープニング)を表示します。次に、プレイヤーが2番目または3番目にクリックスルーできるボタンを作成します。


ステップ2:ルート追跡変数を作成

「プレイヤーがどのルートを選んだか」を記録する変数が必要です。この変数には2つの用途があります:

  • 選択後にボタンを消す(TSXコードがこの変数をチェック — "none"でなければボタンを表示しない)
  • 後の会話で現在のルートを知らせる(ビヘイビアルールがこの変数に基づいてロアエントリを切り替えられる)

エディタ → 左サイドバー → Variables タブ → 「Add Variable」をクリック

フィールド入力内容理由
Display NameCurrent Route自分用の識別名
IDcurrent_routeコードがこのIDで変数を読み書き
TypeString値がテキスト("none""dark""light")だから
Default Valuenone「まだ選択していない」を意味。ボタンコードがこの値をチェック
CategoryTag単なるカテゴリラベル、変数リストで見つけやすくする
Behavior RulesDo not modify this variable. It is controlled by the player's UI choice.この変数を変更しないようAIに伝える — ボタンのみ可能

Behavior RulesフィールドはAIへの指示です。書かないと、AIが返信で自分の判断でこの変数の値を変更するかもしれません(例:AIが「プレイヤーが洞窟に入った」と判断しcurrent_route"dark"に設定)。ルールを書けば、AIは触れません。


ステップ3:(オプション)ロアエントリとビヘイビアルールを作成

ルート選択後にAIの後の返信で異なる世界観を参照させたい場合は、このステップを実行します。後の世界変更なしでオープニングテキストだけを切り替えたい場合は、スキップできます。

2つのロアエントリ(デフォルトで無効)を作成:

エディタ → Entries タブ → 新規エントリを作成

暗い洞窟のロアエントリ:

フィールド入力内容理由
NameDark Cave Lore自分用の識別名
SectionSystem Presetsプリセットセクションのエントリは毎回AIに送信される
EnabledNo(トグルオフ)デフォルトで無効 — プレイヤーが暗いルートを選んだ後、ビヘイビアルールがこれを有効化

内容:

[World Setting: Shadowmaw Cave]
The player is exploring Shadowmaw Cave. Key details:
- Ancient dwarven ruins, abandoned for centuries
- Bioluminescent fungi provide faint blue-green light
- Strange creatures lurk in the deeper tunnels
- Temperature drops the further in you go

Maintain a tense horror-survival atmosphere. Describe echoing sounds, flickering shadows, water dripping, and the oppressive weight of stone overhead.

日当たりの良い草原のロアエントリ: 別のエントリを作成、これもデフォルトで無効、草原の設定と雰囲気を記述する内容にします。

なぜデフォルトで無効? プレイヤーがルートを選ぶ前は、どちらの世界観もAIに影響を与えるべきではないからです。プレイヤーが選んだ後でのみ、ビヘイビアルールが対応するものを有効にし、もう一方を無効にします。

2つのビヘイビアルールを作成:

エディタ → Behaviors タブ → Add Behavior

ビヘイビア1:「Choose Dark Route」

フィールド入力内容理由
NameChoose Dark Route自分用の識別名
Trigger「Action」を選択 → アクションID choose-darkTSXコードがexecuteAction("choose-dark")を呼ぶと発火

そして「Execute Actions」の下に順番に追加:

Action type設定効果
Modify variablecurrent_routedarkに設定プレイヤーが暗いルートを選んだことを記録
Enable entryDark Cave Lore暗い洞窟の設定をオン
Disable entrySunlit Meadow Lore草原の設定をオフ(両方が有効になることを防ぐ)

ビヘイビア2:「Choose Light Route」 — 同じ方法で作成。アクションIDはchoose-light、アクションは逆(草原のロアを有効化、洞窟のロアを無効化)。

なぜTSXコード内でsetVariableを直接使わないのか? setVariableは変数を変更できるだけ — エントリのオン/オフを切り替えられない。ビヘイビアの「Enable Entry」/「Disable Entry」アクションが、実行時にエントリを有効/無効にします。そのため、ボタンがクリックされると3つのことを同時に行います:setVariable(変数を変更)+ executeAction(エントリを切り替えるビヘイビアを発火)+ switchGreeting(オープニングを切り替え)。


ステップ4:Root Componentにルート選択ボタンを追加

これがチャットインターフェースにボタンを表示させる重要なステップです。

エディタ → Custom UI セクション → index.tsxを開く → 以下のコードを貼り付け(デフォルトを置き換え):

tsx
export default function MyWorld() {
  const api = useYumina();
  const hasChosen = api.variables.current_route !== "none";

  return (
    <Chat renderBubble={(msg) => (
      <div>
        {/* Render the message text normally */}
        <div
          style={{ color: "#e2e8f0", lineHeight: 1.7 }}
          dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
        />

        {/* Route selection buttons */}
        {/* msg.messageIndex === 0 means only show on the first message */}
        {/* !hasChosen means hide once a choice has been made */}
        {msg.messageIndex === 0 && !hasChosen && (
          <div style={{
            display: "flex",
            gap: "12px",
            marginTop: "16px",
          }}>
            <button
              onClick={() => {
                api.setVariable("current_route", "dark");   // Record the choice, making the buttons disappear
                api.executeAction("choose-dark");            // Fire the behavior rule to toggle lore entries
                api.switchGreeting?.(1);                     // Switch to the 2nd greeting
              }}
              style={{
                flex: 1,
                padding: "16px",
                background: "linear-gradient(135deg, #1e1b4b, #312e81)",
                border: "1px solid #4338ca",
                borderRadius: "12px",
                color: "#c7d2fe",
                fontSize: "15px",
                fontWeight: "bold",
                cursor: "pointer",
              }}
            >
              Enter the Dark Cave
            </button>

            <button
              onClick={() => {
                api.setVariable("current_route", "light");
                api.executeAction("choose-light");
                api.switchGreeting?.(2);                     // Switch to the 3rd greeting
              }}
              style={{
                flex: 1,
                padding: "16px",
                background: "linear-gradient(135deg, #365314, #4d7c0f)",
                border: "1px solid #65a30d",
                borderRadius: "12px",
                color: "#ecfccb",
                fontSize: "15px",
                fontWeight: "bold",
                cursor: "pointer",
              }}
            >
              Walk to the Sunlit Meadow
            </button>
          </div>
        )}
      </div>
    )} />
  );
}

行ごとの説明:

  • <Chat renderBubble={...} /> — プラットフォームのデフォルトチャットインターフェース(入力ボックス、スワイプ切り替え、セーブポイントすべて内蔵)を使用、バブルのレンダリング方法だけを引き継ぐ
  • const api = useYumina() — YuminaのAPIを取得、変数の読み取り、書き込み、アクションの発火、グリーティングの切り替えが可能
  • api.variables.current_route — 現在のルート変数の値を読む
  • hasChosen"none"でなければプレイヤーはすでに選択済み
  • msg.contentHtml — renderBubbleが渡す事前レンダリングされたHTML(Markdownはすでに処理済み)
  • msg.messageIndex === 0 — 最初のメッセージにのみボタンを表示(全メッセージではない)
  • !hasChosen — 選択後にボタンが消える
  • api.setVariable("current_route", "dark") — 変数を"dark"に設定、hasChosentrueになりボタンが消える
  • api.executeAction("choose-dark") — ステップ3で作成したビヘイビアルールを発火
  • api.switchGreeting?.(1) — 最初のメッセージをインデックス1(2番目のグリーティング)に切り替え。?.はオプショナルチェイニング — APIが利用できない場合スローしない

自分でコードを書きたくない?Studio AIを使おう

エディタ上部 → 「Enter Studio」をクリック → AI Assistantパネル → 欲しいものを平易な言葉で説明すると、AIがコードを生成します。


ステップ5:保存してテスト

  1. エディタ上部の「Save」をクリック
  2. 「Start Game」をクリックするか、ホームページに戻って新規セッションを開始
  3. 下に2つのボタンがあるメインオープニングが見える
  4. 「Enter the Dark Cave」をクリック — 最初のメッセージが瞬時にあなたの事前作成された洞窟オープニングになり、ボタンが消える
  5. AIに数メッセージ送信 — ステップ3を行った場合、AIの返信は洞窟のロアの影響を受ける
  6. もう一方のルートをテストしたい?ホームに戻って新規セッションを開始、今度はもう一方のボタンをクリック

トラブルシューティング:

症状想定される原因対処
ボタンが見えないRoot Componentコードが保存されていない、または構文エラーCustom UIセクション下部のコンパイル状態を確認 — 緑の「OK」が表示されるべき
ボタンクリックで何も起きないサーバー上でswitchGreetingがまだデプロイされていない最新バージョンを使用していることを確認
ボタンクリックでオープニングが切り替わらないグリーティングが足りないFirst Messageタブに3つのグリーティングがあることを確認
ボタンがクリックされるが消えない変数が正しく設定されていないエディタを確認 — 変数のデフォルトがnoneで、Root Componentコードがcurrent_routeを正しくチェックしているか?
ロアが切り替わらないビヘイビアルールの設定ミスビヘイビアのアクションIDがコード(choose-dark / choose-light)と一致するか確認

パート2:プレイヤー入力でエントリ内容を変更

これから作るもの

チャットインターフェースにテキスト入力を追加。プレイヤーがそこに何か(カスタムルール、キャラクター名、ストーリー指示など)を入力します。「Apply」をクリック後、テキストがロアエントリに注入され — AIが次に見るものが変わります。

仕組み

Yuminaのエントリはマクロ構文をサポートします。エントリの内容に{{variableId}}と書けます — それがプレースホルダー。エンジンがAIに送るプロンプトをビルドするたびに、プレースホルダーが変数の現在値に自動的に置換されます。

例:

  • エントリに書く:Special rule: {{custom_rule}}
  • 変数custom_ruleの値が"All magic is allowed"
  • AIが受け取るプロンプトでその行は次のように書き換わる:Special rule: All magic is allowed

重要なポイント:置換はライブではありません。 プロンプトがビルドされるたびに発生 — つまりプレイヤーが次のメッセージを送ってAIが返信しようとするときです。

完全なタイミング:

1. エントリ内容に書く:「Special rule: {{custom_rule}}」
2. 変数 custom_rule の現在値 = 「All magic is allowed」
3. プレイヤーがメッセージ送信 → エンジンがプロンプトビルド → {{custom_rule}} を変数値に置換
   → AIが「Special rule: All magic is allowed」を受け取る → AIがそれに従って返信

4. プレイヤーが入力ボックスに「Magic is forbidden」と入力、「Apply」をクリック
5. コードが setVariable("custom_rule", "Magic is forbidden") を呼ぶ
   → 変数値が即座に更新
6. しかしAIはまだ知らない!プロンプトはまだ再ビルドされていない。

7. プレイヤーが別のメッセージを送信 → エンジンがプロンプト再ビルド → 今度は新しい値を使う
   → AIが「Special rule: Magic is forbidden」を受け取る → AIが新しいルールに従い始める

一行サマリ:変数の変更は即座ですが、AIが変更を見るのは次のメッセージ時です。

ステップバイステップ

ステップ1:文字列変数を作成

この変数はプレイヤーが入力するものを保持します。

エディタ → Variables タブ → 「Add Variable」

フィールド入力内容理由
Display NameCustom Rule自分用の識別名
IDcustom_ruleエントリ内の{{custom_rule}}マクロがこのIDを参照
TypeString内容がプレイヤーが入力する任意のテキストだから
Default Value(空のままでも、All magic is allowedのようなデフォルトを設定してもよい)空 = 新規セッションでルールなし;非空 = 開始ルール
Behavior RulesDo not modify this variable. It is set by the player via UI.この変数自体を変更しないようAIに伝える

ステップ2:エントリでマクロを使用

これで{{custom_rule}}をプレースホルダーとして使うエントリを作成します。プロンプトビルド時にエンジンが自動的に置換します。

エディタ → Entries タブ → 新規エントリを作成

フィールド入力内容理由
NameWorld Rules自分用の識別名
SectionSystem Presetsプリセットセクションのエントリは毎回AIに送信される

内容:

[World Rules]
The following rule is in effect for this world and must be respected at all times:
{{custom_rule}}

何が起きているのか? エンジンがプロンプトをビルドするたびに、すべてのエントリ内容で{{...}}をスキャンします。中括弧内が変数IDに一致すれば、その変数の現在値で置換されます。そのため{{custom_rule}}は変数custom_ruleの値で置換されます。

変数が空なら、行が空になる — AIは「The following rule is in effect...」の後に何もないものを見ます。値が「Magic is forbidden」なら、AIは「The following rule is in effect... Magic is forbidden」を見ます。


ステップ3:Root Componentに入力ボックスを追加

プレイヤーが新しいルールを入力できる入力ボックスをチャットインターフェースに欲しいです。この入力はRoot ComponentのrenderBubble内に書かれ、最後のメッセージの下にのみ表示されます(各メッセージの下に1つの入力が表示されることを避けるため)。

index.tsxに以下を追加します。すでにパート1のコードがあれば、JSXのrenderBubbleが返す中、メッセージテキストの下に追加するだけ:

tsx
// Near the top of MyWorld() (outside <Chat>), add these
const api = useYumina();                                    // If you already have it, don't duplicate
const msgs = api.messages || [];
const [ruleInput, setRuleInput] = React.useState("");
const currentRule = String(api.variables.custom_rule || "");

// Inside renderBubble, add a check
const isLastMsg = msg.messageIndex === msgs.length - 1;    // whether this is the last message

// In the JSX renderBubble returns, below the message text
{isLastMsg && (
  <div style={{
    marginTop: "12px",
    padding: "12px",
    background: "rgba(30,41,59,0.5)",
    borderRadius: "8px",
    border: "1px solid #334155",
  }}>
    <div style={{ fontSize: "12px", color: "#94a3b8", marginBottom: "6px" }}>
      World Rule: {currentRule || "(not set)"}
    </div>
    <div style={{ display: "flex", gap: "8px" }}>
      <input
        type="text"
        value={ruleInput}
        onChange={(e) => setRuleInput(e.target.value)}
        placeholder="Type a new rule..."
        style={{
          flex: 1, padding: "6px 10px", background: "#1e293b",
          border: "1px solid #475569", borderRadius: "6px",
          color: "#e2e8f0", fontSize: "13px", outline: "none",
        }}
        onKeyDown={(e) => {
          if (e.key === "Enter" && ruleInput.trim()) {
            api.setVariable("custom_rule", ruleInput.trim());
            setRuleInput("");
          }
        }}
      />
      <button
        onClick={() => {
          if (ruleInput.trim()) {
            api.setVariable("custom_rule", ruleInput.trim());
            setRuleInput("");
          }
        }}
        style={{
          padding: "6px 14px", background: "#4338ca", borderRadius: "6px",
          color: "#e0e7ff", fontSize: "13px", fontWeight: "600",
          cursor: "pointer", border: "none",
        }}
      >
        Apply
      </button>
    </div>
  </div>
)}

行ごとの説明:

  • isLastMsg — 最後のメッセージにのみ入力を表示、そうしないと各メッセージに1つずつ表示される
  • currentRule — 変数の現在値を読み、入力の上に表示してプレイヤーが現在のルールを見られるようにする
  • ruleInput — 入力中の内容を追跡するReact状態
  • onKeyDown — ボタンクリックだけでなくEnterキーでもサブミット
  • api.setVariable("custom_rule", ...) — プレイヤーのテキストを変数に書き込む。次のAI返信で、エントリ内の{{custom_rule}}がこのテキストで置換される
  • setRuleInput("") — サブミット後に入力をクリア

なぜrenderBubble内に置くのか?

YuminaのRoot ComponentはTSXファイル — デフォルトで<Chat />を返すとプラットフォームの内蔵チャットUIが得られます。チャットにインタラクティブ要素(ボタン、入力)を挿入するには、2つのパスがあります:1) ここのように<Chat renderBubble={...} />内に置く、メッセージバブルと一緒にレンダリング;2) <Chat />と浮動コンポーネントを共有flexレイアウトに置く(サイドバー用)。チャットから完全に外れた全画面UI(例:純粋なビジュアルノベル)が欲しい場合、<Chat />を完全にスキップ — 独自のレイアウトを書き、必要なら<MessageList /> + <MessageInput />を直接使う。


ステップ4:保存してテスト

  1. ワールドを保存、新規セッションを開始
  2. 最後のメッセージの下に「World Rule: (not set)」と入力ボックスが見える
  3. 「Magic is forbidden」と入力して「Apply」をクリック(またはEnterキー)
  4. 入力の上のテキストが「World Rule: Magic is forbidden」に変わる — 変数が更新された
  5. 今度はメッセージを送信(例:「I try to cast a fireball」)— このときエンジンがプロンプトをビルドし、{{custom_rule}}を「Magic is forbidden」に置換
  6. AIの応答はこのルールを反映するはず(例:「You raise your hand to cast, but your mana feels locked away by some unseen force」)
  7. ルールを再変更(例:「Only fire magic is allowed」)して別のメッセージを送信 — AIが適応する

両方のパターンを組み合わせる

グリーティング切り替えとエントリ変更を組み合わせられます。具体例:

キャラクター作成 + ストーリーオープニング:

  • **メイングリーティング(インデックス0)**はストーリーではなく、名前、クラス、バックストーリーの入力を持つキャラクター作成フォーム
  • プレイヤーが入力 → setVariableが彼らの入力を変数に書き込み → {{player_name}}{{player_class}}{{player_backstory}}マクロを持つエントリが値を拾う
  • プレイヤーが「Start Adventure」をクリック → switchGreeting(1)が本物のストーリーオープニングにジャンプ
  • 最初のAI返信から、AIはすでにプレイヤーキャラクターの名前、クラス、バックストーリーを知っている

クイックリファレンス

やりたいこと方法
事前作成されたオープニングにジャンプswitchGreeting(index) — インデックスはFirst Messageタブのグリーティング順序と一致(0始まり)
プレイヤー入力でAI動作を変更文字列変数 + エントリ内の{{variableId}} + UIからsetVariable()を呼ぶ
最初のメッセージにのみボタンを表示<Chat renderBubble>内でmsg.messageIndex === 0をチェック
選択後にボタンを非表示変数で選択を追跡、TSXでhasChosenをチェック
ルート選択後にロアを切り替え「Enable Entry」/「Disable Entry」アクションを持つビヘイビアを作成
切り替え時に音を再生ビヘイビアに「Play Music」または「Play Sound Effect」アクションを追加
切り替え時に通知を表示ビヘイビアに「Show Notification」アクションを追加

自分で試す — インポート可能なデモワールド

このJSONファイルをダウンロードしてインポートし、完全な体験を試してください:

recipe-1-demo.json

インポート方法:

  1. Yumina → My Worlds → Create New World に移動
  2. エディタで「More Actions」→「Import Package」をクリック
  3. ダウンロードした.jsonファイルを選択
  4. 全グリーティング、変数、ビヘイビア、Root Componentが事前設定されたワールドが作成されます
  5. 新規セッションを開始して試す

含まれるもの:

  • 3つのグリーティング(メインオープニング + 暗い洞窟 + 日当たりの良い草原)
  • 2つの変数(ルート追跡用current_route、プレイヤー編集可能ルール用custom_rule
  • 2つのアクションビヘイビア(ルートが選ばれたらロアエントリを切り替え)
  • Root Component(ルート選択ボタン + ルールエディタを持つ<Chat renderBubble>
  • {{custom_rule}}マクロを使うロアエントリ

これはRecipe #1です

さらにレシピが来ます — 戦闘システム、ショップインターフェース、クエスト追跡、その他。各レシピは変数、エントリ、ビヘイビア、UIを組み合わせて部分の和より大きなものを構築します。