Skip to content

角色创建表单 ​

故事开始之前,玩家先填一张角色表:名字、职业、一小段背景故事。然后点「开始冒险」,故事开始,AI 已经知道他是谁了。玩家界面里有专门做这个的模板,所以表单不用写代码。


你要做的东西 ​

玩家在第一条消息之前要走过的几页:

  • 一个名字输入框
  • 一个职业选择:战士、法师、盗贼,每个配一句说明
  • 一个背景故事输入框,可以不填
  • 一个确认页,把选的东西汇总一遍,再用一个按钮开始故事

玩家填的所有东西都存进普通的变量里。AI 从第一条回复起就看得到。

怎么运作 ​

1. 玩家开一局新游戏 → 先出现第一张开局页,而不是聊天
2. 填好名字和背景故事 → 存进变量 玩家名字 和 背景故事
3. 选一个职业 → 存进变量 职业
4. 确认 → 聊天从你的开场白开始
5. 玩家发出第一条消息
   → AI 的提示里有这些变量、它们的行为规则,
     还有只在选了这个职业时才发的设定
   → AI 用名字称呼玩家,按他的职业来写

一步一步来 ​

第 1 步:从模板加开局页 ​

点顶栏中间的 玩家界面,再点 模板。在 开局 · 玩家进故事之前 下面,加上:

模板它给你什么
建档名字、性别、年龄、外貌和一个秘密,边填边出人物卡。只有名字必填
选出身一个有三个选项的卡片选择。你会把它改成职业选择
开局确认页把前面选的汇总一遍。总是排在最后

故事开始前,玩家会按顺序走过这几页。每个模板都写着它会给卡加哪些变量(「会给这张卡加这些变量」)。这些都是普通变量,已经配好了告诉 AI 它们是什么意思的行为规则,比如「玩家的名字。故事里的人都用这个名字称呼玩家,不要另起名字。」

只要一个名字?

填名字进故事 模板只问名字和玩家希望被怎么称呼,别的都不问。

第 2 步:把选出身改成选职业 ​

在左边的页面栏里选中 选出身 这一页,改一改:

  1. 点标题,改成「选择你的职业」
  2. 选中卡片选择,把三个选项改名为 战士(「近战专精,血量高」)、法师(「远程魔法,法力高」)和 盗贼(「灵活隐秘,暴击高」)
  3. 在页面栏里双击这一页,改名为「职业」

这个选择会把选中的选项存进变量 出身。回到画布,在变量块里找到出身,改名为 职业,再重写它的 行为规则:

玩家开局时选的职业:战士、法师或盗贼。它决定玩家战斗时能做什么,以及别人怎么看待他。不要改它。

然后展开 AI 怎么用它,把 AI 权限 设成 AI 只读,这样 AI 改不了玩家的职业。

第 3 步:精简建档页 ​

在 建档 这一页上,留下名字,再把一个输入框改成背景故事:

  • 删掉不想要的部件(选中它们,或者在 图层 里找到,按 Delete)。之后可以在画布上把它们的变量也删掉
  • 把「一个秘密」这个输入框的标签改成「背景故事(可选)」,提示文字改成「用几句话写写你的过去」。然后在画布上把它的变量从 玩家的秘密 改名为 背景故事,行为规则改成 玩家自己写的背景故事。要用上,不要和它矛盾。(模板自带的规则把它当成故事里没人知道的秘密,这不是背景故事该有的用法)

玩家名字 保留模板的规则就行。如果想防着玩家不填名字直接跳过,选中开始按钮,在 这些填好了才能按 里勾上玩家名字。这样不填名字,按钮就一直是灰的。

第 4 步(可选):每个职业配一条自己的设定 ​

变量告诉 AI 玩家选了什么。想让每个职业有更多可以发挥的内容,就每个职业加一条设定,只在选了这个职业时才发给 AI:

  1. 加一条设定「法师设定」,写法师在这个世界里懂什么、能做什么
  2. 把 职业 变量拖到这条设定上,点那根线。把条件设成职业 等于 法师
  3. 战士和盗贼也照做

这样玩法师的玩家,AI 拿到的是法师那条,另外两条不会发。挂条件的设定见开场白和设定。

第 5 步:写开场白 ​

开场白(画布上的 开场白)是表单之后的第一条消息。照平常那样写:

*命运之门缓缓打开,晨光洒满了古老之地。*

*远处城市的轮廓在前方微微发亮,一条石板路一直通向那里。风里带着青草和远处炊烟的味道。*

你面前有三条路:一条宽阔的大路通往镇上,一条小径穿过树林,还有一道斜坡下到河边。你往哪走?

别把玩家的选择写进开场白

开场白在一局游戏创建时就写好了,那时玩家还没填表。开场白里的 {{玩家名字}} 宏显示的是变量的初始值,不是玩家填的内容。名字和职业交给 AI 就行:它的第一条回复就已经知道了。

第 6 步:试玩 ​

在玩家界面里关掉 编辑,就能一页页点过去,不花额度。然后点顶栏的 试玩:

  1. 出现建档页。填「艾拉」和一句背景故事
  2. 选 法师
  3. 确认页列出你的选择。点它的开始按钮
  4. 开场白出现。发一句「我朝镇子走去」
  5. AI 叫你艾拉,把你写成法师(法杖、咒语、路人注意到你的法袍)

试玩右边的 这回合 能看到 AI 拿到了哪些变量和设定。

如果出了问题:

现象可能的原因怎么改
表单没出现这几页不在开局的那一串里它们必须来自 开局 · 玩家进故事之前 里的模板,确认页排在最后
有一页挂着 没出口 的提醒这一页上没有能去别处的按钮给它加一个 跳到页面 的按钮
AI 不管职业职业变量没写行为规则,或者 AI 看不到变量给它写行为规则,再看看卡的 Context 里 变量给 AI 的方式 是不是 不给
职业设定一直没进来条件里的值和选项不完全一样条件里必须写 法师,和选项的标题一字不差
AI 给玩家改了名字玩家名字是 AI 能改的把它的 AI 权限 设成 AI 只读

写代码的做法:放在第一条消息里的表单 ​

如果想让表单由你自己的界面代码来画,比如就放在第一条消息下面,下面是代码版。它用三个文字变量 player_name、player_class 和 player_backstory(都是 AI 只读,player_name 初始值是 旅人),还有两条开场白:第一条是一小段「你是谁?」的场景,第二条才是真正的故事开头。

界面代码按变量名读写变量。面板 → 前端代码 → 打开 index.tsx,把默认的 return <Chat /> 换掉:

tsx
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 里,消息正文上面加:

tsx
{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 导入进去,就能看到能跑的版本:

recipe-4-demo-zh.json

怎么导入:

  1. 点左边的 创建,选 空白项目,再选 画布
  2. 在顶栏点 ⋮ → 导入文件,选下载好的 .json 文件。它会替换掉这张新卡里的所有内容
  3. 点 保存,再点 试玩

演示用的是写代码的版本:两条开场白、三个变量、一条带宏的人物资料设定,表单写在界面代码里。


这是食谱 #4

场景跳转与设定切换里更详细地讲了用按钮切换开场白、在设定里用宏。角色创建常常是别的食谱的第一步:职业可以决定商店的初始金币,或者背包里的初始物品。