Skip to content

マップとシーンナビゲーション

クリック可能なマップインターフェースを構築しましょう — プレイヤーが場所をクリック → シーンが切り替わり、ロアエントリが入れ替わり、BGMがクロスフェードし、AIが新しいエリアの雰囲気を記述します。このレシピでは、変数、ビヘイビア、ロアエントリ、Root Componentですべてを配線する方法を示します。


これから作るもの

チャット内に組み込まれたビジュアルマップナビゲーションシステム:

  • マップUI — 最後のメッセージの下にグリッドレイアウトのマップパネル、各場所が絵文字アイコンボタン
  • 現在地ハイライト — プレイヤーの現在地ボタンは別の色で即座に識別可能
  • シーン切り替え — 場所をクリック → ビヘイビアが発火 → ロアエントリが入れ替わる(古い場所無効、新しい場所有効) → AIが新しいエリアへの到着を記述
  • BGMクロスフェード — 各場所が独自のBGMを持ち、切り替えはクロスフェード(クロスディゾルブ)でスムーズな遷移、突然のカットではない
  • 4つの場所 — Village、Forest、Cave、Market、それぞれにユニークな雰囲気の説明とBGM

仕組み

システム全体の核心は:ボタンがビヘイビアをトリガー → ビヘイビアが変数を更新 + エントリを切り替え + ミュージックをクロスフェード + AI返信を要求 → AIが新しいシーンを記述

プレイヤーがマップの「Forest」ボタンをクリック
  → コードが api.executeAction("go-forest") を呼ぶ
  → ビヘイビアが発火:
    1. current_location を "forest" に設定
    2. 「Village Atmosphere」エントリを無効化、「Forest Atmosphere」エントリを有効化
    3. 森のBGMにクロスフェード
    4. 「プレイヤーが村から森へ移動」というコンテキストでAI返信を要求
  → AIが新しいロアエントリ + コンテキストを受け取る → 森のシーンを記述
  → Root Componentがcurrent_locationの変化を検出 → マップ上の「Forest」ボタンがハイライトされる

クロスフェードとは? クロスフェードはオーディオの遷移技法です — 古いトラックが徐々にフェードアウトする一方、新しいトラックが徐々にフェードインし、両方が短い重なりで同時に再生されます。効果は映画のシーン遷移のよう:ミュージックが突然途切れて再開されるのではなく、ある楽曲から次の楽曲へとスムーズに流れます。Yuminaでは、「Play Music」ビヘイビアアクションがcrossfadeオペレーションをサポート — 新しいトラックIDとフェード期間を指定するだけです。


ステップバイステップ

ステップ1:変数の作成

プレイヤーの現在地を追跡する変数が1つ必要です。

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

フィールド理由
Display NameCurrent Location自分用の識別名
IDcurrent_locationビヘイビアとRoot ComponentがこのIDで読み書き
TypeString値がテキスト("village""forest""cave""market")だから
Default Valuevillage新規セッションは村から開始
CategoryCustomマップシステム専用カテゴリ
Behavior RulesDo not modify this variable. It is controlled by the player's map UI. The current value represents the player's location.AIに場所を自分で変更しないよう伝える — プレイヤーのマップクリックのみ可能

ステップ2:4つの場所のロアエントリを作成

各場所にその雰囲気を記述するロアエントリが必要です。デフォルトで有効なのは「Village」のみで、他の3つは無効です。

エディタ → Lore タブ → エントリを1つずつ作成

エントリ1:Village Atmosphere

フィールド理由
NameVillage Atmosphere自分用の識別名
SectionSystem Presetsプリセットセクションのエントリは毎回AIに送信される
EnabledYes(トグルオン)ゲームが村から始まるためデフォルトで有効

内容:

[Current Location: Village]
The player is in the Village. When describing the scene, convey the following atmosphere:
- A peaceful little village with cobblestone paths winding between wooden houses
- Wisps of smoke rise from rooftops; the air carries the scent of fresh bread and stew
- Villagers chat by the well; rhythmic hammer strikes ring out from the blacksmith's shop
- Golden wheat fields stretch into the distance, swaying gently in the breeze
- The overall mood is warm, tranquil, and full of everyday life

エントリ2:Forest Atmosphere

フィールド理由
NameForest Atmosphere自分用の識別名
SectionSystem Presetsプリセットセクション
EnabledNo(トグルオフ)プレイヤーがここに来たときビヘイビアが有効化

内容:

[Current Location: Forest]
The player is in the Forest. When describing the scene, convey the following atmosphere:
- Towering ancient trees block most of the sunlight; only dappled light filters through onto the moss below
- The air is damp and fresh, a mix of earth, tree resin, and wildflower scents
- Birdsong comes from every direction, punctuated by the occasional sharp crack of a snapping branch
- Bushes along the trail might conceal rabbits, deer, or something more dangerous
- The deeper you go, the denser the trees and the dimmer the light
- The overall mood is mysterious, primal, and full of the unknown

エントリ3:Cave Atmosphere

フィールド理由
NameCave Atmosphere自分用の識別名
SectionSystem Presetsプリセットセクション
EnabledNo(トグルオフ)ビヘイビアが有効化

内容:

[Current Location: Cave]
The player is in the Cave. When describing the scene, convey the following atmosphere:
- Bioluminescent fungi cling to the rock walls, casting a faint blue-green glow
- Water drips from stalactites, each drop echoing through the cavern
- The air is cold and damp, carrying a metallic scent of minerals and underground streams
- The ground underfoot is slippery and uneven; tunnels deeper in are pitch black
- Occasionally, an unidentifiable low growl or the crack of shifting rock reverberates from the depths — it may not be safe down here
- The overall mood is dark, oppressive, and laced with hidden danger

エントリ4:Market Atmosphere

フィールド理由
NameMarket Atmosphere自分用の識別名
SectionSystem Presetsプリセットセクション
EnabledNo(トグルオフ)ビヘイビアが有効化

内容:

[Current Location: Market]
The player is in the Market. When describing the scene, convey the following atmosphere:
- Colorful tents and stalls line up in rows, brimming with every kind of goods
- Merchants shout their wares; the sounds of haggling rise and fall on all sides
- The air is a blend of spices, roasting meat, leather, and flowers
- A magic-item shop's display window flickers with strange light; an alchemist mixes potions in a corner
- Crowds bustle through — travelers of every race and profession converge here
- The overall mood is lively, bustling, and full of commercial energy

なぜデフォルトで「Village」だけが有効なのか? ゲームが村から始まるからです。4つのエントリすべてが同時に有効だと、AIは村、森、洞窟、市場の雰囲気記述を同時に受け取り、どのシーンを記述すべきかわかりません。一度に1つだけ有効にすることで、AIを現在地に焦点を合わせ続けます。


ステップ3:(オプション)場所のBGMをアップロード

各場所に独自のバックグラウンドミュージックが欲しい場合、まずオーディオファイルをアップロードする必要があります。

エディタ → Audio タブ → トラックを追加

トラックID名前タイプループフェードインフェードアウト
bgm_villageVillageBGMYes2s2s
bgm_forestForestBGMYes2s2s
bgm_caveCaveBGMYes2s2s
bgm_marketMarketBGMYes2s2s

オーディオファイルがない? このステップをスキップ。マップナビゲーションの核はロアエントリの切り替えです — BGMはおまけ。後でいつでも追加できます。

BGMプレイリストで、autoPlaytrueに設定し、デフォルトをbgm_villageに。プレイヤーが場所を切り替えると、ビヘイビアがcrossfadeアクションを使ってトラック間をスムーズに遷移します。

クロスフェードの仕組み

単純な「古いトラック停止 → 新しいトラック再生」は不快なギャップを残します — ミュージックが途切れて、別の楽曲が突然始まる。クロスフェードは異なります:古いトラックと新しいトラックがある時間ウィンドウで重なります。3秒のフェード期間を設定したとしましょう:

  • 0秒:古いトラック100%音量、新しいトラックが0%で再生開始
  • 1.5秒:古いトラック50%、新しいトラック50%
  • 3秒:古いトラック0%(停止)、新しいトラック100%

効果はパレット上の2つの色がゆっくり混ざり合いそして分離するよう — 遷移が滑らかで、プレイヤーはトラックが変わったことにほとんど気づかず、雰囲気が自然にシフトするだけです。


ステップ4:ビヘイビアの作成

各場所には1つのビヘイビアが必要です — プレイヤーがマップボタンをクリックすると、対応するビヘイビアが発火し、場所切り替えのすべてを処理します。

エディタ → Behaviors タブ → ビヘイビアを1つずつ追加

ビヘイビア1:Go to Village

WHEN(トリガー):

フィールド理由
Trigger TypeActionRoot ComponentのコードがexecuteAction("go-village")を呼ぶと発火
Action IDgo-villageマップボタンのクリックイベントに対応

DO(アクション):

#Action Type設定目的
1Modify Variablecurrent_locationvillageに設定現在地を更新
2Disable Lore EntryForest Atmosphere他の場所エントリをオフ
3Disable Lore EntryCave Atmosphere他の場所エントリをオフ
4Disable Lore EntryMarket Atmosphere他の場所エントリをオフ
5Enable Lore EntryVillage Atmosphereターゲット場所のエントリをオン
6Play Musicbgm_village、オペレーション:crossfade、フェード期間 3sVillageのBGMにクロスフェード
7Request AI Replyコンテキスト:The player has returned to the Village. Describe the scene the player sees upon arriving.AIに到着の記述を生成させる

なぜ最初に他の3つを無効化してからターゲットを有効化するのか? プレイヤーがどの場所から来ても良いからです。森から村に向かう場合、森のエントリをオフにする必要があります;洞窟から来る場合、洞窟のエントリをオフにする必要があります。最もシンプルなアプローチは、常に他のすべての場所をオフにし、次にターゲットをオンにする — プレイヤーがどこから来ても正しい結果が得られます。

ビヘイビア2:Go to Forest

WHEN:

フィールド
Trigger TypeAction
Action IDgo-forest

DO:

#Action Type設定目的
1Modify Variablecurrent_locationforestに設定現在地を更新
2Disable Lore EntryVillage Atmosphere他の場所をオフ
3Disable Lore EntryCave Atmosphere他の場所をオフ
4Disable Lore EntryMarket Atmosphere他の場所をオフ
5Enable Lore EntryForest Atmosphereターゲット場所をオン
6Play Musicbgm_forest、オペレーション:crossfade、フェード期間 3sBGMをクロスフェード
7Request AI Replyコンテキスト:The player has entered the Forest. Describe the scene the player sees as they step into the forest.AIが到着シーンを記述

ビヘイビア3:Go to Cave

WHEN:

フィールド
Trigger TypeAction
Action IDgo-cave

DO:

#Action Type設定目的
1Modify Variablecurrent_locationcaveに設定現在地を更新
2Disable Lore EntryVillage Atmosphere他の場所をオフ
3Disable Lore EntryForest Atmosphere他の場所をオフ
4Disable Lore EntryMarket Atmosphere他の場所をオフ
5Enable Lore EntryCave Atmosphereターゲット場所をオン
6Play Musicbgm_cave、オペレーション:crossfade、フェード期間 3sBGMをクロスフェード
7Request AI Replyコンテキスト:The player has entered the Cave. Describe the scene the player sees as they step inside.AIが到着シーンを記述

ビヘイビア4:Go to Market

WHEN:

フィールド
Trigger TypeAction
Action IDgo-market

DO:

#Action Type設定目的
1Modify Variablecurrent_locationmarketに設定現在地を更新
2Disable Lore EntryVillage Atmosphere他の場所をオフ
3Disable Lore EntryForest Atmosphere他の場所をオフ
4Disable Lore EntryCave Atmosphere他の場所をオフ
5Enable Lore EntryMarket Atmosphereターゲット場所をオン
6Play Musicbgm_market、オペレーション:crossfade、フェード期間 3sBGMをクロスフェード
7Request AI Replyコンテキスト:The player has arrived at the Market. Describe the scene the player sees as they walk in.AIが到着シーンを記述

4つのビヘイビアすべてがまったく同じ構造に従う — ターゲット場所だけが異なる。 各ビヘイビアは3つのことを行う:(1) 変数を更新 → (2) エントリを入れ替え + ミュージックをクロスフェード → (3) AIに新しいシーンを記述させる。パターンが統一されているため、後で新しい場所を追加するのは1つのビヘイビアをコピーしてパラメータを微調整するだけです。

なぜ「Tell AI」ではなく「Request AI Reply」?

「Tell AI」はコンテキストに隠しテキストを注入するだけ — AIは即座に応答しません。プレイヤーが次のメッセージを送るまで待ちます。「Request AI Reply」はすぐにAI返信をトリガーし、テキストをその返信のバックグラウンドコンテキストとして使います。マップナビゲーションでは、プレイヤーがボタンをクリックした瞬間にAIが新しいシーンを記述するのを見たいのです、別のメッセージを送る必要なく。それが「Request AI Reply」がここでより適している理由です。


ステップ5:Root Componentにマップパネルを追加

これがチャットインターフェースにマップUIを表示させるステップです。スタイル付きのdivボタンと絵文字アイコンを使ってシンプルな「マップ」を作成 — 画像アセット不要です。

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

tsx
export default function MyWorld() {
  const api = useYumina();

  // ---- Read variable ----
  const currentLocation = String(api.variables.current_location || "village");

  // ---- Location configs ----
  const locations = [
    { id: "village", label: "Village", icon: "🏘️", action: "go-village",
      color: "#92400e", bg: "#fef3c7", border: "#f59e0b",
      activeBg: "#f59e0b", activeColor: "#ffffff" },
    { id: "forest",  label: "Forest", icon: "🌲", action: "go-forest",
      color: "#166534", bg: "#dcfce7", border: "#22c55e",
      activeBg: "#22c55e", activeColor: "#ffffff" },
    { id: "cave",    label: "Cave", icon: "🕳️", action: "go-cave",
      color: "#3b0764", bg: "#f3e8ff", border: "#a855f7",
      activeBg: "#a855f7", activeColor: "#ffffff" },
    { id: "market",  label: "Market", icon: "🏪", action: "go-market",
      color: "#9a3412", bg: "#ffedd5", border: "#f97316",
      activeBg: "#f97316", activeColor: "#ffffff" },
  ];

  // ---- Message list, used to find the last one ----
  const msgs = api.messages || [];

  return (
    <Chat renderBubble={(msg) => {
      const isLastMsg = msg.messageIndex === msgs.length - 1;
      return (
    <div>
      {/* Render message text normally (platform already produced HTML — just use contentHtml) */}
      <div
        style={{ color: "#e2e8f0", lineHeight: 1.7 }}
        dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
      />

      {/* Map panel — only on the last message */}
      {isLastMsg && (
        <div style={{
          marginTop: "16px",
          padding: "16px",
          background: "rgba(15,23,42,0.6)",
          borderRadius: "12px",
          border: "1px solid #334155",
        }}>
          {/* Title */}
          <div style={{
            fontSize: "13px",
            color: "#94a3b8",
            marginBottom: "12px",
            fontWeight: "600",
            letterSpacing: "0.05em",
          }}>
            WORLD MAP
          </div>

          {/* 2x2 grid layout */}
          <div style={{
            display: "grid",
            gridTemplateColumns: "1fr 1fr",
            gap: "10px",
          }}>
            {locations.map((loc) => {
              const isActive = currentLocation === loc.id;
              return (
                <button
                  key={loc.id}
                  onClick={() => {
                    if (!isActive) {
                      api.executeAction(loc.action);
                    }
                  }}
                  style={{
                    padding: "14px 10px",
                    background: isActive
                      ? loc.activeBg
                      : loc.bg,
                    border: `2px solid ${isActive ? loc.activeBg : loc.border}`,
                    borderRadius: "10px",
                    color: isActive ? loc.activeColor : loc.color,
                    fontSize: "14px",
                    fontWeight: "700",
                    cursor: isActive ? "default" : "pointer",
                    opacity: isActive ? 1 : 0.85,
                    display: "flex",
                    flexDirection: "column",
                    alignItems: "center",
                    gap: "6px",
                    transition: "all 0.2s ease",
                  }}
                >
                  <span style={{ fontSize: "28px" }}>{loc.icon}</span>
                  <span>{loc.label}</span>
                  {isActive && (
                    <span style={{
                      fontSize: "11px",
                      opacity: 0.9,
                      fontWeight: "500",
                    }}>
                      You are here
                    </span>
                  )}
                </button>
              );
            })}
          </div>
        </div>
      )}
    </div>
      );
    }} />
  );
}

行ごとの解説:

  • api.variables.current_location — 現在地変数の値を読む
  • locations — 各場所のID、英語ラベル、絵文字アイコン、ビヘイビアアクションID、通常状態とハイライト状態の色を定義する設定配列。新しい場所を追加するには、配列にエントリを追加するだけ
  • isLastMsg — マップは最後のメッセージにのみ表示、各メッセージで表示しない
  • isActive — このボタンが現在地と一致するか確認。一致する場合、ボタンはハイライト色と「You are here」を表示
  • executeActionを呼ぶ前に!isActiveをチェック — プレイヤーが現在地を繰り返しクリックすることを防ぐ。すでに村にいる場合、村を再クリックしても何も起きない
  • gridTemplateColumns: "1fr 1fr" — 2列等幅グリッドレイアウト、4つのボタンが2x2グリッドに配置
  • transition: "all 0.2s ease" — ホバー時の微妙なアニメーション

異なるレイアウトが欲しい?

3列ならgridTemplateColumns"1fr 1fr 1fr"に、単一列の縦スタックなら"1fr"に変更してください。gapはボタン間の間隔を制御します。レイアウトは完全にCSS Gridで制御 — 好きなように調整してください。


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

  1. エディタ上部のSaveをクリック
  2. Start Gameをクリックするか、ホームページに戻って新規セッションを開始
  3. AIのメッセージの下に4つの場所ボタンを持つマップパネルが見える。「Village」ボタンがハイライトされ「You are here」と表示
  4. Forestをクリック — AIが森のシーンを記述するパッセージで即座に応答し、マップの「Forest」ボタンがハイライトされる
  5. BGMを設定した場合、ミュージックが村のトラックから森のトラックにクロスフェードするのが聞こえる
  6. Caveをクリック — シーンが再び切り替わり、AIが洞窟を記述、BGMがクロスフェード
  7. 現在ハイライトされている場所をクリック試行 — 何も起きない(すでにそこにいる)
  8. AIと普通にチャットしてから場所を切り替え — すべて動作;マップは最後のメッセージの下に留まる

うまく動かない場合:

症状想定される原因対処
マップパネルが見えないRoot Componentコードが保存されていない、または構文エラーCustom UIパネル下部のコンパイル状態を確認 — 緑の「OK」が表示されるべき
ボタンをクリックしても何も起きないビヘイビアアクションIDが一致しないビヘイビアアクションID(go-villageなど)がコード内のlocations配列のactionフィールドと正確に一致することを確認
AIが新しいシーンで応答しないビヘイビアに「Request AI Reply」アクションが欠落各ビヘイビアの最後のアクションが「Request AI Reply」であることを確認
マップのハイライトが変わらない変数が更新されていない各ビヘイビアの最初のアクションがcurrent_locationをターゲットにする「Modify Variable」であることを確認
BGMが切り替わらないトラックID不一致またはオーディオがアップロードされていないビヘイビアのトラックIDがAudioタブのトラックIDと一致することを確認
BGM遷移が不快に聞こえるクロスフェードを使っていない「Play Music」アクションのオペレーションがcrossfadeで、フェード期間が少なくとも2-3秒であることを確認
4つすべてのエントリが一度に有効になるビヘイビアが他のエントリの無効化を忘れた各ビヘイビアはターゲット場所のエントリを有効にする前に他の3つの場所エントリを無効にする必要がある

拡張アイデア

場所を追加

5番目の場所(たとえば「Harbor」)を追加したい?4つのことを行う:

  1. Lore タブ → 「Harbor Atmosphere」エントリを作成(デフォルトで無効)
  2. Audio タブ → bgm_harborトラックを作成(オプション)
  3. Behaviors タブ → アクションIDgo-harborで「Go to Harbor」ビヘイビアを作成、他の4つと同じアクションパターン。また既存の4つのビヘイビアに戻って「Disable Lore Entry: Harbor Atmosphere」アクションを追加
  4. Root Componentlocations配列にエントリを追加:
tsx
{ id: "harbor", label: "Harbor", icon: "⚓", action: "go-harbor",
  color: "#1e40af", bg: "#dbeafe", border: "#3b82f6",
  activeBg: "#3b82f6", activeColor: "#ffffff" },

グリッドレイアウトは自動的に適応 — 5つのボタンは最初の行に2つ、2行目に2つ、3行目に1つと配置されます。

移動ルートの制限

任意の2つの場所間を自由にジャンプさせたくない場合(例:「洞窟に到達するには森を通る必要がある」)、Root Componentにルートロジックを追加:

tsx
// Define reachable routes
const routes = {
  village: ["forest", "market"],       // Village can reach Forest and Market
  forest:  ["village", "cave"],        // Forest can reach Village and Cave
  cave:    ["forest"],                 // Cave can only go back to Forest
  market:  ["village"],                // Market can only go back to Village
};

const reachable = routes[currentLocation] || [];

// Add checks to the button's onClick and style
const canGo = reachable.includes(loc.id);
// ...
onClick={() => {
  if (!isActive && canGo) {
    api.executeAction(loc.action);
  }
}}
style={{
  // ...
  opacity: isActive ? 1 : canGo ? 0.85 : 0.3,
  cursor: isActive ? "default" : canGo ? "pointer" : "not-allowed",
}}

到達不能な場所はフェードしてクリック不可になる — プレイヤーは「今そこには行けない」と一目でわかります。


クイックリファレンス

やりたいこと方法
プレイヤーの現在地を追跡文字列変数current_locationに場所IDを値として保存
ボタンクリックでシーンを切り替えビヘイビアトリガーを「Action」に設定、アクションIDがRoot ComponentのexecuteAction()と一致
場所の雰囲気を入れ替えビヘイビアアクション:「Disable Lore Entry」で古い場所をオフ、「Enable Lore Entry」で新しい場所をオン
スムーズなBGM遷移ビヘイビアアクション「Play Music」、オペレーションをcrossfadeに設定、フェード期間2-3秒
クリック時にAIがすぐに新シーンを記述ビヘイビアアクション「Request AI Reply」と到着コンテキスト
現在地をハイライトRoot Component内でcurrent_locationをボタンIDと比較、一致するボタンにハイライトスタイル
現在地の再クリックを防ぐexecuteActionを呼ぶ前にif (!isActive)チェック
マップを最後のメッセージにのみ表示<Chat renderBubble>内でmsg.messageIndex === msgs.length - 1をチェック

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

このJSONをダウンロードし、新規ワールドとしてインポートしてすべての動作を確認してください:

recipe-12-demo.json

インポート方法:

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

含まれるもの:

  • 1つの変数(current_locationが現在地を追跡)
  • 4つのロアエントリ(Village / Forest / Cave / Market の雰囲気 — デフォルトでVillageのみ有効)
  • 4つのビヘイビア(Villageに行く / Forestに行く / Caveに行く / Marketに行く — 各々がエントリを入れ替え + ミュージックをクロスフェード + AI記述を要求)
  • Root Component(現在地ハイライト付き2x2グリッドマップパネル)
  • 4つのBGMトラック(プレースホルダーURLを置き換えるために独自のオーディオファイルをアップロードする必要あり)

これはRecipe #12です

このレシピは古典的なビヘイビア + Root Component の組み合わせを示しています — ボタンがビヘイビアをトリガーし、各ビヘイビアが同時に変数を更新し、ロアエントリを入れ替え、BGMをクロスフェードし、AI返信を要求します。同じパターンが、フロアナビゲーション、部屋探索、ワールドポータルなど、「複数のシーン間を移動する」あらゆるものに使えます。