ショップとトレード
ショップUIを構築しましょう — プレイヤーがアイテムを閲覧、クリックして購入、ゴールドが自動的に差し引かれ、アイテムがそのままインベントリに入ります。このレシピでは、変数、ビヘイビア、Root Componentを完全なトレーディングシステムに組み合わせる方法を示します。
これから作るもの
チャットインターフェースに組み込まれたショップパネル。プレイヤーは自分のゴールド量、販売中のアイテム、各アイテムの価格を見られます。「Buy」ボタンをクリックすると:
- ゴールドがアイテムの価格分自動的に減る
- アイテムがインベントリに追加(JSON配列)
- 「Purchase successful!」通知がポップアップ
- ゴールドが不足している場合、「Not enough gold!」警告が表示 — ゴールドは差し引かれず、アイテムも追加されない
下にはプレイヤーのバッグ内のすべてのアイテムをリアルタイムで表示するインベントリグリッドもあります。
プレイヤーが「Buy Potion (20 gold)」をクリック
→ ビヘイビアがチェック:gold >= 20?
→ はい:gold が 20 減る、inventory に "Potion" を push、成功通知を表示
→ いいえ:「Not enough gold!」警告を表示仕組み
このショップシステムは3つのコアメカニズムを組み合わせます:
- 数値変数 + 条件チェック — ゴールドは数値変数。ビヘイビアが実行前に十分かチェック。
- JSON変数 + push操作 — インベントリはJSON配列。各購入で
pushを使ってアイテムを追加。 - アクショントリガー — 各購入ボタンはアクションIDに対応。Root Component内のボタンが
executeAction()を呼んでビヘイビアをトリガー。
完全な流れ:
Root Component ボタン UI
→ プレイヤーが「Buy Potion」をクリック
→ api.executeAction("buy-potion") を呼ぶ
→ エンジンが action ID "buy-potion" を持つビヘイビアを見つける
→ 条件をチェック:gold >= 20?
→ パス → アクション実行:変数変更(gold -20)、変数変更(inventory に "Potion" push)、通知表示
→ 失敗 → 何もしない(「ゴールド不足」メッセージは別のビヘイビアで処理)ステップバイステップ
ステップ1:変数の作成
2つの変数が必要です — 1つはゴールドを追跡、1つはインベントリの内容を追跡。
エディタ → サイドバー → Variables タブ → Add Variable をクリック
変数1:Gold
| フィールド | 値 | 理由 |
|---|---|---|
| Name | Gold | エディタ内で自分用の識別 |
| ID | gold | コードとビヘイビアでこの変数を読み書きするのに使用 |
| Type | Number | ゴールドは数値 — 算術操作が必要 |
| Default Value | 100 | 新規セッションでプレイヤーは100ゴールドで開始 |
| Min Value | 0 | ゴールドがマイナスにならないよう防ぐ — エンジンがクランプする |
| Category | Resources | ゴールドはリソースタイプ変数 |
| Behavior Rules | Gold is automatically deducted when the player buys items from the shop. You may also increase or decrease gold in the story — e.g., quest rewards, getting robbed by thieves, or finding a treasure chest. | ショップだけでなくストーリー中もゴールドが変化できることをAIに伝える |
なぜ最小値を0に設定するのか? ビヘイビアの条件で「プレイヤーが買えるか?」をすでにチェックしますが、エンジンレベルの保護を追加するのは安全です。何かがすり抜けても、ゴールドはマイナスになりません。
変数2:Inventory
| フィールド | 値 | 理由 |
|---|---|---|
| Name | Inventory | 自分用の識別名 |
| ID | inventory | コードとビヘイビアで使用 |
| Type | JSON | インベントリは配列 — 保存にJSONタイプが必要 |
| Default Value | [] | 空の配列 — 新規セッションでインベントリは空で開始 |
| Category | Inventory | これはインベントリタイプ変数 |
| Behavior Rules | Items are automatically added when bought from the shop. You may also add or remove items in the story — e.g., the player picks something up, an item breaks, gets stolen, or is received as a quest reward. | ショップだけでなくストーリー中もインベントリが変化できることをAIに伝える |
JSON変数は任意のJSONデータ構造を保存できます。 ここでは配列(
[])を使ってアイテム名のリストを保持します。各購入でpushを使って配列の末尾に文字列を追加。例えば、ポーション購入後の値は[]から["Potion"]になり、その後アイアンソードを買うと["Potion", "Iron Sword"]になります。
ステップ2:ショップビヘイビアを作成
複数のビヘイビアが必要です — 各アイテムに「購入成功」と「ゴールド不足」のビヘイビア。ここではポーションとアイアンソードを例にします。
エディタ → Behaviors タブ → Add Behavior をクリック
ビヘイビア1:Buy Potion(成功)
WHEN(トリガー):
| フィールド | 値 | 理由 |
|---|---|---|
| Trigger Type | Action button pressed | Root ComponentがexecuteAction("buy-potion")を呼ぶと発火 |
| Action ID | buy-potion | Root Componentコード内のexecuteAction("buy-potion")呼び出しと一致する必要がある |
ONLY IF(条件):
| 変数 | 演算子 | 値 | 理由 |
|---|---|---|---|
gold | Greater than or equal (gte) | 20 | ポーションは20ゴールド — 十分にあるときだけ買える |
DO(アクション):
以下のアクションを順番に追加:
| Action Type | 設定 | 効果 |
|---|---|---|
| Modify Variable | 変数gold、オペレーションsubtract、値20 | 20ゴールド差し引く |
| Modify Variable | 変数inventory、オペレーションpush、値"Potion" | インベントリ配列に「Potion」を追加 |
| Show Notification | メッセージPurchase successful! You got a Potion.、スタイルachievement | 金色の成功通知を表示 |
push操作はJSON配列専用です。 既存の内容を上書きせずに配列の末尾に要素を追加します。そのため、ポーションを買うたびに、別の
"Potion"文字列がインベントリに追加されます。
ビヘイビア2:Buy Potion(ゴールド不足)
このビヘイビアは同じアクションIDをリッスンしますが、条件は「ゴールドが足りない」です。
WHEN:
| フィールド | 値 |
|---|---|
| Trigger Type | Action button pressed |
| Action ID | buy-potion |
ONLY IF:
| 変数 | 演算子 | 値 | 理由 |
|---|---|---|---|
gold | Less than (lt) | 20 | ゴールドが20未満 — 買えない |
DO:
| Action Type | 設定 | 効果 |
|---|---|---|
| Show Notification | メッセージNot enough gold! The potion costs 20 gold.、スタイルwarning | 黄色の警告通知を表示 |
なぜ2つの別々のビヘイビアか? 1つのビヘイビアは1セットの条件しか持てないからです。条件が通ればアクションが実行、失敗すれば何も起きない。そのため2つのビヘイビアを使って両方のケースをカバーします:十分なゴールド → 購入成功;不足 → 警告表示。同じアクションIDをリッスンしますが、条件は相互排他的、どちらか一方だけ発火します。
ビヘイビア3:Buy Iron Sword(成功)
WHEN:
| フィールド | 値 |
|---|---|
| Trigger Type | Action button pressed |
| Action ID | buy-sword |
ONLY IF:
| 変数 | 演算子 | 値 |
|---|---|---|
gold | Greater than or equal (gte) | 50 |
DO:
| Action Type | 設定 | 効果 |
|---|---|---|
| Modify Variable | 変数gold、オペレーションsubtract、値50 | 50ゴールド差し引く |
| Modify Variable | 変数inventory、オペレーションpush、値"Iron Sword" | インベントリ配列に「Iron Sword」を追加 |
| Show Notification | メッセージPurchase successful! You got an Iron Sword.、スタイルachievement | 金色の成功通知を表示 |
ビヘイビア4:Buy Iron Sword(ゴールド不足)
WHEN:
| フィールド | 値 |
|---|---|
| Trigger Type | Action button pressed |
| Action ID | buy-sword |
ONLY IF:
| 変数 | 演算子 | 値 |
|---|---|---|
gold | Less than (lt) | 50 |
DO:
| Action Type | 設定 | 効果 |
|---|---|---|
| Show Notification | メッセージNot enough gold! The iron sword costs 50 gold.、スタイルwarning | 黄色の警告通知を表示 |
もっとアイテムを追加したい?
パターンを繰り返すだけ — アイテムごとに2つのビヘイビア(成功 + 不足)、アクションID、価格、アイテム名を変更。例えば、30ゴールドの「Shield」を追加するには:アクションIDbuy-shield、条件gold gte 30、アクションsubtract 30 + push "Shield"。
ステップ3:Root Componentにショップパネルを追加
これがチャット内にショップUIを表示させる重要なステップです。各メッセージの下に3つのエリアを表示:ゴールド残高、アイテムリスト(購入ボタン付き)、インベントリグリッド。
エディタ → Custom UI セクション → index.tsxを開く → 以下のコードを貼り付け(デフォルトのreturn <Chat />を置き換え):
export default function MyWorld() {
const api = useYumina();
const msgs = api.messages || [];
// Read variables
const gold = Number(api.variables.gold ?? 100);
const inventory = Array.isArray(api.variables.inventory)
? api.variables.inventory
: [];
// Shop item definitions
const shopItems = [
{ name: "Potion", price: 20, actionId: "buy-potion", icon: "\u{1F9EA}", desc: "Restores a small amount of health" },
{ name: "Iron Sword", price: 50, actionId: "buy-sword", icon: "⚔️", desc: "A plain iron sword" },
];
return (
<Chat renderBubble={(msg) => {
const isLastMsg = msg.messageIndex === msgs.length - 1;
return (
<div>
{/* Render message text normally (the platform already rendered HTML, use contentHtml directly) */}
<div
style={{ color: "#e2e8f0", lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
/>
{/* Only show the shop below the last message */}
{isLastMsg && (
<div style={{
marginTop: "16px",
padding: "16px",
background: "rgba(15, 23, 42, 0.6)",
borderRadius: "12px",
border: "1px solid #334155",
}}>
{/* ====== Gold display ====== */}
<div style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "16px",
padding: "10px 14px",
background: "linear-gradient(135deg, #78350f, #92400e)",
borderRadius: "8px",
border: "1px solid #b45309",
}}>
<span style={{ fontSize: "20px" }}>{"💰"}</span>
<span style={{ color: "#fde68a", fontSize: "16px", fontWeight: "bold" }}>
{gold} Gold
</span>
</div>
{/* ====== Shop heading ====== */}
<div style={{
fontSize: "14px",
fontWeight: "bold",
color: "#94a3b8",
marginBottom: "10px",
textTransform: "uppercase",
letterSpacing: "1px",
}}>
Shop
</div>
{/* ====== Item list ====== */}
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginBottom: "16px" }}>
{shopItems.map((item) => (
<div
key={item.actionId}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "10px 14px",
background: "rgba(30, 41, 59, 0.8)",
borderRadius: "8px",
border: "1px solid #475569",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<span style={{ fontSize: "22px" }}>{item.icon}</span>
<div>
<div style={{ color: "#e2e8f0", fontSize: "14px", fontWeight: "600" }}>
{item.name}
</div>
<div style={{ color: "#64748b", fontSize: "12px" }}>
{item.desc}
</div>
</div>
</div>
<button
onClick={() => api.executeAction(item.actionId)}
style={{
padding: "6px 16px",
background: gold >= item.price
? "linear-gradient(135deg, #065f46, #047857)"
: "linear-gradient(135deg, #374151, #4b5563)",
border: gold >= item.price
? "1px solid #10b981"
: "1px solid #6b7280",
borderRadius: "6px",
color: gold >= item.price ? "#a7f3d0" : "#9ca3af",
fontSize: "13px",
fontWeight: "600",
cursor: gold >= item.price ? "pointer" : "not-allowed",
opacity: gold >= item.price ? 1 : 0.6,
whiteSpace: "nowrap",
}}
>
{item.price} Gold
</button>
</div>
))}
</div>
{/* ====== Inventory heading ====== */}
<div style={{
fontSize: "14px",
fontWeight: "bold",
color: "#94a3b8",
marginBottom: "10px",
textTransform: "uppercase",
letterSpacing: "1px",
}}>
Inventory
</div>
{/* ====== Inventory grid ====== */}
{inventory.length === 0 ? (
<div style={{
padding: "20px",
textAlign: "center",
color: "#475569",
fontSize: "13px",
background: "rgba(30, 41, 59, 0.4)",
borderRadius: "8px",
border: "1px dashed #334155",
}}>
Inventory is empty
</div>
) : (
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(80px, 1fr))",
gap: "8px",
}}>
{inventory.map((item, idx) => (
<div
key={idx}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "10px 6px",
background: "rgba(30, 41, 59, 0.8)",
borderRadius: "8px",
border: "1px solid #475569",
gap: "4px",
}}
>
<span style={{ fontSize: "24px" }}>
{item === "Potion" ? "\u{1F9EA}" : item === "Iron Sword" ? "⚔️" : "📦"}
</span>
<span style={{ color: "#cbd5e1", fontSize: "11px", textAlign: "center" }}>
{String(item)}
</span>
</div>
))}
</div>
)}
</div>
)}
</div>
);
}} />
);
}コードウォークスルー
コードの長さに圧倒されないでください — 行うことは非常に簡単です。セクションごとに見ていきましょう:
基本セットアップ
const api = useYumina();
const msgs = api.messages || [];
// ...
<Chat renderBubble={(msg) => {
const isLastMsg = msg.messageIndex === msgs.length - 1;
// ...
}} />- Root Component
MyWorld()はワールドUIのエントリ。<Chat renderBubble={...} />はメッセージリスト、入力ボックス、スクロールをプラットフォームに任せ — 個々のバブルの見た目だけ引き継ぐ useYumina()— Yumina APIを取得して変数を読みアクションをトリガーできるmsg.messageIndex— 現在のバブルのメッセージリスト内のインデックス、最後かどうかチェックするのに使用。ショップパネルは最後のメッセージの下にのみ表示されるため、チャット内の各メッセージで繰り返さないmsg.contentHtml— プラットフォームがMarkdownからレンダリングしたHTML、dangerouslySetInnerHTMLに直接使える
変数の読み取り
const gold = Number(api.variables.gold ?? 100);
const inventory = Array.isArray(api.variables.inventory)
? api.variables.inventory
: [];api.variables.gold— ゴールド変数を読む。?? 100は変数がまだロードされていない場合のフォールバックapi.variables.inventory— インベントリ変数を読む。Array.isArray()を使って実際に配列かを確認、予期しないデータから保護
ショップアイテム定義
const shopItems = [
{ name: "Potion", price: 20, actionId: "buy-potion", icon: "\u{1F9EA}", desc: "Restores a small amount of health" },
{ name: "Iron Sword", price: 50, actionId: "buy-sword", icon: "⚔️", desc: "A plain iron sword" },
];すべてのアイテム情報を1つの配列に定義し、.map()でレンダリング。新しいアイテムを追加したい?配列に行を追加するだけ — そしてもちろん、エディタで対応するビヘイビアも作成。
購入ボタン
<button onClick={() => api.executeAction(item.actionId)}>
{item.price} Gold
</button>これが最も重要な行です。ボタンをクリックするとapi.executeAction("buy-potion")を呼び、エンジンがアクションID "buy-potion"を持つビヘイビアを見つけ、条件をチェックし、アクションを実行します。すべてのロジック(ゴールドチェック、差し引き、アイテム追加、通知表示)はビヘイビアで定義されています — ボタンはそれらをトリガーするだけです。
ボタンの視覚的フィードバック
background: gold >= item.price
? "linear-gradient(135deg, #065f46, #047857)" // affordable → green
: "linear-gradient(135deg, #374151, #4b5563)", // can't afford → gray
cursor: gold >= item.price ? "pointer" : "not-allowed",
opacity: gold >= item.price ? 1 : 0.6,ボタンの色、カーソルスタイル、不透明度がプレイヤーがアイテムを買えるかに応じて動的に変わります。買えるアイテムは緑のボタン;買えないものはグレーアウト。これは純粋な視覚的フィードバック — 実際の購入ロジックはビヘイビアの条件にあります。
インベントリグリッド
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(80px, 1fr))",
gap: "8px",
}}>
{inventory.map((item, idx) => (
<div key={idx} style={{ /* cell styles */ }}>
<span>{item === "Potion" ? "\u{1F9EA}" : item === "Iron Sword" ? "⚔️" : "📦"}</span>
<span>{String(item)}</span>
</div>
))}
</div>CSS Gridを使ってインベントリアイテムをレイアウト。auto-fill + minmax(80px, 1fr)がセルを利用可能な幅に適応 — 幅広いウィンドウは行ごとにより多くのアイテム、狭いウィンドウは少ない。各セルがアイテムのアイコンと名前を表示。
コードを書きたくない?Studio AIを使おう
エディタ上部 → Enter Studio をクリック → AI Assistantパネル → 欲しいものを説明、例えば「ゴールド表示、アイテムリスト、インベントリグリッドを持つショップUIを構築」 — AIがコードを生成します。
ステップ4:保存してテスト
- エディタ上部のSaveをクリック
- Start Gameをクリックするか、ホームページに戻って新規セッションを開く
- AIの返信の下にショップパネルが見える:100ゴールド、2つのアイテム、空のインベントリ
- 20 Goldをクリックしてポーションを購入 — ゴールドが80に、ポーションアイコンがインベントリに表示、金色の通知に「Purchase successful! You got a Potion.」
- もう一度クリック — ゴールドが60に、今度はインベントリに2つのポーション
- 50 Goldをクリックしてアイアンソードを購入 — ゴールドが10に、インベントリに剣を獲得
- 今何かを買おうとする — 黄色の警告が「Not enough gold!」、ゴールドとインベントリは変わらず
- AIとチャットを続ける — ショップパネルは最新メッセージの下に留まり、リアルタイムで更新される
うまく動かない場合:
| 症状 | 想定される原因 | 対処 |
|---|---|---|
| ショップパネルが表示されない | Root Componentコードが保存されていない、または構文エラー | Custom UIセクション下部のコンパイル状態を確認 — 緑の「OK」が表示されるべき |
| ボタンがクリックに応答しない | ビヘイビアのアクションIDがコードと一致しない | ビヘイビアアクションIDがbuy-potion / buy-swordで、コード内のexecuteAction()引数と正確に一致することを確認 |
| ゴールドは引かれたがインベントリが変わらない | ビヘイビアのpushアクションが正しく設定されていない | modify variableアクションを確認:変数はinventory、オペレーションはpush、値は"Potion"(クォート付き)であるべき |
| ゴールド不足だが警告が表示されない | 「ゴールド不足」ビヘイビアの条件が反転 | 条件がgold lt 20(less than)で、gold gte 20ではないことを確認 |
| インベントリアイテムにアイコンが表示されない | アイテム名がコードのアイコンマッピングと一致しない | ビヘイビアのpush値がコードのアイコンマッピングと一致することを確認("Potion"は試験管絵文字にマップなど) |
| 購入後ゴールド表示が更新されない | 正常 — 次のメッセージでリフレッシュ | メッセージを送って再確認、または通知が表示されたか確認(表示されたなら購入成功) |
さらに進める:ショップシステムの拡張
基本が分かったら、同じパターンを使ってより複雑なシステムを構築できます。
アイテムを追加
Root ComponentのshopItems配列に行を追加:
const shopItems = [
{ name: "Potion", price: 20, actionId: "buy-potion", icon: "\u{1F9EA}", desc: "Restores a small amount of health" },
{ name: "Iron Sword", price: 50, actionId: "buy-sword", icon: "⚔️", desc: "A plain iron sword" },
{ name: "Shield", price: 30, actionId: "buy-shield", icon: "🛡️", desc: "Provides basic protection" },
{ name: "Magic Scroll", price: 80, actionId: "buy-scroll", icon: "📜", desc: "Unleashes a fireball spell" },
];そしてエディタのBehaviorsタブで、新しいアイテムごとに2つのビヘイビア(成功 + 不足)を作成、ポーションとアイアンソードと同じパターンに従う。
プレイヤーが何を買ったかAIに知らせる
AIのストーリーが購入に反応するようにしたい場合(例:アイアンソード購入後、AIはプレイヤーが武装していることを知る)、購入成功ビヘイビアに「Tell AI」アクションを追加:
| Action Type | 設定 |
|---|---|
| Tell AI | 内容:The player just bought an Iron Sword at the shop. Please reference this weapon in subsequent replies where appropriate. |
これによりAIのコンテキストに一時的な指示が注入され、何が起きたか知らせます。
ゴールドを稼ぐ
今プレイヤーはゴールドを使うことしかできず、稼ぐことはできません。ビヘイビアを使ってプレイヤーにゴールドを与えられます:
- ターンごとの報酬:トリガー「Every N turns」(例:3ターンごと)でビヘイビアを作成、アクションは
Modify Variable gold add 10。プレイヤーは3会話ラウンドごとに自動的に10ゴールドを稼ぐ。 - キーワード報酬:トリガー「AI said keyword」で「battle won」または「quest complete」のキーワードを使用。AIが返信でこれらの単語を言及すると、ゴールドが自動的に追加。
- 手動稼ぎボタン:Root Componentに「Work for Gold」ボタンを追加、
executeAction("earn-gold")でビヘイビアをトリガー、アクションはgold add 15。
クイックリファレンス
| やりたいこと | 方法 |
|---|---|
| ゴールドを追跡 | 数値変数を作成、カテゴリ:Resources |
| インベントリを追跡 | JSON変数を作成、デフォルト[]、カテゴリ:Inventory |
| 購入時にゴールド差し引き | ビヘイビアアクション:Modify Variable、オペレーションsubtract |
| 購入時にアイテム追加 | ビヘイビアアクション:Modify Variable、オペレーションpush |
| プレイヤーが買えるかチェック | ビヘイビア条件:gold gte price |
| 「ゴールド不足」警告を表示 | 別のビヘイビア、条件gold lt price、アクション:Show Notification(warning) |
| 「購入成功」アラートを表示 | ビヘイビアアクション:Show Notification(achievementスタイル) |
| ボタンが購入をトリガー | Root Componentでapi.executeAction("actionId")を呼ぶ |
| インベントリグリッドを表示 | Root ComponentでCSS Grid + inventory.map()を使用してレンダリング |
| アイテムを追加 | shopItems配列に行を追加 + エディタで2つのビヘイビアを作成 |
自分で試す — インポート可能なデモワールド
このJSONファイルをダウンロードし、インポートして完全なショップシステムを体験してください:
インポート方法:
- Yumina → My Worlds → Create New World に移動
- エディタで More Actions → Import Package をクリック
- ダウンロードした
.jsonファイルを選択 - 全変数、ビヘイビア、Root Componentが事前設定された新規ワールドが作成されます
- 新規セッションを開始して試す
含まれるもの:
- 2つの変数(
gold+inventory) - 4つのビヘイビア(ポーション購入成功/不足 + アイアンソード購入成功/不足)
- Root Component(ゴールド表示 + アイテムリスト + インベントリグリッド)
これはRecipe #3です
以前のレシピではシーンジャンプとエントリ変更をカバーしました。このレシピでは、変数条件チェック + JSON配列 + ビヘイビアアクションをインタラクティブシステムに組み合わせる方法を示します。同じパターンが、クエストシステム、戦闘システム、クラフトシステムなど、「条件チェック → リソース差し引き → アイテム追加 → フィードバックを与える」を必要とするあらゆるものに使えます。
