角色创建表单
故事开始之前,玩家先填一张角色表:名字、职业、一小段背景故事。然后点「开始冒险」,故事开始,AI 已经知道他是谁了。玩家界面里有专门做这个的模板,所以表单不用写代码。
你要做的东西
玩家在第一条消息之前要走过的几页:
- 一个名字输入框
- 一个职业选择:战士、法师、盗贼,每个配一句说明
- 一个背景故事输入框,可以不填
- 一个确认页,把选的东西汇总一遍,再用一个按钮开始故事
玩家填的所有东西都存进普通的变量里。AI 从第一条回复起就看得到。
怎么运作
1. 玩家开一局新游戏 → 先出现第一张开局页,而不是聊天
2. 填好名字和背景故事 → 存进变量 玩家名字 和 背景故事
3. 选一个职业 → 存进变量 职业
4. 确认 → 聊天从你的开场白开始
5. 玩家发出第一条消息
→ AI 的提示里有这些变量、它们的行为规则,
还有只在选了这个职业时才发的设定
→ AI 用名字称呼玩家,按他的职业来写一步一步来
第 1 步:从模板加开局页
点顶栏中间的 玩家界面,再点 模板。在 开局 · 玩家进故事之前 下面,加上:
| 模板 | 它给你什么 |
|---|---|
| 建档 | 名字、性别、年龄、外貌和一个秘密,边填边出人物卡。只有名字必填 |
| 选出身 | 一个有三个选项的卡片选择。你会把它改成职业选择 |
| 开局确认页 | 把前面选的汇总一遍。总是排在最后 |
故事开始前,玩家会按顺序走过这几页。每个模板都写着它会给卡加哪些变量(「会给这张卡加这些变量」)。这些都是普通变量,已经配好了告诉 AI 它们是什么意思的行为规则,比如「玩家的名字。故事里的人都用这个名字称呼玩家,不要另起名字。」
只要一个名字?
填名字进故事 模板只问名字和玩家希望被怎么称呼,别的都不问。
第 2 步:把选出身改成选职业
在左边的页面栏里选中 选出身 这一页,改一改:
- 点标题,改成「选择你的职业」
- 选中卡片选择,把三个选项改名为 战士(「近战专精,血量高」)、法师(「远程魔法,法力高」)和 盗贼(「灵活隐秘,暴击高」)
- 在页面栏里双击这一页,改名为「职业」
这个选择会把选中的选项存进变量 出身。回到画布,在变量块里找到出身,改名为 职业,再重写它的 行为规则:
玩家开局时选的职业:战士、法师或盗贼。它决定玩家战斗时能做什么,以及别人怎么看待他。不要改它。然后展开 AI 怎么用它,把 AI 权限 设成 AI 只读,这样 AI 改不了玩家的职业。
第 3 步:精简建档页
在 建档 这一页上,留下名字,再把一个输入框改成背景故事:
- 删掉不想要的部件(选中它们,或者在 图层 里找到,按 Delete)。之后可以在画布上把它们的变量也删掉
- 把「一个秘密」这个输入框的标签改成「背景故事(可选)」,提示文字改成「用几句话写写你的过去」。然后在画布上把它的变量从 玩家的秘密 改名为 背景故事,行为规则改成
玩家自己写的背景故事。要用上,不要和它矛盾。(模板自带的规则把它当成故事里没人知道的秘密,这不是背景故事该有的用法)
玩家名字 保留模板的规则就行。如果想防着玩家不填名字直接跳过,选中开始按钮,在 这些填好了才能按 里勾上玩家名字。这样不填名字,按钮就一直是灰的。
第 4 步(可选):每个职业配一条自己的设定
变量告诉 AI 玩家选了什么。想让每个职业有更多可以发挥的内容,就每个职业加一条设定,只在选了这个职业时才发给 AI:
- 加一条设定「法师设定」,写法师在这个世界里懂什么、能做什么
- 把 职业 变量拖到这条设定上,点那根线。把条件设成职业 等于
法师 - 战士和盗贼也照做
这样玩法师的玩家,AI 拿到的是法师那条,另外两条不会发。挂条件的设定见开场白和设定。
第 5 步:写开场白
开场白(画布上的 开场白)是表单之后的第一条消息。照平常那样写:
*命运之门缓缓打开,晨光洒满了古老之地。*
*远处城市的轮廓在前方微微发亮,一条石板路一直通向那里。风里带着青草和远处炊烟的味道。*
你面前有三条路:一条宽阔的大路通往镇上,一条小径穿过树林,还有一道斜坡下到河边。你往哪走?别把玩家的选择写进开场白
开场白在一局游戏创建时就写好了,那时玩家还没填表。开场白里的 {{玩家名字}} 宏显示的是变量的初始值,不是玩家填的内容。名字和职业交给 AI 就行:它的第一条回复就已经知道了。
第 6 步:试玩
在玩家界面里关掉 编辑,就能一页页点过去,不花额度。然后点顶栏的 试玩:
- 出现建档页。填「艾拉」和一句背景故事
- 选 法师
- 确认页列出你的选择。点它的开始按钮
- 开场白出现。发一句「我朝镇子走去」
- AI 叫你艾拉,把你写成法师(法杖、咒语、路人注意到你的法袍)
试玩右边的 这回合 能看到 AI 拿到了哪些变量和设定。
如果出了问题:
| 现象 | 可能的原因 | 怎么改 |
|---|---|---|
| 表单没出现 | 这几页不在开局的那一串里 | 它们必须来自 开局 · 玩家进故事之前 里的模板,确认页排在最后 |
| 有一页挂着 没出口 的提醒 | 这一页上没有能去别处的按钮 | 给它加一个 跳到页面 的按钮 |
| AI 不管职业 | 职业变量没写行为规则,或者 AI 看不到变量 | 给它写行为规则,再看看卡的 Context 里 变量给 AI 的方式 是不是 不给 |
| 职业设定一直没进来 | 条件里的值和选项不完全一样 | 条件里必须写 法师,和选项的标题一字不差 |
| AI 给玩家改了名字 | 玩家名字是 AI 能改的 | 把它的 AI 权限 设成 AI 只读 |
写代码的做法:放在第一条消息里的表单
如果想让表单由你自己的界面代码来画,比如就放在第一条消息下面,下面是代码版。它用三个文字变量 player_name、player_class 和 player_backstory(都是 AI 只读,player_name 初始值是 旅人),还有两条开场白:第一条是一小段「你是谁?」的场景,第二条才是真正的故事开头。
界面代码按变量名读写变量。面板 → 前端代码 → 打开 index.tsx,把默认的 return <Chat /> 换掉:
export default function MyWorld() {
const api = useYumina();
const [name, setName] = React.useState("");
const [selectedClass, setSelectedClass] = React.useState("");
const [backstory, setBackstory] = React.useState("");
// 职业存好了,表单就算填完了
const hasCreated = String(api.variables.player_class || "") !== "";
const classes = [
{ id: "战士", label: "战士", icon: "⚔️", desc: "近战专精,血量高" },
{ id: "法师", label: "法师", icon: "🔮", desc: "远程魔法,法力高" },
{ id: "盗贼", label: "盗贼", icon: "🗡️", desc: "灵活隐秘,暴击高" },
];
const handleStart = async () => {
if (!selectedClass) return;
// 先切到第二条开场白:切换会把变量
// 重置成那条开场白的初始值
await api.switchGreeting(1);
api.setVariable("player_name", name.trim() || "旅人");
api.setVariable("player_class", selectedClass);
api.setVariable("player_backstory", backstory.trim());
};
const label = { fontSize: "13px", color: "#a5b4fc", marginBottom: "6px", fontWeight: "600" };
const field = {
width: "100%", padding: "10px 14px", background: "#0f172a",
border: "1px solid #334155", borderRadius: "8px", color: "#e2e8f0",
fontSize: "14px", outline: "none", boxSizing: "border-box", fontFamily: "inherit",
};
return (
<Chat renderBubble={(msg) => (
<div>
<div
style={{ color: "#e2e8f0", lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
/>
{msg.messageIndex === 0 && !hasCreated && (
<div style={{
marginTop: "20px",
padding: "24px",
background: "linear-gradient(135deg, #1e1b4b 0%, #1a1a2e 100%)",
borderRadius: "16px",
border: "1px solid #312e81",
}}>
<div style={{ fontSize: "18px", fontWeight: "bold", color: "#c4b5fd", marginBottom: "20px", textAlign: "center" }}>
创建你的角色
</div>
<div style={{ marginBottom: "16px" }}>
<div style={label}>角色名字</div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="不填就叫「旅人」"
style={field}
/>
</div>
<div style={{ marginBottom: "16px" }}>
<div style={{ ...label, marginBottom: "8px" }}>选择职业</div>
<div style={{ display: "flex", gap: "10px" }}>
{classes.map((cls) => {
const picked = selectedClass === cls.id;
return (
<button
key={cls.id}
onClick={() => setSelectedClass(cls.id)}
style={{
flex: 1,
padding: "14px 10px",
background: picked ? "linear-gradient(135deg, #4338ca, #6366f1)" : "#1e293b",
border: picked ? "2px solid #818cf8" : "1px solid #334155",
borderRadius: "10px",
color: picked ? "#e0e7ff" : "#94a3b8",
cursor: "pointer",
textAlign: "center",
}}
>
<div style={{ fontSize: "24px", marginBottom: "4px" }}>{cls.icon}</div>
<div style={{ fontSize: "14px", fontWeight: "bold", marginBottom: "2px" }}>{cls.label}</div>
<div style={{ fontSize: "11px", opacity: 0.7 }}>{cls.desc}</div>
</button>
);
})}
</div>
</div>
<div style={{ marginBottom: "20px" }}>
<div style={label}>背景故事(可选)</div>
<textarea
value={backstory}
onChange={(e) => setBackstory(e.target.value)}
placeholder="用几句话写写你的角色的过去……"
rows={3}
style={{ ...field, resize: "vertical" }}
/>
</div>
<button
onClick={handleStart}
disabled={!selectedClass}
style={{
width: "100%",
padding: "14px",
background: selectedClass ? "linear-gradient(135deg, #7c3aed, #a855f7)" : "#374151",
border: "none",
borderRadius: "10px",
color: selectedClass ? "#f5f3ff" : "#6b7280",
fontSize: "16px",
fontWeight: "bold",
cursor: selectedClass ? "pointer" : "not-allowed",
}}
>
{selectedClass ? "开始冒险" : "先选一个职业"}
</button>
</div>
)}
</div>
)} />
);
}代码做了什么:
msg.messageIndex === 0 && !hasCreated让表单只出现在第一条消息上,而且只到职业存好为止- 三个
React.useState存着玩家正在填的内容,直到他点 开始冒险 api.switchGreeting(1)把第一条消息换成第二条开场白(开场白从 0 开始数)。切换会把变量重置成那条开场白的初始值,所以代码先等切换完成,再写变量。顺序反过来的话,切换会把它们清掉api.setVariable("player_class", ...)按变量名写入
AI 在下一次的提示里就会读到这些变量。想让一条设定把人物资料写清楚,也可以在一条每回合都发的设定里写宏:
[玩家角色]
名字:{{player_name}}
职业:{{player_class}}
背景故事:{{player_backstory}}设定里的宏在每次拼 AI 的提示时填进去,所以永远是当前的值。开场白里的宏不是这样:开场白在一局游戏创建时就写好了,所以那里的 {{player_name}} 显示的是初始值(「旅人」)。
面板 → 前端代码 里文件旁边的状态应该是绿色的 正常。
在后面的消息上显示角色
在 renderBubble 里,消息正文上面加:
{hasCreated && (
<div style={{ display: "flex", gap: "8px", marginBottom: "8px", fontSize: "12px", color: "#a5b4fc" }}>
<span>{String(api.variables.player_name)}</span>
<span style={{ opacity: 0.5 }}>|</span>
<span>{String(api.variables.player_class)}</span>
</div>
)}不想自己写代码?
点画布右上角的 创作助手,说说你想要什么样的表单。
再进一步
更多职业
用模板的做法,就在卡片选择里加一个选项,再加一条对应的设定。用代码的做法,就在 classes 数组里加一项。
属性点
分配属性点 模板把六点分到力量、敏捷、魅力、智力上。把它放在职业页和确认页之间。
每个职业不同的开局
如果每个职业要从完全不同的地方开始,就每个职业写一条开场白,再加上 选开局 模板(至少要两条开场白)。和某条开场白绑定的情境,只在从那条开场白开始的那局里存在。见情境。
速查
| 你想要 | 怎么做 |
|---|---|
| 故事之前先填一张表 | 玩家界面 → 模板 → 开局 · 玩家进故事之前 |
| 只要名字 | 填名字进故事 模板 |
| 好几个输入项 | 建档 模板 |
| 几个选项里挑一个 | 选出身 模板(一个卡片选择),改个名字 |
| 开始前汇总一遍 | 开局确认页 模板,总是排在最后 |
| 某一项必须填 | 开始按钮 → 这些填好了才能按 |
| 不让 AI 改玩家的选择 | AI 权限:AI 只读 |
| 每个选择配不同的设定 | 挂了变量条件的设定 |
| 把选择写进设定正文 | 在设定里写 {{player_name}} 这样的宏(别写在开场白里) |
| 代码:先切开场白,再存选择 | await api.switchGreeting(1),然后 api.setVariable(...) |
自己试试:可导入的演示
下载这个 JSON 导入进去,就能看到能跑的版本:
怎么导入:
- 点左边的 创建,选 空白项目,再选 画布
- 在顶栏点 ⋮ → 导入文件,选下载好的
.json文件。它会替换掉这张新卡里的所有内容 - 点 保存,再点 试玩
演示用的是写代码的版本:两条开场白、三个变量、一条带宏的人物资料设定,表单写在界面代码里。
