成就系统
玩家达到某个里程碑的时候(金币超过 100、打赢 5 场以上、找到隐藏区域),弹出一张小卡片,写着成就名和一句话。每个成就只解锁一次,解锁后收进一个列表,玩家界面里有一页列出哪些已经拿到、哪些还锁着。整套都不用写代码。
你要做的东西
- 达成的那一刻弹出来:一张小卡片,有标题、一句话,图片可加可不加
- 自动检测:行为盯着数值和对话,玩家什么都不用做
- 每个成就只解锁一次:已经在列表里的时刻不会再弹第二次
- 成就页:所有成就列在一起,解锁的显示说明,没解锁的显示提示
- 可选:重开一局也还在的成就
怎么运作
玩家的 Gold 从 95 涨到 101
→ 行为「小富翁」在盯着 Gold(变量越过阈值,上升到以上 100)
→ 它的效果「解锁一个时刻」弹出卡片「小富翁」
→ 标题「小富翁」被加进列表变量 Achievements
→ 成就页上「小富翁」显示为已解锁数值是 AI 跟着剧情在改,但成就解没解锁是引擎说了算,所以不会漏掉,也不会发两次。
一步一步来
第 1 步:建变量
点卡底下 还可以加 里的 + 变量,下面每个变量建一次。
Gold
| 字段 | 值 |
|---|---|
| 名称 | Gold |
| 类型 | 数值 |
| 初始值 | 0 |
| 范围 | 0 到(不填) |
| 行为规则 | 玩家的金币。拿到报酬、捡到东西、卖掉东西时增加;花钱时减少。 |
Battles won
| 字段 | 值 |
|---|---|
| 名称 | Battles won |
| 类型 | 数值 |
| 初始值 | 0 |
| 行为规则 | 玩家打赢了几场战斗。玩家明确打赢一场时加 1。 |
Achievements
| 字段 | 值 |
|---|---|
| 名称 | Achievements |
| 类型 | 列表/表格 |
| 初始值 | [] |
展开 AI 怎么用它,把 AI 权限 设成 仅引擎(对 AI 隐藏)。这个列表只由行为来写,AI 读了也没用。
重开一局也还在的成就
在同一栏里勾上 跨存档保留(每个玩家自己的)。这样玩家重开一局时,列表从他已经解锁的成就开始,这些成就也不会再弹一次。CG 图鉴、「已达成结局」收集都是这么做的。
第 2 步:每个成就建一条行为
点 还可以加 里的 + 行为。
行为 1:小富翁
| 部分 | 设置 |
|---|---|
| 什么时候触发 | 变量越过阈值:Gold,上升到以上,100 |
| 效果 | 解锁一个时刻 |
把时刻填好:
| 字段 | 值 |
|---|---|
| 标题 | 小富翁 |
| 消息 | 你攒下了 100 多金币。 |
| 图片(链接) | 可选:一个 https:// 链接,或者 面板 → 资源 里的素材 |
| 收进哪个列表 | Achievements |
「变量越过阈值」只在数值从 100 或以下变成 100 以上的那一下触发。金币从 101 继续涨到 150,不会再触发。金币掉回 100 以下再涨上来,会再触发一次,但这个时刻已经在 Achievements 里了,所以什么都不会弹。
行为 2:老练战士
| 部分 | 设置 |
|---|---|
| 什么时候触发 | 变量越过阈值:Battles won,上升到以上,5 |
| 效果 | 解锁一个时刻:标题 老练战士,消息 你已经打赢了 5 场以上。,收进 Achievements |
行为 3:开路先锋(关键词)
这个盯的是对话,不是数值。一条行为只有一个触发,所以想同时抓玩家说的和 AI 说的,就建两条行为。
3a
| 部分 | 设置 |
|---|---|
| 什么时候触发 | 玩家说出关键词:探索, 搜索山洞 |
| 效果 | 解锁一个时刻:标题 开路先锋,消息 你去找了藏起来的东西。,收进 Achievements |
3b
| 部分 | 设置 |
|---|---|
| 什么时候触发 | AI 说出关键词:暗道, 密室 |
| 效果 | 和 3a 一样的 解锁一个时刻,标题也一样 |
关键词用逗号隔开,出现任意一个就算。两条行为收进同一个列表、用同一个标题,所以谁先触发谁解锁,另一条发现列表里已经有了,就不出声。不用另外加条件。
用时刻还是用通知?
显示通知(样式选 成功)也能弹一句「成就解锁」,但它什么都不记:你得每个成就配一个开关变量,加一个条件防止触发两次,再把 最大触发次数 设成 1。解锁一个时刻 把这些记账的事都替你做了。
第 3 步:做成就页
点顶栏中间的 玩家界面。用 新建一页 加一页(选 空白页 就行),起个标题,再从 加一个部件 里加一个 列表:
- 内容从哪来 选 自己写几行,每个成就加一行:
小富翁、老练战士、开路先锋,每行配一句话 - 打开 每一行画成一张卡
- 在 锁住直到 里打开 没解锁的行显示成锁住。看哪个变量 选 Achievements,对照这一行的哪个字段 保持
title不动。列表里有这一行的标题,这一行就解锁,解锁一个时刻 放进去的正好就是标题 - 在 锁住时显示 里写解锁前玩家看到的提示,比如「先攒点钱」
- 加一个 按钮,步骤选 跳到页面,跳回对话页,免得这一页走不出去(不然页面栏会提醒 没出口)
- 在对话页上加一个写着「成就」的 按钮,步骤是 跳到页面 → 你刚建的那一页
如果你的收集品是 AI 在剧情里发的,而不是行为发的,就改从 收集图鉴 模板开始:一开始都是灰的,剧情发给玩家一个就亮一个。它自带列表变量、弹窗,还有告诉 AI 什么时候往里加的行为规则。
把顶上的 编辑 关掉,就能像玩家一样点着试试这一页。
数一数解锁了几个
加一个数值变量叫「Achievements unlocked」,公式(自动算) 填 len(Achievements)。在页面上放一个文字部件写 {Achievements unlocked} / 3,数字会自己更新。
第 4 步:试玩
点顶栏的 试玩。
- 点 成就:三个都锁着,显示各自的提示
- 一直玩到金币超过 100。「小富翁」卡片弹出来,成就页上显示已解锁
- 打赢六场。Battles won 从 5 变成 6 的时候,「老练战士」弹出来
- 发一句「我想探索一下这个山洞」。「开路先锋」弹出来。之后 AI 提到暗道,不会再弹
试玩右边的 这回合 能看到哪些行为触发了,没触发的是为什么。
如果出了问题:
| 现象 | 可能的原因 | 怎么改 |
|---|---|---|
| 金币过了 100 但什么都没弹 | 金币根本没越过这条线:一开始就在 100 以上 | 阈值只在越过的那一下触发。检查 初始值 是不是等于或低于阈值 |
| 时刻弹了,但成就页没变 | 收进哪个列表 没填,或者页面看的是另一个变量 | 两边都选 Achievements |
| 已经弹过了,成就页还显示锁着 | 这一行的标题和时刻的标题不完全一样 | 改成一模一样,大小写也要一样 |
| 一个成就弹了两次 | 两条行为用了不同的标题或不同的列表 | 同一个标题、同一个列表 |
写代码的做法:聊天下方的成就面板
如果想自己画这个面板,比如放在最后一条消息下面,而不是单独一页,就在界面代码里写。上面的行为一点都不用改,代码只是读 Achievements 这个列表。
面板 → 前端代码 → 打开 index.tsx,把默认的 return <Chat /> 换掉:
export default function MyWorld() {
const api = useYumina();
const msgs = api.messages || [];
// 标题要和行为里时刻的标题一致
const achievements = [
{ title: "小富翁", desc: "攒下 100 多金币", icon: "💰" },
{ title: "老练战士", desc: "打赢 5 场以上", icon: "⚔️" },
{ title: "开路先锋", desc: "找到隐藏区域或秘密", icon: "🗺️" },
];
// 时刻收进去的那个列表,按名字读
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" }}>
🏆 成就
</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" }}>✓ 已解锁</span>
)}
</div>
);
})}
</div>
</div>
)}
</div>
);
}} />
);
}代码做了什么:
<Chat renderBubble={...} />保留平台自带的消息列表、输入框和滚动,你只负责画每个气泡msg.messageIndex === msgs.length - 1让面板只出现在最后一条消息下面msg.contentHtml是已经渲染成 HTML 的消息api.variables["Achievements"]按变量名读这个列表。名字里有空格或大写字母,用方括号的写法都能读- 标题在列表里,这个成就就算解锁,和时刻用的是同一条规则
面板 → 前端代码 里文件旁边的状态应该是绿色的 正常。如果显示 错误,下面的提示会告诉你是哪一行。
不想自己写代码?
点画布右上角的 创作助手,说说你想要什么样的面板,它会替你写界面代码。
「变量越过阈值」和「变量变化」
两个触发都是盯变量的,但触发的时机不一样。
变量越过阈值 在数值越过一条线的那一下触发:
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」),而且每变一次都要重新检查一遍。成就关心的是越过的那一下,所以用 变量越过阈值 最顺手。
更多成就
每多一个成就,就是一条行为,加上成就页上的一行:
| 成就 | 什么时候触发 |
|---|---|
| 话痨 | 每 N 回合:50,最大触发次数 填 1 |
| 囤积狂 | 变量越过阈值:Gold 上升到 500 以上 |
| 社交达人 | AI 说出关键词:信任你, 成了朋友 |
| 死里逃生 | 变量越过阈值:Health 下降到 10 以下时,把开关「Was nearly dead」打开;第二条行为在 Health 上升到 50 以上、如果 Was nearly dead 是开着的时候,解锁这个时刻 |
速查
| 你想要 | 怎么做 |
|---|---|
| 数值达标时解锁 | 什么时候触发:变量越过阈值,上升到以上 |
| 说了某句话时解锁 | 玩家说出关键词 或 AI 说出关键词 |
| 弹一张卡片并记住它 | 效果:解锁一个时刻,填好 收进哪个列表 |
| 确保只解锁一次 | 同一个标题、同一个列表。列表里已经有的标题,时刻不会再触发 |
| 重开一局成就还在 | 在列表变量上勾 跨存档保留(每个玩家自己的) |
| 显示已拿到和还锁着的成就 | 玩家界面 → 收集图鉴 模板,或者一个打开了 锁住直到 的列表部件 |
| 数一数 | 一个数值变量,公式填 len(Achievements) |
| 自己画的面板 | 读这个列表的界面代码(见上面写代码的做法) |
自己试试:可导入的演示
下载这个 JSON 导入进去,就能看到能跑的版本:
怎么导入:
- 点左边的 创建,选 空白项目,再选 画布
- 在顶栏点 ⋮ → 导入文件,选下载好的
.json文件。它会替换掉这张新卡里的所有内容 - 点 保存,再点 试玩
演示用的是写代码的版本:每个成就一个开关变量,行为负责打开它们并弹通知,成就面板写在界面代码里。
