视觉小说模式
一个全屏的视觉小说:场景背景、角色立绘、对话框和选项按钮,都跟着 AI 在剧情里改的变量走。界面代码(
index.tsx)不用<Chat />,自己画整个画面,再把<MessageInput />放在玩家打字的地方。只用 Tailwind 和内联样式,不需要组件库。
不写代码的部分
视觉小说里有好几样东西已经不用写界面代码了:
- 角色立绘:在画布上右键一个角色的设定,选 立绘…。聊天里这个角色说的每一句上面,都会显示这张图和他的名字。见视觉与音频
- 像书一样的排版:在玩家界面里选中 对话 那一块,把 消息样子 设成 小说排版
- 跟着变量换的背景或立绘:在玩家界面里加一个 图片 部件,把 图片来源 设成 跟着「<变量>」换。变量里存的是哪个
@asset:引用,它就显示哪张图,所以 AI 改了变量,图就跟着换 - 剧情到了某一刻出图:场景图在它 什么时候展示 那句话成立时会自己出来
- 选项按钮:玩家界面上的按钮,步骤选 替玩家说一句
这一页剩下的部分是用代码搭整个画面,适合你想要这些的时候:对话框读取回复、把旁白和对话分开,选项按钮从回复的文字里取,整个布局都由你控制。
你要做的东西
一个全屏的视觉小说界面:
- 场景背景:AI 通过改一个变量来切换背景图(教室、街道、夜空……),界面代码把它显示成全屏的
background-image - 角色立绘:AI 设置当前说话的人和表情,界面代码在画面中间显示对应的立绘
- 对话框:画面底部一个半透明的框,显示角色名和台词。斜体文字 算旁白或内心独白,普通文字算角色台词
- 选项按钮:AI 给出选择时,界面代码在画面上叠一层可以点的按钮
- 全屏模式:界面代码直接返回自己的全屏布局,不套
<Chat />,所以完全没有普通的聊天气泡
怎么运作
AI 每次回复都用指令控制画面:
AI 的回复:
[current_bg: set "@asset:your-classroom-reference"]
[current_speaker: set "Yuki"]
[speaker_emotion: set "happy"]
*教室沐浴在晨光中。樱花花瓣偶尔从窗户飘进来。*
Yuki 转过身来对你微笑:
早上好!你今天来得真早。引擎解析完这些指令以后:
current_bg变成教室那张图 → 界面代码把background-image换成教室current_speaker变成"Yuki"→ 对话框显示名字「Yuki」speaker_emotion变成"happy"→ 界面代码找到 Yuki 开心的立绘并显示出来- 界面代码解析最新一条消息的文字:斜体 部分显示成旁白,普通文字显示成角色台词
引擎处理流程:
AI 回复 → 引擎提取指令 → 更新变量 → 界面代码读取变量并重画
→ 背景层:<div style={{ backgroundImage: ... }} />
→ 立绘层:<img src={SPRITES[speaker + "_" + emotion]} />
→ 对话框层:区分旁白(灰色斜体)和台词(白色正体)
→ 选项层:show_choices = true 时显示按钮一步一步来
第 1 步:建变量
控制视觉小说的画面需要 4 个变量。
在画布上点卡底下 还可以加 里的 + 变量,每个变量点一次。名字照下面填,界面代码和 AI 都是按名字找变量的。
变量 1:当前背景
| 字段 | 值 | 原因 |
|---|---|---|
| 变量名称 | current_bg | AI 写 [current_bg: set "..."] 来切换背景;代码读 api.variables.current_bg |
| 类型 | 文字 | 值是一个 @asset: 引用或图片 URL |
| 初始值 | 第一个场景背景的 @asset: 引用 | 新一局开始、AI 还没改任何东西时显示的画面。做完第 3 步再填 |
| 行为规则 | 场景的背景图。场景换到别的地方时就改它,用素材表里对应的引用。 | 告诉 AI 什么时候改、用哪些值 |
变量 2:当前说话者
| 字段 | 值 | 原因 |
|---|---|---|
| 变量名称 | current_speaker | AI 写 [current_speaker: set "name"] 来切换说话的人 |
| 类型 | 文字 | 值是角色名 |
| 初始值 | Narrator | 一开始是旁白模式,没有哪个角色在说话 |
| 行为规则 | 现在是谁在说话。角色说话时设成这个角色的名字,旁白或内心独白时设成 "Narrator"。 | 告诉 AI 怎么用 |
变量 3:说话者表情
| 字段 | 值 | 原因 |
|---|---|---|
| 变量名称 | speaker_emotion | AI 写 [speaker_emotion: set "happy"] 来切换表情 |
| 类型 | 文字 | 值是一个表情关键词 |
| 初始值 | neutral | 一开始是平静的表情 |
| 行为规则 | 说话角色的表情。只能是这几个之一:neutral, happy, sad, angry, surprised, shy。表情变了就更新。 | 把表情列出来,AI 就不会编出你没有立绘的表情 |
变量 4:显示选项
| 字段 | 值 | 原因 |
|---|---|---|
| 变量名称 | show_choices | AI 写 [show_choices: set true] 来显示选项按钮 |
| 类型 | 开关 | 只有两种状态:显示 / 隐藏 |
| 初始值 | 关 | 选项按钮默认藏着 |
| 行为规则 | 要给玩家一个选择时打开它,其它时候保持关着。 | 告诉 AI 只在需要玩家做选择时才打开 |
[名字: set 值] 这种格式不用你教 AI,引擎已经教好了。行为规则里只要写每个值什么时候变。
AI 不会执行代码
AI 写指令,引擎把指令应用到变量上,界面代码读变量再重画:AI 写指令 → 引擎解析 → 变量更新 → 界面代码重新渲染。
第 2 步:加一条写着视觉小说说明的设定
AI 需要知道它是在给一个视觉小说画面写东西,以及文字该怎么排。
在画布上的 角色和世界 那一块(每回合都发)右上角点 +,加一条设定:
| 字段 | 值 | 原因 |
|---|---|---|
| 名称 | 视觉小说系统说明 | 给你自己看的 |
| 什么时候发给 AI | 角色和世界(每回合都发) | 格式规则每次回复都要用 |
内容:
[视觉小说模式]
你在为一个视觉小说引擎写内容。每次回复都必须带上控制画面的指令。
格式规则:
1. 在回复开头用指令设置场景:
[current_bg: set "背景引用"]
[current_speaker: set "角色名"]
[speaker_emotion: set "表情"]
2. 文字格式:
- *斜体文字* = 旁白或内心独白。用来描写环境、角色动作、内心想法。
- 普通文字(不加任何格式)= 角色说的话。
- 台词不要加引号,直接写普通文字。
3. 想给玩家一个选择时:
- 用 [show_choices: set true]
- 在文字最后按这个格式列出选项:
A) 选项文字
B) 选项文字
C) 选项文字
4. 每次回复只写一个场景片段(3 到 5 句)。节奏要紧凑,像真正的视觉小说。
5. 可用的表情:neutral, happy, sad, angry, surprised, shy
6. 换场景时一定要更新 current_bg。角色说话时一定要更新 current_speaker 和 speaker_emotion。AI 不知道你的界面代码是怎么读它的文字的,所以要把格式写明白:斜体是旁白,普通文字是台词。不然它可能随便排,代码就分不清哪是旁白哪是台词。
第 3 步:准备并上传素材
视觉小说需要背景图和角色立绘。有两种提供方式:
- 方式 A(推荐):上传到 Yumina 的素材库,用
@asset:引用。稳定,不会过期 - 方式 B:用外部的
https://图片链接(你自己的服务器、图床)。更简单,但链接可能会失效
把素材传到 Yumina
- 打开 面板 → 资源
- 把图片文件 拖进去,或者点 上传
- 每个上传的文件都会有一个
@asset:引用(比如@asset:a1b2c3d4-e5f6-7890) - 在素材上用 复制引用 把它的引用复制下来
@asset:引用是什么? 它是 Yumina 内部的素材标识。在界面代码里,<img src="@asset:xxx" />和内联样式里的@asset:在画出来的时候会变成真实的 CDN 地址,api.resolveAssetUrl("@asset:xxx")能把地址作为字符串给你。变量里也可以存@asset:值。
建议准备的素材
背景图(建议 16:9,1920×1080 或更高):
| 场景 | 建议文件名 | 用途 |
|---|---|---|
| 教室(白天) | classroom_morning.jpg | 上课、对话场景 |
| 学校走廊 | hallway.jpg | 过渡场景 |
| 街道(傍晚) | street_evening.jpg | 放学后的场景 |
| 卧室(夜晚) | room_night.jpg | 夜间场景 |
上传以后,记下每张背景的 @asset: 引用。你会把它们写进那条设定里,让 AI 知道哪个引用对应哪个场景。
角色立绘(建议透明 PNG,高度 1000px 以上):
每个角色的每种表情准备一张立绘。命名统一成 characterName_emotion.png,第 5 步对起来方便。
| 角色 | 示例文件名 | 示例引用 |
|---|---|---|
| Yuki(开心) | yuki_happy.png | @asset:abc123... |
| Yuki(难过) | yuki_sad.png | @asset:def456... |
| 老师(平静) | teacher_neutral.png | @asset:ghi789... |
告诉 AI 用哪些背景
在第 2 步那条视觉小说说明的设定里加一张引用表,让 AI 知道哪个 @asset: 引用对应哪个场景。立绘不用写在这里:AI 只负责设说话的人和表情,用哪张立绘由你的代码来挑。
[背景引用表]
- 教室白天:@asset:your-classroom-reference
- 学校走廊:@asset:your-hallway-reference
- 街道傍晚:@asset:your-street-reference
- 卧室夜晚:@asset:your-bedroom-reference
切换背景时,用上面的引用作为值。例如:
[current_bg: set "@asset:your-classroom-reference"]然后回到 current_bg,把 初始值 设成教室的引用。
还没有素材?照样可以测试
没有图片的时候,界面代码会显示纯色背景。先把逻辑跑通,素材之后再加。想快速搭个原型,用图库的 https:// 链接也行。
第 4 步:写开场白
开场白就是视觉小说的第一个场景。在画布上点开场白那一行,写进去:
*四月的第一天。樱花季的尾声。*
*你推开教室的门。熟悉的粉笔灰和木头的气味扑面而来。大部分座位还空着,离上课还有十分钟。*
*在靠窗的座位上,一个你从未见过的女孩正安静地望着窗外。*
*转校生?你不记得班上有她这样的人。*开场白里不用指令来设变量。新的一局从每个变量的 初始值 开始,所以 current_bg 一开始就指向教室。如果你再加几个从别的地方开始的开场白,在画布上把开场白拖到变量上,就能给这个开场白单独设一个初始值(见开场白和设定)。
第 5 步:写视觉小说的界面代码
界面代码接管整个画面。它不套 <Chat />,而是自己画背景、立绘、对话框和选项按钮。玩家输入交给放在画面底部的 <MessageInput />。
打开 面板 → 前端代码 → index.tsx,把里面的内容换成下面这段。SPRITES 里填上你在第 3 步拿到的引用:
// 每个 "名字_表情"(名字小写)对应的立绘。引用从 面板 → 资源 里复制。
const SPRITES = {
yuki_neutral: "@asset:your-yuki-neutral-reference",
yuki_happy: "@asset:your-yuki-happy-reference",
yuki_sad: "@asset:your-yuki-sad-reference",
teacher_neutral: "@asset:your-teacher-reference",
};
export default function MyWorld() {
const api = useYumina();
const renderMarkdown = api.renderMarkdown;
const msgs = api.messages || [];
const lastMsg = msgs[msgs.length - 1];
const content = lastMsg ? String(lastMsg.content || "") : "";
// ---- 读取变量 ----
const bgRef = String(api.variables.current_bg || "");
const bgUrl = bgRef ? api.resolveAssetUrl(bgRef) : "";
const speaker = String(api.variables.current_speaker || "Narrator");
const emotion = String(api.variables.speaker_emotion || "neutral");
const showChoices = Boolean(api.variables.show_choices);
// ---- 清理内容:去掉指令行,只保留叙事文本 ----
const cleanContent = content
.split("\n")
.filter((line) => !line.trim().match(/^\[.+:\s*(set|add|subtract|multiply|toggle|append|merge|push|delete)\s+.+\]$/) && !line.trim().match(/^\[.+:\s*[+-]?\d+\]$/))
.join("\n")
.trim();
// ---- 解析文本:区分旁白(斜体)和对话(普通文字) ----
// 将文本按段落分割并分类
const paragraphs = cleanContent
.split("\n\n")
.map((p) => p.trim())
.filter((p) => p.length > 0);
const parsed = paragraphs.map((p) => {
// 如果整个段落被 * 包裹,或每行都以 * 开头,则为旁白
const isNarration = /^\*[^*].*[^*]\*$/.test(p.trim())
|| p.trim().startsWith("*");
// 检查是否为选项行(A) B) C) 格式)
const isChoice = /^[A-Z]\)\s/.test(p.trim());
return { text: p, isNarration, isChoice };
});
// ---- 立绘(按角色名和表情查找) ----
const spriteRef = speaker !== "Narrator"
? SPRITES[`${speaker.toLowerCase()}_${emotion}`]
: null;
const spriteUrl = spriteRef ? api.resolveAssetUrl(spriteRef) : null;
// ---- 提取选项 ----
const choices = parsed
.filter((p) => p.isChoice)
.map((p) => p.text.replace(/^[A-Z]\)\s*/, ""));
// ---- 渲染 ----
return (
<div style={{
position: "relative",
width: "100%",
minHeight: "500px",
borderRadius: "12px",
overflow: "hidden",
background: "#000",
}}>
{/* ===== 背景层 ===== */}
<div style={{
position: "absolute",
inset: 0,
backgroundImage: bgUrl ? `url(${bgUrl})` : "linear-gradient(135deg, #1e293b, #0f172a)",
backgroundSize: "cover",
backgroundPosition: "center",
filter: "brightness(0.7)",
transition: "background-image 0.8s ease",
}} />
{/* ===== 角色立绘层 ===== */}
{spriteUrl && (
<div style={{
position: "absolute",
bottom: "120px",
left: "50%",
transform: "translateX(-50%)",
zIndex: 2,
transition: "opacity 0.5s ease",
}}>
<img
src={spriteUrl}
alt={`${speaker} - ${emotion}`}
style={{
maxHeight: "350px",
objectFit: "contain",
filter: "drop-shadow(0 4px 12px rgba(0,0,0,0.5))",
}}
onError={(e) => { e.target.style.display = "none"; }}
/>
</div>
)}
{/* ===== 对话框层 ===== */}
<div style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
zIndex: 3,
background: "linear-gradient(transparent, rgba(0,0,0,0.85) 30%)",
padding: "60px 24px 24px",
}}>
{/* 角色名标签 */}
{speaker !== "Narrator" && (
<div style={{
display: "inline-block",
padding: "4px 16px",
marginBottom: "8px",
background: "rgba(99,102,241,0.8)",
borderRadius: "6px 6px 0 0",
color: "#e0e7ff",
fontSize: "14px",
fontWeight: "bold",
letterSpacing: "0.05em",
}}>
{speaker}
</div>
)}
{/* 文本内容 */}
<div style={{
background: "rgba(15,23,42,0.9)",
borderRadius: speaker !== "Narrator" ? "0 12px 12px 12px" : "12px",
padding: "16px 20px",
border: "1px solid rgba(148,163,184,0.2)",
minHeight: "80px",
}}>
{parsed
.filter((p) => !p.isChoice)
.map((p, i) => (
<p key={i} style={{
margin: i > 0 ? "10px 0 0" : "0",
color: p.isNarration ? "#94a3b8" : "#e2e8f0",
fontStyle: p.isNarration ? "italic" : "normal",
fontSize: "15px",
lineHeight: 1.8,
}}
dangerouslySetInnerHTML={{
__html: renderMarkdown(
p.isNarration
? p.text.replace(/^\*|\*$/g, "")
: p.text
),
}}
/>
))
}
</div>
</div>
{/* ===== 选项按钮层 ===== */}
{showChoices && choices.length > 0 && (
<div style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 4,
display: "flex",
flexDirection: "column",
gap: "10px",
width: "80%",
maxWidth: "400px",
}}>
{choices.map((choice, i) => (
<button
key={i}
onClick={() => {
api.setVariable("show_choices", false);
api.sendMessage(choice);
}}
style={{
padding: "14px 20px",
background: "rgba(30,27,75,0.9)",
border: "1px solid rgba(99,102,241,0.6)",
borderRadius: "10px",
color: "#c7d2fe",
fontSize: "15px",
fontWeight: "600",
cursor: "pointer",
textAlign: "left",
transition: "all 0.2s ease",
backdropFilter: "blur(8px)",
}}
onMouseEnter={(e) => {
e.target.style.background = "rgba(67,56,202,0.8)";
e.target.style.borderColor = "#818cf8";
}}
onMouseLeave={(e) => {
e.target.style.background = "rgba(30,27,75,0.9)";
e.target.style.borderColor = "rgba(99,102,241,0.6)";
}}
>
{choice}
</button>
))}
</div>
)}
{/* ===== 玩家输入层 ===== */}
{/* 没有 <Chat /> 包裹,直接放入 <MessageInput />,让玩家仍然可以打字 */}
<div style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
zIndex: 5,
}}>
<MessageInput />
</div>
</div>
);
}逐块说明:
- 清理内容:
cleanContent把[current_bg: set "xxx"]这样的指令行过滤掉(所有操作类型都能匹配:set/add/subtract/multiply/toggle/append/merge/push/delete,还有[hp: -10]这种简写指令)。指令引擎已经解析过了,界面上不用再显示 - 解析段落:按空行把文字分成段落,每段归成旁白(以
*开头)、台词(普通文字)或选项(以A)这种格式开头) - 背景层:用
backgroundImage显示当前场景的背景。filter: brightness(0.7)把它稍微调暗,前面的文字更好读。transition让换背景时有淡入淡出 - 立绘层:按
speaker和emotion在SPRITES里找立绘。onError会把加载失败的图片藏起来。旁白模式下,或者这个名字和表情没有对应的条目时,不显示立绘 - 对话框层:底部的半透明框。
speaker不是「Narrator」时,对话框上方会出现角色名标签。旁白是灰色斜体,台词是白色正体 - 选项按钮层:
show_choices为true,而且文字里有A)B)C)格式的选项时,按钮出现在画面中间。点一个按钮会藏起选项(show_choices设成false),并把玩家选的内容发出去
立绘怎么找
SPRITES 的键是小写的角色名、一个下划线、再加表情:yuki_happy。你上传的每张立绘都加一行。角色名不是纯英文字母的话,键随便起一个,再告诉 AI 在 current_speaker 里一字不差地写这个名字。值也可以是一个 https:// 链接。
第 6 步:全屏布局,还是聊天加自定义气泡
上面的代码返回的是它自己的布局,所以会占满画面。根元素用的是 width: "100%" 和 minHeight: "500px";想让它永远铺满窗口,就用 100vh。没有单独的全屏开关。
两种写法对比:
- 普通聊天 + 自定义气泡:
return <Chat renderBubble={...} />,保留平台的聊天外壳,只改气泡的样子 - 纯视觉小说(本食谱):
return <div>...所有视觉小说元素...<MessageInput /></div>,完全不用<Chat />,所有东西都由你的代码来画
在聊天和视觉小说之间切换:想要「平时是普通聊天,特殊时刻切成视觉小说」,就在界面代码里按一个变量(比如
vn_mode)分支。关着时返回<Chat />,开着时返回视觉小说的布局。AI 或者一条行为去切这个变量,玩到一半画面就换过来了。
第 7 步:AI 怎么驱动画面:指令示例
下面是玩的时候 AI 的回复怎么改变画面。
场景 1:开场(旁白模式)
AI 的回复:
[current_bg: set "@asset:your-classroom-reference"]
[current_speaker: set "Narrator"]
[speaker_emotion: set "neutral"]
*四月的早晨。空气中弥漫着樱花的甜香。*
*你走进教室,发现一个不认识的女孩坐在窗边。她用手托着下巴,望着窗外,沉浸在自己的思绪中。*显示结果:教室背景 + 没有立绘 + 灰色斜体的旁白。
场景 2:角色对话
AI 的回复:
[current_speaker: set "Yuki"]
[speaker_emotion: set "surprised"]
*她似乎注意到你在看她,转过头来。*
哦,你好。你也是这个班的吗?
[speaker_emotion: set "shy"]
抱歉,我今天刚转学来……还不太认识谁。显示结果:背景不变(没有 current_bg 指令就保持上一个值)+ Yuki 的立绘,用的是这段回复里最后一个表情(害羞)+ 对话框显示名字「Yuki」+ 斜体旁白和正体台词交替出现。
场景 3:给玩家一个选择
AI 的回复:
[current_speaker: set "Narrator"]
[show_choices: set true]
*Yuki 看着你,眼中带着一丝期待。*
*你该怎么做?*
A) 自我介绍,和她聊起来
B) 简单点个头,回到自己的座位
C) 提出带她参观教室和学校显示结果:旁白 + 三个可以点的按钮出现在画面中间。玩家点了其中一个,按钮消失,选中的文字作为玩家的回复发给 AI。
场景 4:换场景
AI 的回复:
[current_bg: set "@asset:your-hallway-reference"]
[current_speaker: set "Narrator"]
*铃声响起。走廊里瞬间热闹起来,学生们三三两两地涌出教室。*
[current_speaker: set "Yuki"]
[speaker_emotion: set "happy"]
一起去天台吃午饭吧?我发现了一个很不错的地方。显示结果:背景过渡到走廊(带淡入淡出)+ 旁白 + Yuki 开心的立绘 + 台词。
第 8 步:斜体旁白和普通台词:解析规则
界面代码用一条简单的规则区分两种文字:
| 格式 | 识别为 | 显示样式 | 用途 |
|---|---|---|---|
*这是斜体文字* | 旁白 | 灰色(#94a3b8),斜体 | 环境描写、角色动作、内心独白 |
这是普通文字 | 台词 | 白色(#e2e8f0),正体 | 角色说的话 |
A) 选项文字 | 选项 | 按钮 | 玩家可以点的选择 |
这些规则已经在那条设定里告诉 AI 了。如果它偶尔把格式写错(比如用斜体写台词),不是以 * 开头的都会被当成台词,所以不会出问题。
为什么用斜体,不用
>引用块或**粗体**? 大多数模型在角色扮演里本来就用斜体写旁白和动作,所以这是它们最能稳定照做的格式。
第 9 步:保存并测试
- 点顶栏的 保存
- 点 试玩 开始试玩(试玩会花额度)
- 你应该能看到全屏的视觉小说画面:背景 + 对话框 + 开场旁白
- 在输入框里打一句话(比如「跟她打个招呼」)
- AI 的回复应该带着指令:背景可能会换,角色出现,台词显示在对话框里
- 如果 AI 给了选项,按钮会出现在画面中间。点一个试试
- 接着聊,看 AI 换场景时会不会更新
current_bg,角色说话时会不会更新current_speaker和speaker_emotion。试玩里的 这回合 能看到某个值为什么变了或者没变
如果出了问题:
| 现象 | 可能的原因 | 怎么改 |
|---|---|---|
| 背景是黑的,或者只是一片渐变色 | current_bg 是空的,或者不是有效的图片引用 | 在试玩里看一下这个变量的值。它应该是 面板 → 资源 里的 @asset: 引用,或者一个 https:// 链接 |
| 看不到立绘 | SPRITES 里没有这个名字和表情的条目 | 检查键是不是小写名字 + _ + 表情(yuki_happy),引用对不对 |
| 指令行显示在画面上 | 指令格式不标准,正则没匹配上 | 确认格式是 [variableName: set "value"],注意冒号后面有空格 |
| 全都是旁白 / 全都是台词 | AI 没照格式规则写 | 检查设定里的格式说明写清楚了没有。也可以在行为规则里再强调一下 |
| 选项按钮没出现 | show_choices 没设成 true,或者没有 A) 格式的选项 | 检查 AI 的回复里有没有 [show_choices: set true] 和 A) 格式的选项 |
| 画面没有占满 | 根元素没铺满可见区域 | 在最外层的 div 上加 minHeight: "100vh" 或 height: "100%" |
| 什么都没显示 | 编译出错 | 面板 → 前端代码 会显示 错误 和提示信息,按它指的那一行去改 |
进阶技巧
多个角色对话
同一段回复里可以切换好几个角色:
[current_speaker: set "Yuki"]
[speaker_emotion: set "happy"]
今天天气真好!
[current_speaker: set "Teacher"]
[speaker_emotion: set "neutral"]
好了同学们,开始上课了。请回到座位上。
[current_speaker: set "Narrator"]
*教室瞬间安静了下来。*引擎按顺序应用这些指令,所以画面最后显示的是最后一个 current_speaker 的立绘。想让每一段都显示各自角色的立绘,就改代码,让每个段落去读它前面最近的那条 [current_speaker: set ...] 指令(从消息的原始文字里读)。
过渡效果
背景层的 CSS 里有 transition: background-image 0.8s ease,换背景时会淡入淡出。不同类型的场景也可以用不同的过渡:
- 普通切换:淡入淡出(已经有了)
- 闪回 / 回忆:加一层白色闪光遮罩
- 紧张的场景:加一个画面震动动画
搭配音效和 BGM
给每个场景配上自己的音乐。可以在音轨上用 条件 BGM(current_bg 是走廊的引用时放走廊的曲子,见音频设计),也可以加一条行为:什么时候触发 → 变量变化 盯 current_bg,如果…… current_bg 等于这个场景的引用,效果 播放音乐。昼夜循环食谱用同样的办法按时段换音乐。
速查
| 你想要 | 怎么做 |
|---|---|
| 换背景 | AI 发 [current_bg: set "@asset:..."] |
| 换说话的人 | AI 发 [current_speaker: set "characterName"] |
| 换表情 | AI 发 [speaker_emotion: set "emotion"] |
| 显示选项按钮 | AI 发 [show_choices: set true],再按 A) B) C) 格式列出选项 |
| 区分旁白和台词 | *斜体* = 旁白,普通文字 = 台词 |
| 全屏的视觉小说 | index.tsx 直接返回视觉小说布局(不用 <Chat />),底部放 <MessageInput /> 给玩家输入 |
| 角色立绘 | 传到 面板 → 资源,把引用按 名字_表情 列进 SPRITES |
| 玩家点选项时发消息 | 按钮的 onClick 调用 api.sendMessage(choiceText) |
| 不写代码做立绘、跟着变量换的背景、选项按钮 | 设定 → 立绘…;玩家界面的 图片 部件设成 跟着 某个变量换;玩家界面上的按钮 |
