Skip to content

クエストトラッカー

クエストトラッカーパネルを構築しましょう — 各クエストの完了状態を表示(チェックマークまたはX)、ゴールド報酬をリアルタイムで表示します。プレイヤーがクエストを完了すると、アチーブメント通知を自動的にポップアップし報酬を渡します。このレシピでは、変数、ビヘイビア、Root Componentで配線する方法を教えます。


これから作るもの

チャットインターフェースに組み込まれたクエストトラッカーパネル:

  • クエストリスト — 各クエストが名前と完了状態を表示(完了 = 緑のチェックマーク、未完了 = 赤のX)
  • ゴールドカウンタ — プレイヤーの現在のゴールドをリアルタイムで表示
  • 自動検出 — プレイヤーのメッセージにキーワード(例:「herb」または「defeat」)が含まれると、クエストが自動的に完了マーク
  • アチーブメント通知 — クエスト完了時に金色のトーストがポップアップし、プレイヤーにいくら獲得したか伝える
  • ゴールド報酬 — 各クエストが完了時に自動的にゴールドを支払う
プレイヤーが「found the herbs」を含むメッセージを送信
  → エンジンがプレイヤーキーワード「herb」を検出
  → 条件をチェック:quest_1_complete == false?
    → はい:quest_1_complete = true に設定、30ゴールド追加、アチーブメント通知をポップアップ
    → いいえ:何もしない(クエストはすでに完了)
  → クエストパネルが自動更新:「Find Herbs」が ✗ から ✓ に変わる

仕組み

このクエストシステムは3つのコアメカニズムを使用します:

  1. boolean変数 + キーワードトリガー — 各クエストはboolean変数で追跡されます。プレイヤーのメッセージに特定のキーワードが含まれると、ビヘイビアルールが自動的に変数をtrueに設定
  2. 条件チェック — ビヘイビアは発火前にクエストがすでに完了しているかチェック。完了したクエストは再度トリガーしない(二重報酬なし)
  3. Root Componentが変数を読む — パネルはクエスト状態とゴールドをリアルタイムで変数から読み、チェックマークまたはXを動的にレンダリング

ステップバイステップ

ステップ1:変数の作成

5つの変数が必要です — クエスト完了状態用の2つ、ゴールド用の1つ、クエスト名用の2つ(Root Componentが動的に表示できるように)。

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

変数1:Quest 1 completion status

フィールド理由
NameQuest 1 Complete変数リスト内で自分用の識別ラベル
IDquest_1_completeビヘイビアとRoot ComponentがこのIDで値を読み書き
TypeBoolean2状態のみ:「完了」と「未完了」
Default Valuefalse新規セッション開始時はクエスト未完了
CategoryFlagこれはステータスフラグ、数値ステータスではない
Behavior RulesSet to true when the player completes the Find Herbs quest. Behaviors auto-detect this via keywords, but you may also mark it complete at an appropriate story moment.この変数の意味といつ変わるべきかをAIに伝える

変数2:Quest 2 completion status

フィールド理由
NameQuest 2 Complete識別しやすい名前
IDquest_2_completeビヘイビアとRoot Componentで使用
TypeBoolean同じ2状態セットアップ
Default Valuefalseセッション開始時は未完了
CategoryFlagステータスフラグ
Behavior RulesSet to true when the player defeats the Forest Wolf. Behaviors auto-detect this via keywords, but you may also mark it complete at an appropriate story moment.この変数の意味といつ変わるべきかをAIに伝える

変数3:Gold

フィールド理由
NameGold識別しやすい名前
IDgoldクエスト完了時に自動的に増加
TypeNumberゴールドは数値 — 加減算が必要
Default Value0セッション開始時はゴールドなし — クエスト完了で獲得
Min Value0ゴールドがマイナスになることを防ぐ
CategoryResourceゴールドはリソース変数
Behavior RulesGold is automatically awarded on quest completion. You may also add or subtract gold in the story — e.g., combat loot, trading, or theft.ゴールドが複数のコンテキストで変化できることをAIに伝える

変数4:Quest 1 name

フィールド理由
NameQuest 1 Name識別しやすい名前
IDquest_1_nameRoot ComponentがこのIDでクエスト名を表示
TypeStringクエスト名はテキスト
Default ValueFind Herbs最初のクエストの名前
CategoryCustom単なる記述データ
Behavior RulesDo not modify this variable.クエスト名は変更すべきでない

変数5:Quest 2 name

フィールド理由
NameQuest 2 Name識別しやすい名前
IDquest_2_nameRoot Componentで使用
TypeStringクエスト名はテキスト
Default ValueDefeat the Forest Wolf2番目のクエストの名前
CategoryCustom記述データ
Behavior RulesDo not modify this variable.クエスト名は変更すべきでない

なぜすべての変数にビヘイビアルールを書くのか?

AIが返信を生成するとき変数の変更を「提案」できるからです。変数を放っておくよう伝えないと、AIが自分でクエストを完了マークするかもしれません(例:AIが「プレイヤーがハーブを見つけた」と判断しquest_1_completetrueに設定 — しかしビヘイビアロジックをバイパスしたため、ゴールド報酬は支払われない)。ビヘイビアルールフィールドはAIへの指示 — 一度書けば、AIはこれらの変数がシステム制御だと知ります。


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

これがクエストシステムの心臓部です。キーワードを検出し対応するクエストを完了マークしながら報酬を渡す2つのビヘイビアが必要です。

エディタ → Behaviors タブ → 各々「Add Behavior」をクリック

ビヘイビア1:「Find Herbs」クエスト完了

WHEN(チェックタイミング):

フィールド理由
Trigger typePlayer said keyword (keyword)プレイヤーのメッセージに特定のテキストが含まれると発火
Keywordsherb または found herbプレイヤーが「I found the herbs」のようなことを言ったら一致

キーワードマッチングはどう動くのか? エンジンはプレイヤーのメッセージの内容をチェック — どこかにキーワードが含まれていれば一致します。そのため「I found the herbs in the cave」は「herb」を含むためトリガーします。AIの応答内のキーワードも検出したい場合、トリガータイプを「AI said keyword」(ai-keyword)に設定した別のビヘイビアを作成します。

ONLY IF(条件):

変数演算子理由
quest_1_completeequals (eq)falseクエストがまだ完了していないときだけトリガー — 二重報酬を防ぐ

なぜ条件が必要なのか? 条件がなければ、誰かが「herb」と言うたびに報酬が再度発火します。quest_1_complete == falseがあれば、最初の「herb」言及 → クエスト完了、報酬支払い、trueマーク。それ以降の言及 → 条件失敗(すでにtrue)、何も起きません。

DO(アクション):

これらのアクションを順番に追加:

Action type設定効果
Modify variable変数quest_1_complete、オペレーションset、値trueクエストを完了マーク
Modify variable変数gold、オペレーションadd、値3030ゴールド報酬を支払う
Show notificationメッセージQuest Complete: Find Herbs! +30 gold、スタイルachievement金色のアチーブメントトーストをポップアップ
Tell AI内容:The player just completed the quest "Find Herbs" and received 30 gold as a reward. Please acknowledge this in your response.何が起きたかAIに知らせ、より良いナラティブ遷移を書けるようにする

なぜ「Tell AI」? 変数の変更と通知の表示は静かなシステム操作です — AI自体は「クエストが完了した」ことを知りません。このステップを追加することで、AIは次の返信で自然なフォローアップを書けます(例:「You carefully tuck the herbs into your pack, remembering the village elder's request. The trip wasn't for nothing after all」)。

ビヘイビア2:「Defeat the Forest Wolf」クエスト完了

WHEN(チェックタイミング):

フィールド理由
Trigger typePlayer said keyword (keyword)同上 — プレイヤーキーワードトリガー
Keywordsdefeatwolf両方の単語が表示される必要がある — 「I saw a wolf」がトリガーしないようにする

複数キーワードマッチングロジック。 複数のキーワードを入力すると、メッセージはすべてを含む必要があります。そのため「I defeated the forest wolf」はトリガー(「defeat」と「wolf」両方を含む)、「I spotted a wolf」はトリガーしない(「wolf」のみ、「defeat」なし)。

ONLY IF(条件):

変数演算子理由
quest_2_completeequals (eq)false同上 — 繰り返しトリガーを防ぐ

DO(アクション):

Action type設定効果
Modify variable変数quest_2_complete、オペレーションset、値trueクエストを完了マーク
Modify variable変数gold、オペレーションadd、値5050ゴールド支払い(オオカミ討伐は難しい、報酬は大きい)
Show notificationメッセージQuest Complete: Defeat the Forest Wolf! +50 gold、スタイルachievement金色のアチーブメントトーストをポップアップ
Tell AI内容:The player just completed the quest "Defeat the Forest Wolf" and received 50 gold as a reward. Please acknowledge this in your response.何が起きたかAIに知らせる

アクション実行順序

1つのビヘイビア内のアクションは順番に実行されます。そのため:完了マーク → ゴールド追加 → 通知ポップアップ → AIに伝える。この順序は重要 — 変数を最初にマークすることで、すべての後続ロジックが最新の状態に基づくことを保証します。


ステップ3:Root Componentにクエストトラッカーパネルを追加

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

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

tsx
export default function MyWorld() {
  const api = useYumina();
  const msgs = api.messages || [];

  // Read variables
  const quest1Done = api.variables.quest_1_complete === true;
  const quest2Done = api.variables.quest_2_complete === true;
  const quest1Name = String(api.variables.quest_1_name || "Find Herbs");
  const quest2Name = String(api.variables.quest_2_name || "Defeat the Forest Wolf");
  const gold = Number(api.variables.gold ?? 0);

  // Quest list data
  const quests = [
    { name: quest1Name, done: quest1Done, reward: 30 },
    { name: quest2Name, done: quest2Done, reward: 50 },
  ];

  const completedCount = quests.filter(q => q.done).length;

  return (
    <Chat renderBubble={(msg) => {
      const 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 }}
      />

      {/* Quest tracker panel — only shown on the last message */}
      {isLastMsg && (
        <div style={{
          marginTop: "16px",
          padding: "16px",
          background: "linear-gradient(135deg, rgba(30,41,59,0.8), rgba(15,23,42,0.9))",
          borderRadius: "12px",
          border: "1px solid #334155",
        }}>
          {/* Panel header */}
          <div style={{
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            marginBottom: "14px",
          }}>
            <div style={{
              fontSize: "15px",
              fontWeight: "bold",
              color: "#e2e8f0",
              letterSpacing: "0.5px",
            }}>
              Quest Tracker
            </div>
            {/* Gold counter */}
            <div style={{
              display: "flex",
              alignItems: "center",
              gap: "6px",
              padding: "4px 12px",
              background: "rgba(234,179,8,0.15)",
              border: "1px solid rgba(234,179,8,0.3)",
              borderRadius: "20px",
            }}>
              <span style={{ fontSize: "14px" }}>💰</span>
              <span style={{
                fontSize: "14px",
                fontWeight: "bold",
                color: "#fbbf24",
              }}>
                {gold}
              </span>
            </div>
          </div>

          {/* Progress indicator */}
          <div style={{
            fontSize: "12px",
            color: "#64748b",
            marginBottom: "12px",
          }}>
            Completed {completedCount}/{quests.length}
          </div>

          {/* Quest list */}
          <div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
            {quests.map((quest, idx) => (
              <div
                key={idx}
                style={{
                  display: "flex",
                  justifyContent: "space-between",
                  alignItems: "center",
                  padding: "10px 14px",
                  background: quest.done
                    ? "rgba(34,197,94,0.08)"
                    : "rgba(30,41,59,0.5)",
                  border: quest.done
                    ? "1px solid rgba(34,197,94,0.2)"
                    : "1px solid #1e293b",
                  borderRadius: "8px",
                }}
              >
                {/* Left side: quest name */}
                <div style={{
                  display: "flex",
                  alignItems: "center",
                  gap: "10px",
                }}>
                  <span style={{
                    fontSize: "13px",
                    color: quest.done ? "#94a3b8" : "#e2e8f0",
                    textDecoration: quest.done ? "line-through" : "none",
                  }}>
                    {quest.name}
                  </span>
                </div>

                {/* Right side: status badge */}
                <div style={{
                  display: "flex",
                  alignItems: "center",
                  gap: "8px",
                }}>
                  {/* Reward amount */}
                  <span style={{
                    fontSize: "12px",
                    color: quest.done ? "#4ade80" : "#64748b",
                  }}>
                    {quest.done ? `+${quest.reward} g` : `${quest.reward} g`}
                  </span>

                  {/* Completion status badge */}
                  <span style={{
                    display: "inline-flex",
                    alignItems: "center",
                    justifyContent: "center",
                    width: "24px",
                    height: "24px",
                    borderRadius: "6px",
                    fontSize: "13px",
                    fontWeight: "bold",
                    background: quest.done
                      ? "rgba(34,197,94,0.2)"
                      : "rgba(239,68,68,0.15)",
                    color: quest.done ? "#4ade80" : "#f87171",
                    border: quest.done
                      ? "1px solid rgba(34,197,94,0.3)"
                      : "1px solid rgba(239,68,68,0.25)",
                  }}>
                    {quest.done ? "✓" : "✗"}
                  </span>
                </div>
              </div>
            ))}
          </div>

          {/* All quests complete banner */}
          {completedCount === quests.length && (
            <div style={{
              marginTop: "12px",
              padding: "10px",
              background: "rgba(34,197,94,0.1)",
              border: "1px solid rgba(34,197,94,0.25)",
              borderRadius: "8px",
              textAlign: "center",
              fontSize: "13px",
              color: "#4ade80",
              fontWeight: "600",
            }}>
              All quests complete!
            </div>
          )}
        </div>
      )}
    </div>
      );
    }} />
  );
}

コードウォークスルー

長さに圧倒されないでください — 行うことは非常に簡単です。セクションごとに見ていきましょう:

基本セットアップ

tsx
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に直接使える

変数の読み取り

tsx
const quest1Done = api.variables.quest_1_complete === true;
const quest2Done = api.variables.quest_2_complete === true;
const quest1Name = String(api.variables.quest_1_name || "Find Herbs");
const quest2Name = String(api.variables.quest_2_name || "Defeat the Forest Wolf");
const gold = Number(api.variables.gold ?? 0);
  • === true — 厳密比較、boolean trueのみが完了とカウントされる。"true"(文字列)や1(数値)が誤解釈されることを防ぐ
  • String(... || "Find Herbs") — クエスト名を読み、変数が存在しない場合はデフォルトにフォールバック
  • Number(... ?? 0) — ゴールドを数値に変換。?? 0は「変数が存在しないなら0を使う」を意味

クエストリストデータ

tsx
const quests = [
  { name: quest1Name, done: quest1Done, reward: 30 },
  { name: quest2Name, done: quest2Done, reward: 50 },
];
const completedCount = quests.filter(q => q.done).length;

クエスト情報を配列に収集し、.map()でループできるようにします。completedCountは何個完了したか集計、進捗表示に使用。

ステータスバッジ

tsx
<span style={{
  background: quest.done
    ? "rgba(34,197,94,0.2)"    // done → green background
    : "rgba(239,68,68,0.15)",  // not done → red background
  color: quest.done ? "#4ade80" : "#f87171",
}}>
  {quest.done ? "✓" : "✗"}
</span>

各クエストの右側に小さなバッジ — 完了なら緑のチェックマーク、未完了なら赤のX。これがバッジコンポーネント効果。

ゴールドカウンタ

tsx
<div style={{
  padding: "4px 12px",
  background: "rgba(234,179,8,0.15)",
  borderRadius: "20px",
}}>
  💰 {gold}
</div>

パネル右上のピル形状のゴールド表示。クエストが完了するたびに、ゴールドが増加しパネルが自動的にリフレッシュして新しい値を表示。

全完了バナー

tsx
{completedCount === quests.length && (
  <div style={{ /* green highlight styles */ }}>
    All quests complete!
  </div>
)}

すべてのクエストが完了すると、パネル下部に緑のテキスト行が表示されます。completedCount === quests.lengthは完了数が合計に等しいかチェック。

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

エディタ上部 → 「Enter Studio」をクリック → AI Assistantパネル → 平易な言葉で説明(例:「クエスト完了状態とゴールドを表示するクエストトラッカーパネルを構築」)すると、AIがコードを生成します。


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

  1. エディタ上部のSaveをクリック
  2. Start Gameをクリックするか、ホームページに戻って新規セッションを開く
  3. AIの返信の下にクエストトラッカーパネルが見える:2つの赤Xマーク付きクエスト、0ゴールド
  4. キーワードを含むメッセージを送信(例:「I found the herbs」)— あなたのメッセージに「herb」が含まれ、ビヘイビアが即座に発火、パネルが更新:「Find Herbs」が緑のチェックマークに変わり、ゴールドが30になり、アチーブメント通知がポップアップ
  5. キーワード付き別メッセージを送信(例:「I defeated the forest wolf」)— あなたのメッセージに「defeat」と「wolf」両方が含まれ、2つ目のクエストが完了、ゴールドは80に増加
  6. 両方のクエストが完了したら、パネル下部に緑の「All quests complete!」バナーが表示

何かが機能していない場合:

症状想定される原因対処
クエストパネルが表示されないRoot Componentコードが保存されていない、または構文エラーCustom UIセクション下部のコンパイル状態を確認 — 緑の「OK」が表示されるべき
「herb」を含むメッセージを送ったがクエストが完了しないビヘイビアキーワードが実際の表現と一致しないメッセージに実際に「herb」が含まれていることを確認。注:トリガーはプレイヤーキーワードトリガー — プレイヤーのメッセージのみチェック、AIの返信はチェックしない
クエストは完了したがゴールドが変わらないビヘイビアに「modify variable gold add」アクションが欠落ビヘイビアエディタに戻り、「modify variable quest_1_complete」の後に「modify variable gold add 30」アクションがあることを確認
同じクエストが繰り返し報酬を与える条件が設定されていないビヘイビアのONLY IF条件にquest_1_complete eq falseが含まれていることを確認 — 未完了時のみトリガー
パネルがリアルタイムで更新されない正常 — パネルは次のメッセージで更新変数はすでに変更済み、AIの返信を待つか別のメッセージを送るとパネルが自動更新
通知がポップアップしないビヘイビアに「show notification」アクションが欠落アクションリストに通知表示アクションがあり、スタイルがachievementに設定されていることを確認
「Defeat the wolf」クエストがトリガーしない両方のキーワードが同じメッセージに表示される必要メッセージに「defeat」と「wolf」両方が含まれていることを確認。「I beat the wolf」と書いた場合、キーワードを「beat」に変更するか「beat」を代替として追加する必要

さらに進める:クエストシステムの拡張

基本が分かったら、この基盤の上に構築できます。

クエストを追加

Variablesタブに新しいboolean変数(quest_3_complete)と文字列変数(quest_3_name)を追加し、Behaviorsタブで対応するキーワードトリガービヘイビアを作成。最後に、Root Componentのquests配列に行を追加:

tsx
const quests = [
  { name: quest1Name, done: quest1Done, reward: 30 },
  { name: quest2Name, done: quest2Done, reward: 50 },
  { name: quest3Name, done: quest3Done, reward: 100 },
];

AIにクエストを割り当てさせる

「クエスト受諾」フローを構築できます — AIが対話で新しいクエストを記述し、ビヘイビアが特定のキーワードを検出してクエスト名変数を動的に更新:

Action type設定
Modify variable変数quest_3_name、オペレーションset、値Escort the merchant to safety
Show notificationメッセージNew Quest: Escort the merchant to safety、スタイルachievement

ショップシステムとの組み合わせ

クエストで獲得したゴールドはショップで使えます。Recipe #3(ショップとトレード)を参照 — 同じgold変数を使用。クエストシステムはゴールドを追加し、ショップシステムはそれを差し引きます。両方のシステムが1つの経済を共有。

クエストチェーン

ビヘイビアで条件の組み合わせを使って複雑なクエスト依存関係を作成できます。例:「Find Herbsを完了してからのみSave the Villageを受諾できる」:

変数演算子
quest_1_completeequals (eq)true
quest_3_completeequals (eq)false

両方の条件が満たされる必要がある — 前提クエストが完了し、現在のクエストがまだ完了していないことを保証。


クイックリファレンス

やりたいこと方法
クエスト完了を追跡boolean変数を作成、デフォルトfalse、カテゴリFlag
キーワードでクエスト完了を検出ビヘイビアトリガータイプ「Player said keyword」(keyword)、キーワードを入力
繰り返しトリガーを防ぐビヘイビアの条件にquest_complete eq falseを追加
完了時にアチーブメントトーストをポップアップビヘイビアアクション:通知表示、スタイルachievement
完了時にゴールドを授与ビヘイビアアクション:変数を変更、gold add 数量
クエスト完了をAIに知らせるビヘイビアアクション:tell AI、何が起きたか説明する文を書く
クエストパネルを表示Root Componentで変数を読み、チェックマーク/Xマークとゴールドをレンダリング
パネルを最後のメッセージにのみ表示<Chat renderBubble>内でmsg.messageIndex === msgs.length - 1をチェック
完了したクエストに取り消し線textDecoration: "line-through"スタイルを使用
完了進捗を表示quests.filter(q => q.done).lengthでカウント
すべてのクエスト完了時に特別バナーcompletedCount === quests.lengthをチェック

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

このJSONをダウンロードし、インポートしてクエスト追跡システムを体験してください:

recipe-6-demo.json

インポート方法:

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

含まれるもの:

  • 5つの変数(クエスト状態用quest_1_completequest_2_complete、通貨用gold、クエスト名用quest_1_namequest_2_name
  • 2つのビヘイビア(Find Herbs完了 + Defeat the Forest Wolf完了、各々条件チェック、変数変更、通知、tell-AIアクション付き)
  • Root Component(クエストトラッカーパネル:クエストリスト + ステータスバッジ + ゴールドカウンタ + 完了進捗)

これはRecipe #6です

これまでのレシピでは、シーンジャンプ、戦闘システム、ショップとトレード、キャラクター作成をカバーしました。このレシピでは、boolean変数 + キーワードトリガー + 条件チェックを使ってクエスト追跡システムを構築する方法を教えます。同じパターンが、アチーブメントシステム、ストーリー進捗追跡、サイドクエストツリーなど、「イベントを検出 → 状態をマーク → 報酬を支払う → UIを更新」のループが必要なあらゆるものに拡張できます。