昼夜循环
构建一个自动推进的时间系统——每 3 个回合,时间向前推进(早晨 → 中午 → 傍晚 → 夜晚 → 早晨)。不同时段激活不同的知识条目和背景音乐,使 AI 的写作氛围随之变化。玩家不需要做任何事——时间自然流逝。
你将构建什么
一个嵌入在聊天中的昼夜循环系统:
- 自动计数 — 每个对话回合,内部计数器加 1。到第 3 个回合时,时间推进一个时段
- 四个时段 — 早晨 → 中午 → 傍晚 → 夜晚 → 早晨(如此循环)
- 氛围切换 — 每个时段有自己的知识条目,描述该时段的光线、温度、NPC 行为等。当时段变化时,旧条目被禁用,新条目被启用
- BGM 切换 — 早晨播放鸟鸣,夜晚切换到蛐蛐和蛙鸣。过渡使用淡入淡出,不会突然中断
- 时间徽章 — 聊天中最后一条消息上的小图标(☀️🌤️🌅🌙),让玩家随时知道现在是什么时间
工作原理
整个系统归结为:每回合计数器 +1 → 计数器到 3 → 切换时段 → 重置计数器 → 切换条目和音乐。
玩家发送消息 → AI 回复 → 回合结束
→ 「每回合 +1」行为触发:turn_counter 从 0 变为 1
→ 下一回合:turn_counter 从 1 变为 2
→ 下一回合:turn_counter 从 2 变为 3
→ 「变量越过阈值」行为触发:turn_counter 超过 2
→ 动作执行:time_period 设为下一个时段,turn_counter 重置为 0
→ 旧时段条目禁用,新时段条目启用
→ 淡入淡出切换到新时段的 BGM
→ Root Component 读取变量并更新时间徽章有两种实现方式。结果相同,思维模型不同:
| 方式 | 使用的触发器 | 行为数量 | 适合 |
|---|---|---|---|
| 方式 A:每回合 +1 + 变量越过阈值 | every-turn + variable-crossed | 2 | 想理解底层机制的人 |
| 方式 B:直接每 N 回合触发 | turn-count (everyNTurns=3) | 1 | 只想把事情做完的人 |
本教程教的是方式 A(更灵活,帮助你理解行为如何链式连接)。方式 B 在最后简要介绍。
分步教程
第 1 步:创建变量
我们需要 2 个变量——一个追踪当前时段,一个作为计数器。
编辑器 → 侧边栏 → 变量标签页 → 为每个点击「添加变量」
变量 1:当前时段
| 字段 | 值 | 原因 |
|---|---|---|
| 名称 | Current Time Period | 供你自己参考 |
| ID | time_period | 行为和 Root Component 通过这个 ID 读写 |
| 类型 | String | 因为值是文本("Morning"、"Noon"、"Evening"、"Night") |
| 默认值 | Morning | 新会话从早晨开始 |
| 分类 | Custom | 时间系统专用分类 |
| 行为规则 | Do not modify this variable. It is controlled automatically by the day-night cycle system. Its current value represents the in-game time period. | 告诉 AI 不要自行改变时间——只有行为可以 |
变量 2:回合计数器
| 字段 | 值 | 原因 |
|---|---|---|
| 名称 | Turn Counter | 供你自己参考 |
| ID | turn_counter | 每回合递增,到 3 时重置 |
| 类型 | Number | 需要算术运算 |
| 默认值 | 0 | 从 0 开始计数 |
| 分类 | Custom | 时间系统专用分类 |
| 行为规则 | Do not modify this variable. It is controlled automatically by the day-night cycle system. | 防止 AI 篡改 |
为什么用计数器而不是直接每 3 回合触发?
计数器 + 变量越过阈值的方式更灵活。假设你以后想要「白天 3 回合,夜晚 5 回合」——只需在行为中添加条件检查。turn-count 触发器更简单但适应性更低。两种方式各有优势;选择适合你需求的即可。
第 2 步:创建四个时段知识条目
每个时段需要一个知识条目来描述该时段的氛围。只有「Morning」默认启用;其他三个默认禁用。
编辑器 → 条目标签页 → 逐一创建条目
条目 1:早晨氛围
| 字段 | 值 | 原因 |
|---|---|---|
| 名称 | Morning Atmosphere | 供你自己参考 |
| 段落 | Presets | 预设段落中的条目每次都会发送给 AI |
| 启用 | 是(开启开关) | 游戏从早晨开始,所以这个默认启用 |
内容:
[Current Time Period: Morning]
It is early morning. Reflect the following atmosphere when describing the scene:
- Soft morning light spills in from the east; the air is fresh and cool
- Dewdrops cling to blades of grass and flower petals, refracting tiny glints of light
- Birds sing in the branches; a rooster crows in the distance
- NPCs are just waking up, shops are opening one by one, foot traffic is picking up
- The overall mood is peaceful and full of hope条目 2:中午氛围
| 字段 | 值 | 原因 |
|---|---|---|
| 名称 | Noon Atmosphere | 供你自己参考 |
| 段落 | Presets | 预设段落 |
| 启用 | 否(关闭开关) | 行为会在时段切换时启用它 |
内容:
[Current Time Period: Noon]
It is midday. Reflect the following atmosphere when describing the scene:
- The sun beats down directly overhead; the light is searing and bright, the ground reflecting a blinding white glare
- The air is stifling; distant scenery shimmers and warps in the heat haze
- Most people have retreated to the shade to rest; the streets are quieter than morning
- Taverns and eateries are at their busiest, the smell of food drifting through the air
- The overall mood is languid and sweltering条目 3:傍晚氛围
| 字段 | 值 | 原因 |
|---|---|---|
| 名称 | Evening Atmosphere | 供你自己参考 |
| 段落 | Presets | 预设段落 |
| 启用 | 否(关闭开关) | 行为会启用它 |
内容:
[Current Time Period: Evening]
It is dusk. Reflect the following atmosphere when describing the scene:
- The setting sun paints the sky in shades of orange-red and purple, clouds rimmed with gold
- Long shadows stretch from buildings and trees
- Flocks of birds sweep across the sky heading home; wisps of cooking smoke rise from rooftops
- NPCs are wrapping up for the day, heading home; children chase each other through the streets
- The overall mood is warm, nostalgic, tinged with a gentle melancholy条目 4:夜晚氛围
| 字段 | 值 | 原因 |
|---|---|---|
| 名称 | Night Atmosphere | 供你自己参考 |
| 段落 | Presets | 预设段落 |
| 启用 | 否(关闭开关) | 行为会启用它 |
内容:
[Current Time Period: Night]
It is deep night. Reflect the following atmosphere when describing the scene:
- Moonlight and starlight are the only natural sources of illumination, casting a silvery glow over everything
- Most buildings have gone dark; the occasional window glows with dim candlelight
- A cool night breeze carries the chorus of crickets and frogs
- The streets are nearly deserted; night-watch guards pace slowly by, torches in hand
- Danger may lurk in the shadows — wild beasts, thieves, or something stranger still
- The overall mood is mysterious, hushed, and laced with hidden peril为什么只有「Morning」默认启用? 因为游戏从早晨开始。如果四个条目同时启用,AI 会同时收到早晨、中午、傍晚和夜晚的描述,不知道该遵循哪一个。一次只启用一个可以确保 AI 锁定正确的氛围。
第 3 步:(可选)上传各时段 BGM
如果你想让每个时段有自己的背景音乐,先上传音频文件。
编辑器 → 音频标签页 → 添加曲目
| 曲目 ID | 名称 | 类型 | 循环 | 淡入 | 淡出 |
|---|---|---|---|---|---|
bgm_morning | Morning Theme | BGM | 是 | 2s | 2s |
bgm_noon | Afternoon Theme | BGM | 是 | 2s | 2s |
bgm_evening | Dusk Theme | BGM | 是 | 2s | 2s |
bgm_night | Night Theme | BGM | 是 | 2s | 2s |
没有音频文件? 跳过这一步。昼夜循环的核心是知识条目切换——BGM 是锦上添花。你随时可以后续添加。
在 BGM 播放列表中,将 autoPlay 设为 true,默认播放 bgm_morning。后续时段切换时,行为会使用 crossfade 动作平滑过渡曲目。
第 4 步:创建行为
这是系统的核心。我们需要 2 个行为——实际上是 6 个。继续阅读。
编辑器 → 行为标签页 → 逐一添加行为
行为 1:每回合计数
这个非常简单——每个对话回合后,计数器加 1。
WHEN(何时检查):
| 字段 | 值 | 原因 |
|---|---|---|
| 触发类型 | Every turn | 每次玩家-AI 交互后自动触发 |
DO(做什么):
| 动作类型 | 设置 | 效果 |
|---|---|---|
| 修改变量 | turn_counter add 1 | 计数器 +1 |
这是唯一的动作。没有条件,没有额外配置。它忠实地每回合加 1。
为什么不在这里就检查「到 3 了吗?」 因为行为系统的设计哲学是一个行为做一件事。递增计数器是一个行为的工作;检查是否到了 3 是另一个行为的工作。引擎会自动链接它们——计数器递增后,如果值越过了阈值,另一个行为就会触发。
行为 2:推进时段
这个行为在计数器达到 3 时触发,执行所有切换逻辑。
WHEN(何时检查):
| 字段 | 值 | 原因 |
|---|---|---|
| 触发类型 | Variable crossed threshold | 当 turn_counter 超过阈值时触发 |
| 变量 | turn_counter | 我们监控的变量 |
| 方向 | Rises above | 当值从低于阈值变为高于阈值时触发 |
| 阈值 | 2 | 当 turn_counter 从 2 变为 3 时触发(超过 2) |
为什么阈值是 2 而不是 3?
variable-crossed中的「rises above」方向检测值从 <= 阈值变为 > 阈值的时刻。当 turn_counter 从 2 变为 3 时,它「rises above 2」——即从 <=2 变为 >2。如果你把阈值设为 3,则需要 turn_counter 从 3 变为 4 才会触发,那不是我们想要的。
DO(做什么):
这个行为需要做很多事。按顺序添加这些动作:
| # | 动作类型 | 设置 | 效果 |
|---|---|---|---|
| 1 | 修改变量 | turn_counter set to 0 | 重置计数器,开始下一个 3 回合倒计时 |
| 2 | 禁用知识条目 | Morning Atmosphere | 关闭所有时段条目 |
| 3 | 禁用知识条目 | Noon Atmosphere | 全部关闭 |
| 4 | 禁用知识条目 | Evening Atmosphere | 全部关闭 |
| 5 | 禁用知识条目 | Night Atmosphere | 全部关闭 |
等等——这把四个都关了。它怎么知道该启用哪一个?
好问题。这就是 ONLY IF 条件用于分支的地方。但单个行为只能有一组动作。所以我们将「推进时段」拆成 5 个行为:1 个重置计数器并禁用所有条目,4 个启用对应的时段。
让我重新组织:
完整行为列表(共 6 个):
行为 1:每回合计数
(同上——不重复。)
行为 2:推进——早晨 → 中午
WHEN:
| 字段 | 值 |
|---|---|
| 触发类型 | Variable crossed threshold |
| 变量 | turn_counter |
| 方向 | Rises above |
| 阈值 | 2 |
ONLY IF:
| 变量 | 运算符 | 值 |
|---|---|---|
time_period | 等于 (eq) | Morning |
DO:
| # | 动作类型 | 设置 | 效果 |
|---|---|---|---|
| 1 | 修改变量 | turn_counter set to 0 | 重置计数器 |
| 2 | 修改变量 | time_period set to Noon | 推进到下一个时段 |
| 3 | 禁用知识条目 | Morning Atmosphere | 关闭旧时段条目 |
| 4 | 启用知识条目 | Noon Atmosphere | 开启新时段条目 |
| 5 | 播放音乐 | bgm_noon,操作:crossfade,时长 3s | 淡入淡出切换到中午 BGM |
| 6 | Tell AI | 内容:Time has advanced from Morning to Noon. Naturally reflect this time change in your upcoming descriptions. | 让 AI 平滑过渡 |
行为 3:推进——中午 → 傍晚
WHEN: 与行为 2 相同(variable crossed threshold,turn_counter rises above 2)
ONLY IF:
| 变量 | 运算符 | 值 |
|---|---|---|
time_period | 等于 (eq) | Noon |
DO:
| # | 动作类型 | 设置 | 效果 |
|---|---|---|---|
| 1 | 修改变量 | turn_counter set to 0 | 重置计数器 |
| 2 | 修改变量 | time_period set to Evening | 推进到傍晚 |
| 3 | 禁用知识条目 | Noon Atmosphere | 关闭中午条目 |
| 4 | 启用知识条目 | Evening Atmosphere | 开启傍晚条目 |
| 5 | 播放音乐 | bgm_evening,操作:crossfade,时长 3s | 淡入淡出切换 BGM |
| 6 | Tell AI | 内容:Time has advanced from Noon to Evening. Naturally reflect this time change in your upcoming descriptions. | AI 过渡 |
行为 4:推进——傍晚 → 夜晚
WHEN: 同上
ONLY IF:
| 变量 | 运算符 | 值 |
|---|---|---|
time_period | 等于 (eq) | Evening |
DO:
| # | 动作类型 | 设置 | 效果 |
|---|---|---|---|
| 1 | 修改变量 | turn_counter set to 0 | 重置计数器 |
| 2 | 修改变量 | time_period set to Night | 推进到夜晚 |
| 3 | 禁用知识条目 | Evening Atmosphere | 关闭傍晚条目 |
| 4 | 启用知识条目 | Night Atmosphere | 开启夜晚条目 |
| 5 | 播放音乐 | bgm_night,操作:crossfade,时长 3s | 淡入淡出切换 BGM |
| 6 | Tell AI | 内容:Time has advanced from Evening to Night. Naturally reflect this time change in your upcoming descriptions. | AI 过渡 |
行为 5:推进——夜晚 → 早晨
WHEN: 同上
ONLY IF:
| 变量 | 运算符 | 值 |
|---|---|---|
time_period | 等于 (eq) | Night |
DO:
| # | 动作类型 | 设置 | 效果 |
|---|---|---|---|
| 1 | 修改变量 | turn_counter set to 0 | 重置计数器 |
| 2 | 修改变量 | time_period set to Morning | 循环回到早晨 |
| 3 | 禁用知识条目 | Night Atmosphere | 关闭夜晚条目 |
| 4 | 启用知识条目 | Morning Atmosphere | 开启早晨条目 |
| 5 | 播放音乐 | bgm_morning,操作:crossfade,时长 3s | 淡入淡出切换 BGM |
| 6 | Tell AI | 内容:Time has advanced from Night to Morning — a new day begins. Naturally reflect this time change in your upcoming descriptions. | AI 过渡 |
为什么拆成 4 个行为? 因为每个时段过渡需要启用不同的条目和播放不同的 BGM。单个行为只能有一组条件和一组动作——不支持 if-else 分支。所以我们用 4 个带有不同 ONLY IF 条件的行为来模拟分支:当同一个触发器触发时(计数器超过 2),引擎检查所有行为,但只有
time_period匹配的那个会执行。
行为 6:会话初始化
这个行为在会话开始时设置初始状态,确保变量对新会话或重新进入都有正确的起始值。
WHEN:
| 字段 | 值 | 原因 |
|---|---|---|
| 触发类型 | Session start (session-start) | 新会话开始时自动触发一次 |
DO:
| # | 动作类型 | 设置 | 效果 |
|---|---|---|---|
| 1 | 修改变量 | time_period set to Morning | 确保从早晨开始 |
| 2 | 修改变量 | turn_counter set to 0 | 重置回合计数器 |
为什么需要会话初始化行为? 变量默认值只在首次创建时生效。如果玩家中途退出并开始新会话,变量可能保留之前的值(例如
time_period停在「Night」,turn_counter停在 2)。会话初始化行为确保每个新会话都从早晨开始,计数器为 0。
行为优先级
所有 4 个推进行为可以保持默认优先级(0)。它们的 ONLY IF 条件互斥——当前时段只能匹配其中一个,所以不会冲突。
第 5 步:在 Root Component 中添加时间徽章
在聊天中最后一条消息上显示当前时段的图标,让玩家一眼就能知道现在是什么时间。
编辑器 → Custom UI 部分 → 打开 index.tsx → 粘贴以下内容(替换默认的 return <Chat />):
export default function MyWorld() {
const api = useYumina();
// ---- 读取变量 ----
const timePeriod = String(api.variables.time_period || "Morning");
// ---- 时段图标和颜色映射 ----
const timeConfig = {
"Morning": { icon: "☀️", label: "Morning", color: "#fbbf24", bg: "rgba(251,191,36,0.15)" },
"Noon": { icon: "🌤️", label: "Noon", color: "#f59e0b", bg: "rgba(245,158,11,0.15)" },
"Evening": { icon: "🌅", label: "Evening", color: "#f97316", bg: "rgba(249,115,22,0.15)" },
"Night": { icon: "🌙", label: "Night", color: "#818cf8", bg: "rgba(129,140,248,0.15)" },
};
const current = timeConfig[timePeriod] || timeConfig["Morning"];
const msgs = api.messages || [];
return (
<Chat renderBubble={(msg) => {
const isLastMsg = msg.messageIndex === msgs.length - 1;
return (
<div>
{/* 正常渲染消息文本——contentHtml 是已经渲染好的 HTML */}
<div
style={{ color: "#e2e8f0", lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
/>
{/* 时间徽章——只在最后一条消息上显示 */}
{isLastMsg && (
<div style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
marginTop: "12px",
padding: "4px 12px",
background: current.bg,
border: `1px solid ${current.color}33`,
borderRadius: "999px",
fontSize: "13px",
color: current.color,
fontWeight: "600",
}}>
<span style={{ fontSize: "16px" }}>{current.icon}</span>
<span>{current.label}</span>
</div>
)}
</div>
);
}} />
);
}逐行说明:
api.variables.time_period— 读取当前时段变量timeConfig— 一个查找表,将每个时段映射到图标、文本标签和颜色。你可以随意更改颜色以匹配你的世界风格isLastMsg— 只在最后一条消息上显示徽章,不是每条消息都显示- 徽章使用
inline-flex+border-radius: 999px实现药丸形状——简洁但一目了然
想在每条消息上都显示时间?
去掉 {isLastMsg && ...} 检查,直接把徽章放在 return 中。这样每条消息都会带有时间标记,就像聊天记录中的时间戳一样。
第 6 步:保存并测试
- 点击编辑器顶部的保存
- 点击开始游戏或返回主页打开新会话
- 正常与 AI 聊天。前 2 个回合,时间徽章显示「☀️ Morning」,没有变化
- 第 3 个回合后——时间推进到「🌤️ Noon」,AI 的下一个回复会自然反映时间变化
- 再过 3 个回合——推进到「🌅 Evening」
- 再过 3 个回合——推进到「🌙 Night」。如果你设置了 BGM,应该能听到淡入淡出的过渡
- 再过 3 个回合——循环回到「☀️ Morning」,新的一天开始
如果出了问题:
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| 时间永远不变 | 「每回合计数」行为没有触发 | 检查行为 1 的触发器是否设为「Every turn」且行为已启用 |
| 第 3 回合没有切换 | 阈值设置错误 | 确认「variable crossed threshold」的阈值是 2(不是 3),方向是「rises above」 |
| 切换后条目没变 | 条目名称不匹配 | 检查行为中的「Enable lore entry」/「Disable lore entry」动作引用的条目名称是否正确 |
| 4 个行为同时触发 | 缺少 ONLY IF 条件 | 每个推进行为必须有 ONLY IF 条件限制当前 time_period 的值 |
| 时间徽章不可见 | Root Component 语法错误 | 检查 Custom UI 面板底部的编译状态——应该显示绿色的「OK」 |
| BGM 没切换 | 曲目 ID 不匹配或没有上传音频 | 确认行为中的 trackId 与音频标签页中的曲目 ID 一致 |
方式 B 对比:使用 turn-count 触发器
如果方式 A 觉得行为太多,可以使用更简单的方式 B。
区别:
| 方式 A(本教程) | 方式 B | |
|---|---|---|
| 触发器 | every-turn + variable-crossed | turn-count (everyNTurns=3) |
需要 turn_counter 变量 | 是 | 否 |
| 行为数量 | 6(1 个计数 + 4 个推进 + 1 个可选初始化) | 4(4 个推进) |
| 灵活性 | 高(间隔可以动态调整) | 低(间隔固定为 N) |
| 适合 | 需要动态时间流速的世界 | 固定节奏的世界 |
方式 B 怎么做:
删除 turn_counter 变量和「每回合计数」行为。将所有 4 个推进行为的触发器改为:
| 字段 | 值 |
|---|---|
| 触发类型 | Every N turns |
| everyNTurns | 3 |
其他所有内容(ONLY IF 条件、DO 动作)与方式 A 完全相同。turn-count 触发器每 3 回合自动触发——不需要手动计数。
turn-count触发器如何工作: 引擎维护一个内部的全局回合计数。当你设置everyNTurns: 3时,引擎在第 3、6、9、12 回合等自动触发行为。你不需要自己管理计数器变量。
快速参考
| 你想做什么 | 怎么做 |
|---|---|
| 每回合做某事 | 行为触发器:「Every turn」(every-turn) |
| 每 N 回合做某事 | 行为触发器:「Every N turns」(turn-count),设置 everyNTurns |
| 检测变量越过某个值 | 行为触发器:「Variable crossed threshold」(variable-crossed),设置变量、方向和阈值 |
| 切换知识条目 | 动作:「Enable lore entry」/「Disable lore entry」 |
| 淡入淡出音乐 | 动作:「Play music」,操作:crossfade,设置淡入淡出时长 |
| 让 AI 知道发生了什么 | 动作:「Tell AI」,写一条临时系统指令 |
| 在消息上显示状态徽章 | 在 Root Component 的 <Chat renderBubble> 中读取变量并用 JSX 渲染 |
| 模拟 if-else 分支 | 多个行为共享同一触发器 + 不同的 ONLY IF 条件 |
试试看——可导入的演示世界
下载这个 JSON 并作为新世界导入,查看完整效果:
如何导入:
- 前往 Yumina → 我的世界 → 创建新世界
- 在编辑器中,点击更多操作 → 导入包
- 选择下载的
.json文件 - 新世界会被创建,所有变量、条目、行为和 Root Component 都已预配置
- 开始新会话并试试看
包含内容:
- 2 个变量(
time_period追踪当前时段,turn_counter作为回合计数器) - 4 个知识条目(早晨 / 中午 / 傍晚 / 夜晚氛围,只有早晨默认启用)
- 6 个行为(1 个每回合计数 + 4 个时段推进 + 1 个会话初始化)
- 一个 Root Component(
<Chat renderBubble>中的时段图标徽章) - 4 个 BGM 曲目(你需要上传自己的音频文件来替换 URL)
这是教程 #9
本教程展示了行为系统的链式连接能力——通过简单的计数器 + 阈值触发器 + 条件分支,你可以构建一个全自动的时间系统。相同的模式适用于天气变化、季节循环、NPC 情绪波动,或任何「按节奏自动变化」的功能。
