インベントリと装備
インベントリグリッドを構築しましょう — プレイヤーが集めた全アイテムをアイコンと数量付きで表示します。消耗品は使用可能(消耗品が尽きると消える)、装備品は身につけることができます。このレシピでは、JSON変数、Root Componentロジック、ビヘイビアを組み合わせて完全なインベントリシステムを構築する方法を示します。
これから作るもの
チャットインターフェースに組み込まれたインベントリパネル。プレイヤーはすべてのアイテムを見ることができ、各アイテムにアイコン、名前、数量が表示されます。各アイテムの下にアクションボタンがあります:
- 消耗品(例:ポーション)— 「Use」をクリック → HPが20回復 → ポーション数が1減る → 数が0になったらインベントリから削除 → ポップアップに「Used a potion! HP +20」
- 装備品(例:アイアンソード)— 「Equip」をクリック → 武器スロットに「Iron Sword」が表示 → AIがプレイヤーがアイアンソードを装備していることを知る → ポップアップに「Equipped Iron Sword!」
プレイヤーがポーションの「Use」ボタンをクリック
→ レンダラーがチェック:インベントリにポーションは含まれているか?
→ はい:インベントリ配列を更新、hp +20、成功トースト
→ いいえ:警告トースト「No potions left!」
プレイヤーがアイアンソードの「Equip」ボタンをクリック
→ レンダラーがチェック:すでに装備済みか?
→ いいえ:「equip-sword」ビヘイビアをトリガー
→ ビヘイビアがequipped_weaponを設定、AIに伝え、通知を表示
→ はい:情報トースト「Already equipped!」仕組み
インベントリはJSON変数として保存されます — アイテムオブジェクトの配列全体を保持する単一の変数です。Root Componentがこの配列を読んでグリッドを表示し、プレイヤーがアイテムを使ったり取得したりするときapi.setVariable()を使って直接操作します。
なぜRoot Componentでロジックを処理するのか? ビヘイビアシステムの条件演算子(eq、neq、gt、lt、containsなど)は単純な値 — 数値、文字列、boolean — で動作します。JSON配列の中を検索することはできません(例:「配列にname = Potionのオブジェクトが含まれているか?」)。インベントリのような複雑なデータ構造には、JavaScriptを使ってロジックを処理するRoot Componentが正しい場所です。
ビヘイビアは引き続き得意なことに使います:単純な変数の設定(equipped_weapon)、AI指示の注入(「Tell AI」)、通知の表示です。
分担:
| 何を | どこで | 理由 |
|---|---|---|
| インベントリグリッドを表示 | Root Component | JSON配列を読み取りUIをレンダリング |
| 消耗品を使う | Root Component | 配列要素を探し、更新し、削除する必要がある |
| 武器を装備 | ビヘイビア | 文字列変数を設定 + AIに伝える |
| 変更をAIに伝える | ビヘイビア | ビヘイビアのみがAI指示を注入できる |
ステップバイステップ
ステップ1:変数の作成
3つの変数が必要です — インベントリ(JSON配列)、ヒットポイント(数値)、現在装備中の武器(文字列)。
エディタ → 左サイドバー → Variables タブ → 各々「Add Variable」をクリック
変数1:Inventory
| フィールド | 値 | 理由 |
|---|---|---|
| Display Name | Inventory | 自分用の人間可読ラベル |
| ID | inventory | コードとビヘイビアがこの変数を読み書きするときに使うID |
| Type | JSON | インベントリは配列 — JSONタイプが必要 |
| Default Value | [{"name":"Potion","icon":"🧪","count":2},{"name":"Iron Sword","icon":"⚔️","count":1}] | 新規セッションはポーション2個とアイアンソード1個でスタート |
| Category | Inventory | Inventoryカテゴリにグループ化 |
| Behavior Rules | Inventory buttons handle use and equip actions automatically. You may also add items during the story (player finds loot, receives a reward) or remove items (broken, lost, stolen). | ストーリー中もインベントリが変化できることをAIに伝える |
JSON変数のデフォルト値は有効なJSONでなければなりません。 フィールド名と文字列値にはダブルクォートを使用。各アイテムオブジェクトには3つのフィールドがあります:
name(マッチングと表示用)、icon(UI用)、count(消耗品の数量追跡用)。
変数2:Hit Points
| フィールド | 値 | 理由 |
|---|---|---|
| Display Name | Hit Points | 人間可読ラベル |
| ID | hp | ポーションがHPを回復するときに使用 |
| Type | Number | HPは数値 — 加減算が必要 |
| Default Value | 80 | 最大未満から始まることで、プレイヤーにポーションを使う理由を与える |
| Min Value | 0 | HPがマイナスにならないよう防ぐ |
| Max Value | 100 | HPの上限100、無限スタックを防ぐ |
| Category | Stats | キャラクターステータス変数 |
| Behavior Rules | Current value represents the player's remaining hit points (0-100). Decrease in combat or dangerous situations, increase when using potions or resting. | いつHPを変更するかAIに伝える |
変数3:Equipped Weapon
| フィールド | 値 | 理由 |
|---|---|---|
| Display Name | Equipped Weapon | 人間可読ラベル |
| ID | equipped_weapon | プレイヤーの装備武器名を記録 |
| Type | String | 武器名をテキストとして保存 |
| Default Value | (空のまま) | 空文字列 = 武器未装備 |
| Category | Custom | 装備状態変数 |
| Behavior Rules | Current value is the name of the player's equipped weapon. Empty string means nothing equipped. The equip button sets this automatically, but you may also change it during the story — e.g. weapon breaks, gets stolen, or player finds a new one. | 物語的にも装備状態が変化できることをAIに伝える |
なぜequipped_weaponにJSONではなく文字列を使うのか? プレイヤーは一度に1つの武器しか振るえないからです。単純な文字列で十分 — 空は未装備、
"Iron Sword"は装備中。マルチスロット装備システム(武器 + 防具 + アクセサリ)が欲しい場合はJSONオブジェクトを使うこともできます。
ステップ2:ビヘイビアの作成
2つのビヘイビアが必要です — アイアンソードを装備(成功と装備済み)。ポーションの使用は完全にRoot Componentで処理されます。
エディタ → Behaviors タブ → 「Add Behavior」をクリック
ビヘイビア1:Equip Iron Sword(成功)
WHEN(トリガー):
| フィールド | 値 | 理由 |
|---|---|---|
| Trigger Type | Action button pressed | Root ComponentがexecuteAction("equip-sword")を呼ぶと発火 |
| Action ID | equip-sword | Root Component内のexecuteAction("equip-sword")呼び出しと一致 |
ONLY IF(条件):
| 変数 | 演算子 | 値 | 理由 |
|---|---|---|---|
equipped_weapon | neq | Iron Sword | まだ装備していない — ビヘイビア2との重複を防ぐ |
DO(エフェクト):
これらのエフェクトを順番に追加:
| Effect Type | 設定 | 動作 |
|---|---|---|
| Modify Variable | 変数equipped_weapon、オペレーションset、値Iron Sword | 現在の武器をアイアンソードに設定 |
| Tell AI | 内容:The player equipped an Iron Sword. From now on, the player is wielding an iron longsword. Reflect the weapon's presence in combat descriptions and interactions. | AIが武器について知るよう指示を注入 |
| Show Notification | メッセージEquipped Iron Sword!、スタイルachievement | 金色の成功ポップアップ |
「Tell AI」は何をする? AIのコンテキストに一時的な指示を注入します。これにより、AIが次の応答を書くときに、プレイヤーがちょうど剣を装備したことを知り、それをナラティブに反映できます(例:「You tighten your grip on the iron sword. Its cold edge glints in the firelight.」)。
ビヘイビア2:Equip Iron Sword(既に装備済み)
WHEN:
| フィールド | 値 |
|---|---|
| Trigger Type | Action button pressed |
| Action ID | equip-sword |
ONLY IF:
| 変数 | 演算子 | 値 | 理由 |
|---|---|---|---|
equipped_weapon | eq | Iron Sword | 既に装備済み — 再装備不要 |
DO:
| Effect Type | 設定 | 動作 |
|---|---|---|
| Show Notification | メッセージIron Sword is already equipped!、スタイルinfo | 青の情報ポップアップ |
なぜこれを2つのビヘイビアに分割するのか? ショップレシピと同じパターン — 1つのビヘイビアは1セットの条件しか持てません。条件が通れば実行、通らなければ何も起きません。そのため、2つのケースをカバーするために2つのビヘイビアを使います。同じアクションIDをリッスンしますが、条件は相互排他的 — どちらか一方だけが発火します。
なぜ「use-potion」ビヘイビアがないのか? JSON配列に特定のアイテムが含まれているかチェックするにはJavaScriptが必要だからです — ビヘイビアシステムの
contains演算子は文字列でのみ動作し、配列では動作しません。そのため、ポーションロジックはJavaScriptへの完全なアクセスを持つRoot Componentに存在します。Root Componentがapi.setVariable()経由でinventoryとhp変数を直接更新します。
ステップ3:Root Componentにインベントリパネルを追加
これがチャット内にインベントリUIを表示させるステップです。最新メッセージの下に3つのセクションを表示します:HPバー、装備スロット、インベントリグリッド(各アイテムにアクションボタン付き)。
エディタ → Custom UI セクション → index.tsxを開く → 以下を貼り付け(デフォルトのreturn <Chat />を置き換え):
export default function MyWorld() {
var api = useYumina();
var msgs = api.messages || [];
// Read variables
var hp = Number(api.variables.hp ?? 80);
var equippedWeapon = String(api.variables.equipped_weapon || "");
var inventory = Array.isArray(api.variables.inventory)
? api.variables.inventory
: [];
// ── Inventory logic (runs in the Root Component) ──
function useItem(itemName) {
var inv = Array.isArray(api.variables.inventory)
? api.variables.inventory
: [];
var idx = -1;
for (var i = 0; i < inv.length; i++) {
if (inv[i] && inv[i].name === itemName) { idx = i; break; }
}
if (idx === -1) {
api.showToast("No " + itemName + " left!", "error");
return;
}
var item = inv[idx];
var newInv = inv.slice(); // copy the array
if (Number(item.count) <= 1) {
newInv.splice(idx, 1); // remove entirely
} else {
newInv[idx] = { name: item.name, icon: item.icon, count: Number(item.count) - 1 };
}
api.setVariable("inventory", newInv);
// Potion-specific: restore HP
if (itemName === "Potion") {
var currentHp = Number(api.variables.hp ?? 0);
api.setVariable("hp", Math.min(currentHp + 20, 100));
api.showToast("Used a potion! HP +20", "success");
}
}
function equipItem(itemName, actionId) {
if (equippedWeapon === itemName) {
api.showToast(itemName + " is already equipped!", "info");
return;
}
api.executeAction(actionId); // triggers the behavior for set + Tell AI
}
// Item type map: decides what action each item gets
var itemActions = {
"Potion": { type: "consumable", handler: function() { useItem("Potion"); }, label: "Use" },
"Iron Sword": { type: "equipment", handler: function() { equipItem("Iron Sword", "equip-sword"); }, label: "Equip" },
};
return (
<Chat renderBubble={(msg) => {
var isLastMsg = msg.messageIndex === msgs.length - 1;
return (
<div>
{/* Render message text normally (platform already rendered HTML, use contentHtml directly) */}
<div
style={{ color: "#e2e8f0", lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
/>
{/* Show inventory panel only below the last message */}
{isLastMsg && (
<div style={{
marginTop: "16px",
padding: "16px",
background: "rgba(15, 23, 42, 0.6)",
borderRadius: "12px",
border: "1px solid #334155",
}}>
{/* ====== HP Bar ====== */}
<div style={{
display: "flex",
alignItems: "center",
gap: "10px",
marginBottom: "14px",
}}>
<span style={{ fontSize: "16px" }}>❤️</span>
<div style={{ flex: 1 }}>
<div style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "4px",
}}>
<span style={{ color: "#94a3b8", fontSize: "12px" }}>HP</span>
<span style={{ color: "#e2e8f0", fontSize: "12px", fontWeight: "bold" }}>
{hp} / 100
</span>
</div>
<div style={{
height: "8px",
background: "#1e293b",
borderRadius: "4px",
overflow: "hidden",
}}>
<div style={{
height: "100%",
width: Math.min(hp, 100) + "%",
background: hp > 50
? "linear-gradient(90deg, #22c55e, #4ade80)"
: hp > 20
? "linear-gradient(90deg, #eab308, #facc15)"
: "linear-gradient(90deg, #ef4444, #f87171)",
borderRadius: "4px",
transition: "width 0.3s ease",
}} />
</div>
</div>
</div>
{/* ====== Equipment Slot ====== */}
<div style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "14px",
padding: "10px 14px",
background: "rgba(30, 41, 59, 0.8)",
borderRadius: "8px",
border: "1px solid #475569",
}}>
<span style={{ fontSize: "16px" }}>⚔️</span>
<span style={{ color: "#94a3b8", fontSize: "13px" }}>Weapon:</span>
<span style={{
color: equippedWeapon ? "#e2e8f0" : "#475569",
fontSize: "13px",
fontWeight: equippedWeapon ? "600" : "normal",
fontStyle: equippedWeapon ? "normal" : "italic",
}}>
{equippedWeapon || "None"}
</span>
</div>
{/* ====== Inventory Header ====== */}
<div style={{
fontSize: "14px",
fontWeight: "bold",
color: "#94a3b8",
marginBottom: "10px",
textTransform: "uppercase",
letterSpacing: "1px",
}}>
Inventory
</div>
{/* ====== Inventory Grid ====== */}
{inventory.length === 0 ? (
<div style={{
padding: "24px",
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(140px, 1fr))",
gap: "8px",
}}>
{inventory.map(function(item, idx) {
var name = String(item?.name || item);
var icon = String(item?.icon || "📦");
var count = Number(item?.count ?? 1);
var action = itemActions[name];
return (
<div
key={idx}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: "12px 8px 8px",
background: "rgba(30, 41, 59, 0.8)",
borderRadius: "8px",
border: equippedWeapon === name
? "1px solid #22d3ee"
: "1px solid #475569",
gap: "6px",
}}
>
<span style={{ fontSize: "28px" }}>{icon}</span>
<span style={{
color: "#e2e8f0",
fontSize: "12px",
fontWeight: "600",
textAlign: "center",
}}>
{name}
</span>
<span style={{
color: "#64748b",
fontSize: "11px",
}}>
x{count}
</span>
{/* Action button */}
{action && (
<button
onClick={action.handler}
style={{
marginTop: "4px",
padding: "4px 14px",
background: action.type === "consumable"
? "linear-gradient(135deg, #065f46, #047857)"
: equippedWeapon === name
? "linear-gradient(135deg, #374151, #4b5563)"
: "linear-gradient(135deg, #1e3a5f, #1e40af)",
border: action.type === "consumable"
? "1px solid #10b981"
: equippedWeapon === name
? "1px solid #6b7280"
: "1px solid #3b82f6",
borderRadius: "6px",
color: action.type === "consumable"
? "#a7f3d0"
: equippedWeapon === name
? "#9ca3af"
: "#bfdbfe",
fontSize: "12px",
fontWeight: "600",
cursor: "pointer",
width: "100%",
}}
>
{equippedWeapon === name ? "Equipped" : action.label}
</button>
)}
</div>
);
})}
</div>
)}
</div>
)}
</div>
);
}} />
);
}コードウォークスルー
長さに圧倒されないでください — 行うことは非常に簡単です。セクションごとに見ていきましょう:
基本セットアップ
var api = useYumina();
var msgs = api.messages || [];
// ...
<Chat renderBubble={(msg) => {
var isLastMsg = msg.messageIndex === msgs.length - 1;
// ...
}} />- Root Component
MyWorld()はワールドUIのエントリ。<Chat renderBubble={...} />はメッセージリスト、入力ボックス、スクロールをプラットフォームに任せ — 各バブルの見た目だけ引き継ぐ useYumina()— Yumina APIを取得して変数を読み、アクションをトリガーできるmsg.messageIndex— 現在のバブルのメッセージリスト内のインデックス。インベントリパネルは最後のメッセージの下にのみレンダリングされるため、各メッセージで繰り返さないmsg.contentHtml— プラットフォームがMarkdownからレンダリングしたHTML、dangerouslySetInnerHTMLに直接渡せる
変数の読み取り
var hp = Number(api.variables.hp ?? 80);
var equippedWeapon = String(api.variables.equipped_weapon || "");
var inventory = Array.isArray(api.variables.inventory)
? api.variables.inventory
: [];api.variables.hp— ヒットポイントを読み取る。?? 80は変数がまだロードされていない場合のフォールバックapi.variables.equipped_weapon— 現在の武器を読み取る。空文字列は何も装備していないことを意味するapi.variables.inventory— インベントリを読み取る。Array.isArray()は予期しない型から保護
インベントリロジック関数
function useItem(itemName) {
var inv = Array.isArray(api.variables.inventory)
? api.variables.inventory : [];
var idx = -1;
for (var i = 0; i < inv.length; i++) {
if (inv[i] && inv[i].name === itemName) { idx = i; break; }
}
if (idx === -1) {
api.showToast("No " + itemName + " left!", "error");
return;
}
// ... update array and call api.setVariable()
}これが重要なパターンです。ビヘイビアシステムの条件演算子がJSON配列の中を検索できないため、ロジックをここRoot Componentで処理します:
- アイテムを見つける — 配列をループし
nameで一致を確認 - 存在を確認 — 見つからない場合エラートーストを表示
- 配列を更新 — カウントを減らすか完全に削除
- 書き戻す —
api.setVariable("inventory", newInv)を呼んで変更を永続化
装備の場合、equipItem()はapi.executeAction()に委任します。ビヘイビアが変数設定とAI指示の注入を処理するからです:
function equipItem(itemName, actionId) {
if (equippedWeapon === itemName) {
api.showToast(itemName + " is already equipped!", "info");
return;
}
api.executeAction(actionId);
}アイテムタイプマップ
var itemActions = {
"Potion": { type: "consumable", handler: function() { useItem("Potion"); }, label: "Use" },
"Iron Sword": { type: "equipment", handler: function() { equipItem("Iron Sword", "equip-sword"); }, label: "Equip" },
};ルックアップテーブルです。アイテム名を与えると、ボタンラベルと呼び出すハンドラ関数を教えてくれます。typeフィールドがボタン色を制御 — 消耗品は緑、装備品は青。新しいアイテムを追加したい?ここに行を追加。消耗品の場合はuseItemにロジックを追加。装備品の場合はエディタで対応するビヘイビアを作成。
アクションボタン
<button onClick={action.handler}>
{equippedWeapon === name ? "Equipped" : action.label}
</button>ボタンをクリックするとハンドラ関数を直接呼びます。消耗品の場合、ハンドラがJavaScriptで配列を管理。装備品の場合、ハンドラがapi.executeAction()を呼んで対応するビヘイビアをトリガー。
自分でコードを書きたくない?Studio AIを使おう
エディタ上部 → 「Enter Studio」をクリック → AI Assistantパネル → 平易な言葉で説明、例:「HPバー、装備スロット、使用または装備可能なアイテムを持つインベントリグリッドを構築」と説明すれば、AIがコードを生成します。
ステップ4:保存してテスト
- エディタ上部のSaveをクリック
- Start Gameをクリックするか、ホームページに戻って新規セッションを開始
- AIの応答の下にインベントリパネルが見える:HP 80/100、武器スロット空、ポーション2個とアイアンソード1個
- ポーションの「Use」をクリック — HPが80から100に、ポーションが消える、トーストに「Used a potion! HP +20」
- アイアンソードの「Equip」をクリック — 武器スロットに「Iron Sword」が表示、ボタンがグレーになり「Equipped」と表示、ポップアップに「Equipped Iron Sword!」
- アイアンソードの「Equipped」ボタンを再びクリック — トーストに「Iron Sword is already equipped!」
- AIとチャットを続ける — 「Tell AI」エフェクトを追加していれば、AIの応答にプレイヤーがアイアンソードを振るっていることが反映される
何かが機能していない場合:
| 症状 | 想定される原因 | 対処 |
|---|---|---|
| インベントリパネルが表示されない | Root Componentコードが保存されていない、または構文エラー | Custom UIセクション下部のコンパイル状態を確認 — 緑の「OK」が表示されるべき |
| インベントリにアイテムが表示されない | JSON変数のデフォルト値の書式が不正 | デフォルトがダブルクォートされたフィールド名を持つ有効なJSON配列であることを確認 |
| クリックしてもボタンが反応しない | ビヘイビアアクションIDがコードと一致しない | ビヘイビアのアクションIDが正確にequip-swordで、コード内のexecuteAction()引数と一致することを確認 |
| ポーションを使ったが消えない | useItem関数がアイテム名を見つけられない | JSONのnameフィールドがuseItem()が探すものと完全に一致することを確認 — 大文字小文字を区別 |
| HPが変わらない | api.setVariable呼び出しが正しい変数に届いていない | 変数IDが正確にhpで、変数定義と一致することを確認 |
| 装備したがAIが知らない | 「Tell AI」エフェクトが欠落 | 装備ビヘイビアのDOセクションに「Tell AI」エフェクトを追加 |
AIがインベントリを変更する方法
AIもストーリー中にディレクティブを使ってアイテムを追加または削除できます。インベントリはJSON変数なので、AIはpushディレクティブを使ってアイテムを追加できます:
You defeated the goblin and found a health potion among its belongings.
[inventory: push {"name":"Potion","icon":"🧪","count":1}]配列に対するAIディレクティブの制限
pushディレクティブはアイテムの追加にうまく動作します。ただし、配列に対するdeleteは数値インデックスでのみ動作し(例:[inventory: delete 0]は最初の要素を削除)、mergeはプレーンオブジェクトでのみ動作し配列では動作しません。複雑なインベントリ操作(名前で特定アイテムを削除、アイテム数を更新)には、Root ComponentのJavaScriptロジックを使うか、AIが他の変数を介して意図を伝え、ビヘイビアがそれに基づいて動作するようにシステムを設計してください。
クイックリファレンス
| やりたいこと | 方法 |
|---|---|
| アイテムリストを保存 | JSON変数を作成、デフォルト値[{...}, ...] |
| インベントリグリッドを表示 | Root ComponentでCSS Grid + inventory.map()を使用 |
| 消耗品を使う | Root Component:アイテムを探す → 配列を更新 → api.setVariable() → トースト表示 |
| アイテムを装備 | Root Component:api.executeAction()を呼ぶ → ビヘイビア:変数設定 + Tell AI |
| プレイヤーがアイテムを持っているか確認 | Root Component:inventory.find(i => i.name === "ItemName") |
| アイテムを追加(AI) | AIディレクティブ:[inventory: push {"name":"Item","icon":"📦","count":1}] |
| 現在の装備を追跡 | 文字列変数を作成 — 空文字列 = 何も装備していない |
| ボタンで使用/装備をトリガー | Root Componentでハンドラ関数またはapi.executeAction("actionId")を呼ぶ |
| 変更をAIに伝える | ビヘイビアに「Tell AI」エフェクトを追加 |
自分で試す — インポート可能なデモワールド
このJSONをダウンロードし、新規ワールドとしてインポートしてすべての動作を確認してください:
インポート方法:
- Yumina → My Worlds → Create New World に移動
- エディタで More Actions → Import Package をクリック
- ダウンロードした
.jsonファイルを選択 - 全変数、ビヘイビア、Root Componentが事前設定されたワールドが作成されます
- 新規セッションを開始して試す
含まれるもの:
- 3つの変数(
inventory+hpヒットポイント +equipped_weapon現在の武器) - 2つのビヘイビア(アイアンソード装備成功 + 装備済み)
- Root Component(HPバー + 装備スロット + インベントリグリッド + アクションボタン + 使用/装備ロジック)
これはRecipe #7です
これまでのレシピでは、シーンジャンプ、戦闘システム、ショップインターフェース、キャラクター作成をカバーしました。このレシピでは、Root ComponentのJavaScriptロジックとビヘイビアを組み合わせて単純な状態変更でJSON配列インベントリを管理する方法を教えます。同じパターンが、クエストログ、スキルツリー、クラフトレシピなど、「リストを管理しその要素に対する操作を実行する」あらゆるものに使えます。
