v
Skip to content

実績システム ​

プレイヤーが節目に届いたとき(ゴールドが 100 を超えた、戦闘に 5 回以上勝った、隠しエリアを見つけた)、実績の名前とひとことが書かれたカードが出てきます。実績はそれぞれ一度だけ解放されてリストに入り、プレイヤー画面のページで、見つけたものとまだロックされているものを確認できます。どれもコードなしで作れます。


作るもの ​

  • 節目に届いた瞬間に出るポップアップ:タイトル、ひとこと、必要なら画像の付いた小さなカード
  • 自動で判定:ビヘイビアが数値と会話を見ているので、プレイヤーは何もしなくて大丈夫です
  • 実績は一度だけ解放:すでにリストに入っている場面は、二度と出てきません
  • 実績ページ:すべての実績が並び、解放したものは本文、ロック中のものはヒントが表示されます
  • 任意:新しくプレイを始めても解放したままの実績

しくみ ​

プレイヤーのゴールドが 95 から 101 になる
  → ビヘイビア「Big Spender」が Gold を見ている(変数がしきい値を越える、上回る 100)
  → 効果「ひとつの場面を解放」でカード「Big Spender」が出る
  → タイトル「Big Spender」がリスト変数 Achievements に追加される
  → 実績ページで Big Spender が解放済みになる

数値は物語の流れに合わせて AI が動かしますが、実績を解放するかどうかはエンジンが決めます。なので、実績が忘れられたり、二重に渡されたりすることはありません。


手順 ​

ステップ 1:変数を作る ​

カードの下にある 追加できるもの から、次の変数をひとつずつ + 変数 で追加します。

Gold ​

項目値
名前Gold
型数値
初期値0
範囲0 から(上限は空欄)
ビヘイビアルールプレイヤーの所持ゴールド。報酬をもらったとき、戦利品を得たとき、何かを売ったときに増え、お金を使うと減る。

Battles won ​

項目値
名前Battles won
型数値
初期値0
ビヘイビアルールプレイヤーが戦闘に勝った回数。プレイヤーがはっきり勝ったときに 1 増やす。

Achievements ​

項目値
名前Achievements
型リスト/表
初期値[]

AI での扱い を開き、AI アクセス を エンジン専用(AI に非表示) にします。このリストに書き込むのはビヘイビアだけで、AI が読む必要もありません。

新しくプレイしても残る実績

同じ欄で 周回をまたいで保持(プレイヤーごと) にチェックを入れます。こうすると、プレイヤーが最初からやり直しても、リストはそれまでに解放した実績から始まり、その実績がもう一度出てくることもありません。CG ギャラリーや「見たエンディング」のコレクションも、このしくみで作ります。


ステップ 2:実績ごとにビヘイビアを作る ​

追加できるもの から + ビヘイビア を押します。

ビヘイビア 1:Big Spender ​

部分設定
発動するとき変数がしきい値を越える:Gold、上回る、100
効果ひとつの場面を解放

場面の中身を入れます。

項目値
タイトルBig Spender
メッセージゴールドを 100 より多く貯めた。
画像(リンク)任意:https:// のリンクか、パネル → アセット のアセット
どのリストに集めるかAchievements

「変数がしきい値を越える」が発動するのは、数値が 100 以下から 100 より大きくなったその瞬間だけです。ゴールドがそのまま 101 から 150 へ増えても、何も起きません。ゴールドが 100 未満に戻ってからまた増えれば再び発動しますが、場面はもう Achievements に入っているので、何も出てきません。

ビヘイビア 2:Seasoned Fighter ​

部分設定
発動するとき変数がしきい値を越える:Battles won、上回る、5
効果ひとつの場面を解放:タイトル Seasoned Fighter、メッセージ 戦闘に 5 回より多く勝った。、集めるリストは Achievements

ビヘイビア 3:Trailblazer(キーワード) ​

これは数値ではなく会話を見ます。ビヘイビアのトリガーは 1 つだけなので、プレイヤーの発言と AI の発言の両方を拾うには、ビヘイビアを 2 つ作ります。

3a

部分設定
発動するときプレイヤーがキーワードを発言:探索, 洞窟を調べる
効果ひとつの場面を解放:タイトル Trailblazer、メッセージ 隠されたものを探しに行った。、集めるリストは Achievements

3b

部分設定
発動するときAIがキーワードを発言:隠し通路, 秘密の部屋
効果3a と同じ ひとつの場面を解放。タイトルも同じにします

キーワードはカンマで区切り、どれか 1 つが出れば発動します。2 つのビヘイビアは同じタイトルで同じリストに集めるので、先に発動したほうが実績を解放し、もう一方はすでに入っているのを見て何もしません。そのための条件を追加する必要はありません。

場面と通知、どちらを使う?

「実績解除」と一行出すだけなら、通知を表示(スタイルは 成功)でもできます。ただ、通知は何も覚えていません。実績ごとにスイッチ変数を用意し、二度発動しないように条件を付け、最大発動回数 を 1 にする必要があります。ひとつの場面を解放 なら、覚えておく部分を代わりにやってくれます。


ステップ 3:実績ページを作る ​

上部バー中央の プレイヤー画面 を押します。ページを追加 でページを足し(白紙のページ で大丈夫です)、タイトルを付けて、部品を追加 から リスト を置きます。

  1. 中身の出どころ で 自分で書く を選び、実績ごとに 1 行ずつ追加します。Big Spender、Seasoned Fighter、Trailblazer に、それぞれ一行の本文を付けます
  2. 各行をカードで表示 をオンにします
  3. ロック解除の条件 で 未解除の行をロック表示 をオンにします。見張る変数 を Achievements にし、照合する行の項目 は title のままにします。リストにその行のタイトルが入ると、その行が解放されます。ひとつの場面を解放 がリストに入れるのも、まさにこのタイトルです
  4. ロック中の表示 に、解放前にプレイヤーが見るヒントを書きます。たとえば「少し貯金してみよう」
  5. 会話に戻る ページへ移動 の ボタン を置いて、行き止まりにならないようにします(置かないと、ページの列に 出口なし の注意が出ます)
  6. 会話のページに「実績」と書いた ボタン を置き、その手順を ページへ移動 → 作ったページにします

物語の中で AI が収集品を渡す作りなら、ビヘイビアを使う代わりに コレクション テンプレートから始めましょう。「集めるものは最初は灰色。物語で見つけるたびに光ります。」というテンプレートで、専用のリスト変数とポップアップ、それに AI がいつリストに追加するかを書いたビヘイビアルールが付いています。

上の 編集 をオフにすると、プレイヤーと同じようにページを操作して確かめられます。

数を数える

「Achievements unlocked」という数値の変数を追加し、計算式(自動で計算) に len(Achievements) と入れます。ページの文字の部品に {Achievements unlocked} / 3 と書けば、解放数が自動で更新されます。


ステップ 4:プレイテスト ​

上部バーの プレイ を押します。

  1. 実績 を押します。3 つともロックされていて、ヒントが表示されています
  2. ゴールドが 100 を超えるまで遊びます。「Big Spender」のカードが出て、ページでも解放済みになります
  3. 戦闘に 6 回勝ちます。Battles won が 5 から 6 になったところで「Seasoned Fighter」が出ます
  4. 「この洞窟を探索したい」と送ります。「Trailblazer」が出ます。あとで AI が隠し通路のことを話しても、もう一度出ることはありません

プレイテストの右側にある このターン で、どのビヘイビアが発動したか、発動しなかったのはなぜかを確認できます。

うまくいかないとき:

症状考えられる原因直し方
ゴールドが 100 を超えているのに何も出ないゴールドが線を越えていない。最初から 100 より上だったしきい値は越えた瞬間にだけ発動します。初期値 がしきい値以下になっているか確認します
場面は出たのにページが変わらないどのリストに集めるか が空になっている、またはページが別の変数を見ているどちらも Achievements を選びます
出てきたのにページではロックのまま行のタイトルと場面のタイトルが完全には一致していない大文字・小文字も含めて同じにします
実績が二度出た2 つのビヘイビアでタイトルかリストが違うタイトルもリストも同じにします

コードで作る場合:チャットの下に実績パネルを出す ​

パネルを自分で描きたい場合、たとえば別ページではなく最後のメッセージの下に出したい場合は、画面のコードで書きます。上のビヘイビアはそのままで、コードは Achievements のリストを読むだけです。

パネル → フロントエンドコード で index.tsx を開き、デフォルトの return <Chat /> を置き換えます。

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

  // Titles must match the moment titles in your behaviors
  const achievements = [
    { title: "Big Spender", desc: "Save more than 100 gold", icon: "💰" },
    { title: "Seasoned Fighter", desc: "Win more than 5 battles", icon: "⚔️" },
    { title: "Trailblazer", desc: "Find a hidden area or secret", icon: "🗺️" },
  ];

  // The list the moments are collected into, read by its name
  const unlockedList = Array.isArray(api.variables["Achievements"])
    ? api.variables["Achievements"].map(String)
    : [];
  const isUnlocked = (a) => unlockedList.includes(a.title);
  const unlockedCount = achievements.filter(isUnlocked).length;

  return (
    <Chat renderBubble={(msg) => {
      const isLastMsg = msg.messageIndex === msgs.length - 1;
      return (
        <div>
          <div
            style={{ color: "#e2e8f0", lineHeight: 1.7 }}
            dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
          />

          {isLastMsg && (
            <div style={{
              marginTop: "16px",
              padding: "12px 16px",
              background: "linear-gradient(135deg, #1c1917, #292524)",
              border: "1px solid #44403c",
              borderRadius: "10px",
            }}>
              <div style={{
                display: "flex",
                justifyContent: "space-between",
                alignItems: "center",
                marginBottom: "10px",
              }}>
                <span style={{ fontSize: "13px", fontWeight: "bold", color: "#fbbf24" }}>
                  🏆 Achievements
                </span>
                <span style={{ fontSize: "12px", color: "#a8a29e" }}>
                  {unlockedCount} / {achievements.length}
                </span>
              </div>

              <div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
                {achievements.map((a) => {
                  const unlocked = isUnlocked(a);
                  return (
                    <div
                      key={a.title}
                      style={{
                        display: "flex",
                        alignItems: "center",
                        gap: "10px",
                        padding: "6px 8px",
                        borderRadius: "6px",
                        background: unlocked
                          ? "rgba(251, 191, 36, 0.08)"
                          : "rgba(120, 113, 108, 0.08)",
                      }}
                    >
                      <span style={{ fontSize: "18px", opacity: unlocked ? 1 : 0.3 }}>
                        {unlocked ? a.icon : "🔒"}
                      </span>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{
                          fontSize: "13px",
                          fontWeight: "600",
                          color: unlocked ? "#fbbf24" : "#78716c",
                        }}>
                          {a.title}
                        </div>
                        <div style={{
                          fontSize: "11px",
                          color: unlocked ? "#a8a29e" : "#57534e",
                          marginTop: "1px",
                        }}>
                          {a.desc}
                        </div>
                      </div>
                      {unlocked && (
                        <span style={{ fontSize: "11px", color: "#fbbf24" }}>✓ Unlocked</span>
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </div>
      );
    }} />
  );
}

コードがしていること:

  • <Chat renderBubble={...} /> で、プラットフォームのメッセージ一覧、入力欄、スクロールはそのまま使い、吹き出しの中身だけを自分で描きます
  • msg.messageIndex === msgs.length - 1 で、パネルを最後のメッセージの下にだけ出します
  • msg.contentHtml は、HTML に変換済みのメッセージです
  • api.variables["Achievements"] で、変数名を使ってリストを読みます。スペースや大文字を含む名前も、このかっこの書き方なら読めます
  • 実績は、タイトルがリストに入っていれば解放済みとみなします。場面と同じ判定です

パネル → フロントエンドコード のファイル名の横の表示が、緑の OK になっていれば大丈夫です。エラー になっているときは、その下のメッセージに何行目かが書いてあります。

コードを自分で書きたくないときは

キャンバス右上の AIアシスタント を押して、ほしいパネルを説明してください。画面のコードを書いてくれます。


「変数がしきい値を越える」と「変数が変化」の違い ​

どちらも変数を見るトリガーですが、発動するタイミングが違います。

変数がしきい値を越える は、数値がある線を越えたその瞬間に発動します。

gold: 80 → 95 → 101   ← 95→101 で発動(100 を上に越えた)
gold: 101 → 150 → 200  ← 発動しない(もう上にいる)
gold: 200 → 50 → 120   ← 50→120 で発動(もう一度 100 を上に越えた)

節目、警告(「体力が 20 を下回った」)、死亡判定に使います。

変数が変化 は、選んだ変数が変わるたびに、値に関係なく発動します。

gold: 80 → 95   ← 発動
gold: 95 → 101  ← 発動
gold: 101 → 150 ← 発動

役に立てるには 次の場合のみ の条件(「Gold ≥ 100 の場合のみ」)が必要で、変わるたびに毎回チェックし直します。実績で大事なのは線を越えた瞬間なので、変数がしきい値を越える のほうが自然に合います。


実績を増やす ​

実績を 1 つ増やすには、ビヘイビアを 1 つと実績ページの行を 1 つ足します。

実績発動するとき
おしゃべり好きNターンごと:50、最大発動回数 は 1
ため込み屋変数がしきい値を越える:Gold が 500 を上回る
人気者AIがキーワードを発言:あなたを信頼, 友達になる
九死に一生変数がしきい値を越える:Health が 10 を下回ったらスイッチ「死にかけた」をオンにする。2 つめのビヘイビアで Health が 50 を上回ったとき、次の場合のみ「死にかけた」がオン、の条件で場面を解放する

早見表 ​

やりたいことやり方
数値が目標に届いたら解放発動するとき:変数がしきい値を越える、上回る
何かが話されたら解放プレイヤーがキーワードを発言 か AIがキーワードを発言
カードを出して覚えておく効果:ひとつの場面を解放、どのリストに集めるか を指定
一度しか解放しないようにするタイトルもリストも同じにします。リストに入っているタイトルの場面は発動しません
新しくプレイしても実績を残すリスト変数で 周回をまたいで保持(プレイヤーごと) にチェック
見つけた実績とロック中の実績を見せるプレイヤー画面 → コレクション テンプレート、または ロック解除の条件 を付けたリストの部品
数を数える計算式 len(Achievements) の数値変数
独自のパネルリストを読む画面のコード(上の「コードで作る場合」)

これはレシピ #13 です

実績はほかのレシピと組み合わせられます。戦闘システムの勝利数を数えたり、ショップのゴールドを見たり、クエストをクリアしたら解放したり。ビヘイビアどうしは干渉しないので、実績を足してもカードのほかの部分の動きは変わりません。