Skip to content

地图与场景切换 ​

一张玩家能点的地图:选森林,场景就移到森林。AI 拿到森林的氛围,音乐淡到森林的曲子,AI 的下一次回复描写抵达的过程。用一个变量、条件设定、音乐规则和玩家界面上的按钮搭出来,不用写代码。


你要做的东西 ​

  • 一个地图页,四个地点:村庄、森林、山洞、集市
  • 标出当前所在的地点,它的按钮不能再点
  • 点一个地点,场景就移过去:位置变了,那个地点的设定发给 AI,玩家的一句「(我前往森林)」发进聊天,AI 就会描写抵达
  • 每个地点有自己的音乐,一首淡到下一首

怎么运作 ​

玩家在地图上点了「森林」
  → 按钮把 Location 设成 森林,回到聊天,
    再替玩家说一句「(我前往森林)」
  → 设定「森林」(条件:Location 是 森林)现在会发给 AI;「村庄」不再发
  → 森林的音乐规则对上了:村庄的曲子淡出,森林的曲子淡入
  → AI 带着森林的氛围回复「(我前往森林)」

变的只有这个变量。设定和音乐盯着它,自己就跟上了。


一步一步来 ​

第 1 步:建变量 ​

点卡底下 还可以加 里的 + 变量。

字段值
名称Location
类型文字
初始值村庄
行为规则玩家现在在哪:村庄、森林、山洞或集市。玩家明确去了其中一个地方时改它。

如果 AI 也可以在故事里带玩家走(「你顺着小溪走进了森林」),AI 权限 就保持 AI 可读可写。只想让地图来移动玩家的话,设成 AI 只读。


第 2 步:每个地点写一条设定 ​

每个地点加一条设定。然后把 Location 拖到每条设定上,点那根线,设好条件:Location 等于 村庄(其余依此类推)。这些设定会挪到条件设定那一块,只有当前地点那条会发给 AI。

村庄 ​

[地点:村庄]
玩家在村子里。描写场景时带上这些:
- 一个安静的小村子,石子路在木房子之间绕来绕去
- 烟囱冒着烟,空气里是新烤面包和炖菜的味道
- 村民在井边聊天,铁匠铺里传来锤子声
- 麦田一直铺到远处,随风起伏
- 气氛温暖,过日子的样子

森林 ​

[地点:森林]
玩家在森林里。描写场景时带上这些:
- 老树把大半个天空都遮住了,光一块一块落在青苔上
- 空气潮湿,有泥土、松脂和野花的味道
- 四面八方都是鸟叫,时不时有树枝咔嚓一声
- 灌木丛里可能藏着兔子、鹿,或者更糟的东西
- 越往里走,树越密,光越暗
- 气氛古老、荒野,说不准会遇到什么

山洞 ​

[地点:山洞]
玩家在山洞里。描写场景时带上这些:
- 岩壁上的发光菌类透出淡淡的蓝绿色光
- 钟乳石往下滴水,每一滴都有回声
- 空气又冷又湿,有一股矿石的味道
- 地面又滑又不平,再往里的隧道漆黑一片
- 有时下面会传来低低的吼声,或者岩石移动的碎裂声
- 气氛昏暗、压抑

集市 ​

[地点:集市]
玩家在集市上。描写场景时带上这些:
- 一排排鲜艳的摊位和帐篷,堆满了各种货物
- 商贩吆喝着自家的东西,讨价还价声此起彼伏
- 香料、烤肉、皮革和鲜花的味道混在一起
- 魔法店的橱窗里闪着奇怪的光,角落里有个炼金术士在调东西
- 各色旅人在人群里挤来挤去
- 气氛吵闹、忙碌

第 3 步(可选):每个地点配音乐 ​

在 音频 页加四条 BGM 音轨(卡里还没有音频的话,点 还可以加 里的 + 音频):村庄、森林、山洞、集市,每条都打开 循环播放,淡入 (秒) 和 淡出 (秒) 都填 2。把村庄放进 默认播放列表,打开 自动播放。

然后 BGM 播放配置 → 条件 BGM → 添加条件规则,森林、山洞、集市各加一条:

字段值
条件变量条件:Location 等于 森林
播放森林
淡入 (秒) / 淡出 (秒)3 / 3
停止上一首 BGM打开
结束时返回默认播放列表

玩家回到村庄时,没有规则对得上,结束时 会让播放列表里的村庄音乐回来。

淡入淡出设 3 秒,新旧两首会重叠一小会儿,一首变小声,一首变大声,所以换地点时音乐不会突然断掉。更多见音频设计指南。


第 4 步:做地图页 ​

点顶栏中间的 玩家界面。用 新建一页 加一页(空白页),起名叫「地图」,放一个标题和一句短话,比如「去哪儿?」。

每个地点加一个 按钮(加一个部件 → 按钮)。森林按钮的 按下时,依次做 里:

  1. 改变量:Location,设为,森林
  2. 跳到页面:对话页
  3. 替玩家说一句:(我前往森林)

最后一步让 AI 马上描写抵达:就像玩家自己打了这句话。

为了不让玩家去自己已经在的地方,给每个按钮设 什么时候显示 → 变量满足条件时:Location 不等于 森林。在最上面加一个 文字 部件,写 你在:{Location}(用 插入变量的值…)。

再在这一页加一个只负责返回的 按钮(跳到页面 → 对话页),玩家不想走的时候也能关掉地图。对话页上加一个「地图」按钮,跳到页面 → 地图。

把 编辑 关掉,就能点着试,不花额度。

地图模板

模板 → 地图前往(点地点就走过去,还没解锁的点不了)会直接做好一个地图页,带五个示例地点。它会加 所在地 和 能去的地方 两个变量,后者是一个列表,故事让玩家有办法去某个地方时,AI 就把它加进去,所以地点一开始是锁着的,跟着剧情一个个打开。想让地点随剧情解锁、而不是一开始全开着,就用它。

把地点做成情境

当一个地点长成了游戏里独立的一块(有自己的 NPC、规则和变量,也许还有自己的 AI),就把它做成一个情境,而不只是一条设定。把情境的 什么时候用 设成 变量满足条件时,Location = 山洞,里面的一切就只在玩家在那里时生效。也可以把 Location 变量拖到情境上来设这个条件。


第 5 步:试玩 ​

点顶栏的 试玩。

  1. 村庄的音乐响起。打开 地图:「你在:村庄」,没有村庄按钮
  2. 点 森林。回到聊天,「(我前往森林)」发了出去,AI 描写森林
  3. 加了音乐的话,村庄的曲子淡成森林的曲子
  4. 去山洞,再回村庄:播放列表里的村庄音乐回来了
  5. 两次出行之间照常聊天;地图随时点一下就能打开

试玩右边的 这回合 能看到 AI 拿到的是哪个地点的设定。

如果出了问题:

现象可能的原因怎么改
点地点没反应按钮没有步骤检查 按下时,依次做
位置变了但 AI 没回复没有 替玩家说一句 这一步加在最后一步
AI 描写的地方不对某条设定的条件和按钮设的值对不上按钮里和条件里的 森林 要一字不差
两个地方的氛围混在一起有一条设定在每回合都发的那一块检查每条设定的 什么时候发给 AI
两首曲子同时在放停止上一首 BGM 没打开每条地点规则都打开它
音乐是跳过去的,没有淡入淡出淡入淡出是 0把规则上的 淡入 (秒) 和 淡出 (秒) 设成 2 或 3

写代码的做法:带路线的地图面板 ​

如果你自己写界面,下面是一个放在最后一条消息下面的地图面板,它还会限制玩家从当前位置能去哪里。它设的是同一个 Location 变量,发的也是同一句话,所以第 2、3 步的设定和音乐不用改就能用。

面板 → 前端代码 → 打开 index.tsx,把默认的 return <Chat /> 换掉:

tsx
export default function MyWorld() {
  const api = useYumina();
  const current = String(api.variables["Location"] || "村庄");
  const msgs = api.messages || [];

  const locations = [
    { id: "村庄", icon: "🏘️", color: "#92400e", bg: "#fef3c7", border: "#f59e0b", activeBg: "#f59e0b" },
    { id: "森林", icon: "🌲", color: "#166534", bg: "#dcfce7", border: "#22c55e", activeBg: "#22c55e" },
    { id: "山洞", icon: "🕳️", color: "#3b0764", bg: "#f3e8ff", border: "#a855f7", activeBg: "#a855f7" },
    { id: "集市", icon: "🏪", color: "#9a3412", bg: "#ffedd5", border: "#f97316", activeBg: "#f97316" },
  ];

  // 每个地方能去哪里
  const routes = {
    "村庄": ["森林", "集市"],
    "森林": ["村庄", "山洞"],
    "山洞": ["森林"],
    "集市": ["村庄"],
  };
  const reachable = routes[current] || [];

  const travel = (loc) => {
    api.setVariable("Location", loc.id);
    api.sendMessage("(我前往" + loc.id + ")");
  };

  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 && !msg.isStreaming && (
            <div style={{
              marginTop: "16px",
              padding: "16px",
              background: "rgba(15,23,42,0.6)",
              borderRadius: "12px",
              border: "1px solid #334155",
            }}>
              <div style={{ fontSize: "13px", color: "#94a3b8", marginBottom: "12px", fontWeight: "600", letterSpacing: "0.05em" }}>
                世界地图
              </div>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px" }}>
                {locations.map((loc) => {
                  const isHere = current === loc.id;
                  const canGo = reachable.includes(loc.id);
                  return (
                    <button
                      key={loc.id}
                      onClick={() => { if (!isHere && canGo) travel(loc); }}
                      style={{
                        padding: "14px 10px",
                        background: isHere ? loc.activeBg : loc.bg,
                        border: `2px solid ${isHere ? loc.activeBg : loc.border}`,
                        borderRadius: "10px",
                        color: isHere ? "#ffffff" : loc.color,
                        fontSize: "14px",
                        fontWeight: "700",
                        cursor: isHere ? "default" : canGo ? "pointer" : "not-allowed",
                        opacity: isHere ? 1 : canGo ? 0.85 : 0.3,
                        display: "flex",
                        flexDirection: "column",
                        alignItems: "center",
                        gap: "6px",
                      }}
                    >
                      <span style={{ fontSize: "28px" }}>{loc.icon}</span>
                      <span>{loc.id}</span>
                      {isHere && <span style={{ fontSize: "11px", fontWeight: "500" }}>你在这里</span>}
                    </button>
                  );
                })}
              </div>
            </div>
          )}
        </div>
      );
    }} />
  );
}

代码做了什么:

  • api.variables["Location"] 按名字读这个变量
  • routes 列出每个地方通向哪里。从这里去不了的地方会变淡,点不了
  • travel() 先设好 Location,再用 api.sendMessage(...) 以玩家的身份发出那句出行的话,这样 AI 回复时提示里已经有新地点的设定了
  • !msg.isStreaming 在 AI 还在写的时候把地图藏起来,玩家没法在回复写到一半时出发
  • gridTemplateColumns: "1fr 1fr" 把四个地点排成两行两列;想排三列就用 "1fr 1fr 1fr"

面板 → 前端代码 里文件旁边的状态应该是绿色的 正常。

不想自己写代码?

点画布右上角的 创作助手,说说你想要什么样的地图。


加一个地点 ​

  1. 加一条设定「港口」,条件是 Location 等于 港口
  2. 可以再加一首港口的曲子,配一条 条件 BGM 规则
  3. 在地图页上加一个港口按钮(或者在 locations 数组和 routes 里各加一行)
  4. 在 Location 的行为规则里加上港口,让 AI 知道有这个地方

已经有的东西都不用改。


速查 ​

你想要怎么做
记录玩家在哪一个文字变量 Location
给 AI 当前地点的氛围每个地点一条设定,条件看 Location
用按钮移动玩家步骤:改变量、跳到页面、替玩家说一句
让 AI 马上描写抵达替玩家说一句(或者代码里的 api.sendMessage())
藏起当前所在地点的按钮什么时候显示 → 变量满足条件时,Location 不等于 ……
每个地点配音乐每个地点一条 条件 BGM 规则,打开 停止上一首 BGM
随剧情打开的地点地图前往 模板,配 能去的地方
自带内容的大地点一个情境,什么时候用 设成 变量满足条件时
限制地点之间的路线界面代码(见上面写代码的做法)

自己试试:可导入的演示 ​

下载这个 JSON 导入进去,就能看到能跑的版本:

recipe-12-demo-zh.json

怎么导入:

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

演示用行为来开关各地点的设定,地图画在界面代码里。演示里没有音乐,想要声音的话,照第 3 步自己加音轨。


这是食谱 #12

同样的搭法(一个变量,设定和音乐跟着它走)也能用在塔的楼层、房子里的房间、传送门后面的世界。昼夜循环用的是同一套,只是把地点换成了时间,两个还能叠在一起:夜里的森林可以有自己的一条设定,两个条件都写上。