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」をクリック。番号付きタブ1と2が表示されるはず。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 Name | Current Route | 自分用の識別名 |
| ID | current_route | コードがこのIDで変数を読み書き |
| Type | String | 値がテキスト("none"、"dark"、"light")だから |
| Default Value | none | 「まだ選択していない」を意味。ボタンコードがこの値をチェック |
| Category | Tag | 単なるカテゴリラベル、変数リストで見つけやすくする |
| Behavior Rules | Do 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 タブ → 新規エントリを作成
暗い洞窟のロアエントリ:
| フィールド | 入力内容 | 理由 |
|---|---|---|
| Name | Dark Cave Lore | 自分用の識別名 |
| Section | System Presets | プリセットセクションのエントリは毎回AIに送信される |
| Enabled | No(トグルオフ) | デフォルトで無効 — プレイヤーが暗いルートを選んだ後、ビヘイビアルールがこれを有効化 |
内容:
[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」
| フィールド | 入力内容 | 理由 |
|---|---|---|
| Name | Choose Dark Route | 自分用の識別名 |
| Trigger | 「Action」を選択 → アクションID choose-dark | TSXコードがexecuteAction("choose-dark")を呼ぶと発火 |
そして「Execute Actions」の下に順番に追加:
| Action type | 設定 | 効果 |
|---|---|---|
| Modify variable | current_routeをdarkに設定 | プレイヤーが暗いルートを選んだことを記録 |
| Enable entry | Dark Cave Lore | 暗い洞窟の設定をオン |
| Disable entry | Sunlit 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を開く → 以下のコードを貼り付け(デフォルトを置き換え):
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"に設定、hasChosenがtrueになりボタンが消えるapi.executeAction("choose-dark")— ステップ3で作成したビヘイビアルールを発火api.switchGreeting?.(1)— 最初のメッセージをインデックス1(2番目のグリーティング)に切り替え。?.はオプショナルチェイニング — APIが利用できない場合スローしない
自分でコードを書きたくない?Studio AIを使おう
エディタ上部 → 「Enter Studio」をクリック → AI Assistantパネル → 欲しいものを平易な言葉で説明すると、AIがコードを生成します。
ステップ5:保存してテスト
- エディタ上部の「Save」をクリック
- 「Start Game」をクリックするか、ホームページに戻って新規セッションを開始
- 下に2つのボタンがあるメインオープニングが見える
- 「Enter the Dark Cave」をクリック — 最初のメッセージが瞬時にあなたの事前作成された洞窟オープニングになり、ボタンが消える
- AIに数メッセージ送信 — ステップ3を行った場合、AIの返信は洞窟のロアの影響を受ける
- もう一方のルートをテストしたい?ホームに戻って新規セッションを開始、今度はもう一方のボタンをクリック
トラブルシューティング:
| 症状 | 想定される原因 | 対処 |
|---|---|---|
| ボタンが見えない | 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 Name | Custom Rule | 自分用の識別名 |
| ID | custom_rule | エントリ内の{{custom_rule}}マクロがこのIDを参照 |
| Type | String | 内容がプレイヤーが入力する任意のテキストだから |
| Default Value | (空のままでも、All magic is allowedのようなデフォルトを設定してもよい) | 空 = 新規セッションでルールなし;非空 = 開始ルール |
| Behavior Rules | Do not modify this variable. It is set by the player via UI. | この変数自体を変更しないようAIに伝える |
ステップ2:エントリでマクロを使用
これで{{custom_rule}}をプレースホルダーとして使うエントリを作成します。プロンプトビルド時にエンジンが自動的に置換します。
エディタ → Entries タブ → 新規エントリを作成
| フィールド | 入力内容 | 理由 |
|---|---|---|
| Name | World Rules | 自分用の識別名 |
| Section | System 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が返す中、メッセージテキストの下に追加するだけ:
// 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:保存してテスト
- ワールドを保存、新規セッションを開始
- 最後のメッセージの下に「World Rule: (not set)」と入力ボックスが見える
- 「Magic is forbidden」と入力して「Apply」をクリック(またはEnterキー)
- 入力の上のテキストが「World Rule: Magic is forbidden」に変わる — 変数が更新された
- 今度はメッセージを送信(例:「I try to cast a fireball」)— このときエンジンがプロンプトをビルドし、
{{custom_rule}}を「Magic is forbidden」に置換 - AIの応答はこのルールを反映するはず(例:「You raise your hand to cast, but your mana feels locked away by some unseen force」)
- ルールを再変更(例:「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ファイルをダウンロードしてインポートし、完全な体験を試してください:
インポート方法:
- Yumina → My Worlds → Create New World に移動
- エディタで「More Actions」→「Import Package」をクリック
- ダウンロードした
.jsonファイルを選択 - 全グリーティング、変数、ビヘイビア、Root Componentが事前設定されたワールドが作成されます
- 新規セッションを開始して試す
含まれるもの:
- 3つのグリーティング(メインオープニング + 暗い洞窟 + 日当たりの良い草原)
- 2つの変数(ルート追跡用
current_route、プレイヤー編集可能ルール用custom_rule) - 2つのアクションビヘイビア(ルートが選ばれたらロアエントリを切り替え)
- Root Component(ルート選択ボタン + ルールエディタを持つ
<Chat renderBubble>) {{custom_rule}}マクロを使うロアエントリ
これはRecipe #1です
さらにレシピが来ます — 戦闘システム、ショップインターフェース、クエスト追跡、その他。各レシピは変数、エントリ、ビヘイビア、UIを組み合わせて部分の和より大きなものを構築します。
