自定义 UI 指南
平台上 76% 的世界使用自定义 UI。排名第一的世界(大逃杀)有完整的战术界面,包含血条、击杀信息流和动态地图。最热门的恋爱世界(樱花季)使用视觉小说布局,带有角色立绘和场景背景。而黑马之作 Still 在 UI 中运行着一个完整的解谜游戏。
这些创作者都没有自己写代码。他们描述想要什么,Studio AI 帮他们构建。
本指南教你如何让你的世界看起来惊艳。如果你还没读过自定义 UI — 基础篇,请先从那里了解概览。
世界的三种视觉风格
Yumina 上的每个世界都归入三种视觉风格之一。你在此做出的决定将影响一切。
默认聊天(无需自定义 UI)
消息以文字气泡形式出现。底部有输入框。一切自然滚动。这是每个新世界的起点,对很多世界来说也完全够用。
适用于:日常角色扮演、简单冒险、角色聊天——任何以文字本身为核心体验的世界。
如果 AI 的文笔是你世界的特色,默认聊天让玩家保持专注。不要仅仅因为可以就添加自定义 UI。
自定义消息气泡
平台上最受欢迎的自定义方式。38% 的世界使用这种模式。你保留完整的聊天体验(滚动、流式输出、滑动切换、输入框),但改变每条消息的外观。
这能解锁:
- 契合世界氛围的主题背景和字体
- 对话旁边的角色头像
- 每条消息上可见的状态栏(HP、金币、好感度)
- 暗色调和诡异字体的恐怖游戏风格
- 多角色场景中按角色着色
你不是在替换聊天。你是在装饰它。
全应用模式
最强大的选项。你控制每一个像素。聊天只是你整体设计中的一个组件,或者你可以完全跳过它,构建截然不同的东西。
这能解锁:
- 带场景背景和角色立绘的视觉小说引擎
- 点击地点即发送消息的地图导航
- 回合制战斗画面
- 手机模拟器,不同「app」触发不同的 AI 行为
- 交互式仪表盘、道具栏画面、任务日志
大逃杀用全应用模式展示战术概览。Still 用它实现解谜界面。这些世界看起来完全不像聊天应用。
做决定:从默认聊天开始。如果你的世界需要视觉氛围,添加自定义气泡。如果你的世界需要交互面板、游戏化布局或非聊天体验,使用全应用模式。
Studio AI 是你的构建者
你不需要写代码。你需要知道自己想要什么并清晰描述它。
工作流程
打开编辑器,点击进入 Studio,与 AI 助手对话。用自然语言描述你想要的外观。Studio AI 生成代码,你在 Canvas 面板中看到实时预览。
好的提示
描述越具体,结果越好。模式是:描述布局、氛围和要显示哪些变量。
模糊(AI 得猜所有东西):
「做得酷一点」
好(清晰的布局和氛围):
「我想要一个暗黑恐怖风的界面。顶部红色血条,消息样式做成老式打字机文字写在泛黄纸张上,边缘有暗雾效果。」
很好(布局 + 氛围 + 具体变量 + 行为):
「构建一个视觉小说布局。全屏背景图来自
scene_bg变量。左侧角色立绘来自character_sprite。底部半透明对话框,角色名来自speaker_name用粉色显示。当好感度超过 75 时,添加微妙的心形粒子效果。」
迭代过程
没有人第一次就做到完美。最好的世界是通过反复打磨构建的:
- 描述大局 -- 「我想要一个带场景背景和角色立绘的视觉小说布局」
- 查看 Canvas 预览 -- 布局感觉对吗?间距合适吗?
- 细化细节 -- 「对话框做得更透明些。角色立绘移到右边。对话用衬线字体。」
- 加入打磨 -- 「场景切换时加淡入动画。好感度增加时让指示条发光。」
每轮只需几秒。五轮迭代胜过一小时试图一次性描述所有东西。
告诉 Studio AI 你的变量
Studio AI 可以读取你世界的变量定义,但明确说明什么重要会有帮助:
「我的变量:health(0-100,显示为红色条),gold(数字,显示为带金币图标的文字),location(字符串如 'forest' 或 'cave',显示在右上角),is_night(布尔值,为 true 时将背景调暗)」
可以做什么:bridge API
你的自定义 UI 能做的远不止显示变量。以下是 bridge 给你的能力,以你能构建什么来解释,而非要调用什么函数。
读取游戏状态
你的 UI 可以读取任何变量、完整的消息历史、当前玩家是谁、选择了哪个模型、AI 是否正在生成等。这就是状态栏、道具栏和任务追踪器的工作原理——它们读取变量并可视化显示。
控制聊天
你的 UI 可以以玩家身份发送消息、编辑或删除现有消息、要求 AI 重新生成、中途停止生成、或重新开始对话。这就是交互按钮的工作原理——一个「喝药水」按钮以玩家消息的形式发送文字,触发 AI 回应。
播放音频
你的 UI 可以播放背景音乐、音效,在曲目间渐变切换,并控制音量。结合变量,你可以实现音乐自动随地点或心情变化。
Side completions — 一个世界中的多个 AI 「声音」
这是整个平台最强大的能力之一。你的 UI 可以调用 ai.complete() 来运行一个独立的 AI 对话,完全不影响主聊天。AI 只对你的 UI 回应——玩家看不到它作为聊天消息,也不会影响主对话的历史或状态。
想想这能解锁什么:
- NPC 手机对话:一个角色在你的 UI 中有自己的聊天窗口。玩家给他们发消息,AI 以那个角色的声音回复,主故事线继续独立运行。每个配角可以有自己的系统 prompt 和人格。
- AI 生成的物品描述:玩家在道具栏中悬停在一件物品上,AI 根据当前故事上下文即时写出独特描述。
- 提示系统:一个「思考」按钮,分析玩家的处境并给出提示,不会让主 AI 跳出角色。
- 内心独白面板:一个侧边面板,展示 NPC 在想什么,由不同于驱动对话的 AI prompt 生成。
- 翻译或摘要面板:伴随主聊天的实时 AI 驱动的摘要或翻译。
你可以传入 includeLorebook: "matched" 让侧面 AI 看到与主聊天相同的世界设定和角色描述——让侧面对话保持在世界观内而不偏离。或者省略它用于不需要世界上下文的任务(翻译、分类、纯工具性功能)。
Side completions 与主聊天共享相同的速率限制和信用计费。完整方法签名、限制和 includeLorebook 选项请参阅 API Reference。
隐形上下文注入
你的 UI 可以发送一条消息,主 AI 在下一回合看到它但玩家在聊天中永远看不到。调用 injectContext(),引擎会在下一次 prompt 中插入一条一次性的 system(或 user)消息,然后自动丢弃。
这让 AI 能对主对话之外发生的事情做出反应:
- 幕后事件:「NPC 在你离开后自言自语:『我不能让他们找到那封信。』」AI 自然地将此融入下一次回复。
- 环境变化:「开始下雨了。洞穴入口现在部分被淹没。」玩家看不到这条指令,但 AI 会描述雨。
- UI 驱动的后果:当玩家在自定义 UI 中点击按钮(比如从商店偷东西),注入上下文告诉 AI 发生了什么,让它做出反应。
- 手机消息和通知:「你刚收到一条神秘短信:『今晚,9 点,老地方。』」AI 将其融入叙事,玩家看不到系统消息。
与 ai.complete() 运行独立 AI 调用不同,injectContext() 会融入主 AI 的下一次回复。两者互补:想要独立的 AI 声音时用 ai.complete(),想让主 AI 知道玩家没说的事时用 injectContext()。
方法签名请参阅 API Reference。
保存和加载
跨会话持久存储。高分、解锁的成就、玩家偏好、自定义设置——任何你想在游玩会话之间记住的东西。
导航和通知
切换沉浸模式、显示 toast 通知、复制文本到剪贴板、在问候语变体间切换。你的 UI 拥有平台内置界面同样的控制能力。
完整 API 在哪里
完整的逐方法参考(含类型签名和示例)在两个地方:
- API Reference -- 带实例的导览
- World Spec: Custom UI -- 机器可读规范,为 AI 消费设计
构建复杂 UI 时,把 World Spec 给 Studio AI、Claude 或 Cursor。它们会处理技术细节。
三种自定义路径
路径 1:使用 Studio AI(推荐给大多数创作者)
大多数成功世界走的路。你描述想要什么,Studio AI 写代码,你通过对话迭代。
优势:不需要代码知识。迭代快速。Studio AI 了解完整 API,自动处理边界情况(流式输出、空状态、移动端布局)。
何时使用:始终从这里开始。只有遇到 Studio AI 做不到的事情才切换到其他路径。
路径 2:使用外部 AI(Claude、Cursor、ChatGPT)
如果你偏好不同的 AI 工具,或者你在构建复杂功能需要更长的对话,可以使用任何能写代码的 AI。关键是给它 Yumina 的技术上下文。
告诉外部 AI:
- 你的代码是 TSX(React),在沙箱 iframe 中运行
- 所有东西都是全局可用的:React、useYumina、Icons、Chat、MessageList、MessageInput、Tailwind CSS
- 入口文件是
index.tsx,包含export default function MyWorld() { ... } - 游戏状态来自
useYumina()-- 变量、消息、流式状态等 - 使用
var和function()而非const/let/箭头函数 - 不使用 TypeScript 语法(无泛型、无
as断言、无 interface)
何时使用:复杂的多文件 UI,当你想对对话有更多控制,或者你已经在 AI 代码编辑器中工作时。
路径 3:手写代码(适合有经验的开发者)
打开编辑器,进入 Custom UI,直接写 TSX。实时预览随你输入更新。
何时使用:你是一个用 React 思考的开发者,或者你想精确控制每个细节。
自定义 UI 代码的基本规则
无论走哪条路径都适用。如果你使用 Studio AI,它会自动处理这些——本节是为了理解底层发生了什么,或者在出问题时调试用。
六条规则
1. 入口文件格式
index.tsx 必须导出一个默认函数组件。这是你 UI 的根:
export default function MyWorld() {
return <Chat />;
}2. 全局变量——不要 import 它们
以下在任何地方都已可用,无需 import:React、useYumina、Icons、Chat、MessageList、MessageInput、useAssetFont,以及所有 Tailwind CSS 类。
写 import React from "react" 不会报错(会被静默剥离),但没必要。
3. 你自己的文件可以 import
多文件 Root Component 使用 ES module 语法:
import StatBar from "./stat-bar"
import DialogueBox from "./dialogue-box"4. 使用 React.useState(),而非 useState()
React 作为模块在作用域内,但单个 hook 没有解构。始终加 React. 前缀:
var [count, setCount] = React.useState(0)5. 使用 var 和 function(),而非 const/let/箭头函数
沙箱有时在 const/let 和箭头函数上有作用域问题。var 和 function() 更稳健:
// 推荐
var api = useYumina()
var items = api.variables.inventory || []
// 不推荐
const api = useYumina()
const items = api.variables.inventory ?? []6. 不使用 TypeScript 语法
不用泛型(<T>)、不用 interface、不用 as 类型断言、不用 satisfies。沙箱编译 TSX 但不编译完整 TypeScript。
常见模式
这些是顶级世界组合使用的构建模块。每个描述告诉你模式做什么以及何时使用。代码示例是可折叠的——仅供参考,Studio AI 会为你生成。
自定义消息气泡
最常见的模式。保留完整聊天体验,只改变消息外观。使用 <Chat renderBubble={...} /> 接管气泡渲染,平台处理其他一切(滚动、流式输出、滑动切换、输入)。
何时使用:你想要主题化消息(暗黑恐怖、优雅浪漫、科幻终端)而不需要重建整个聊天。
代码示例:带状态栏的主题气泡
export default function MyWorld() {
var api = useYumina()
return (
<Chat renderBubble={function(msg) {
if (msg.role === "user") {
return (
<div className="ml-auto max-w-[80%] rounded-xl bg-blue-500/20 px-4 py-3 text-blue-100">
{msg.rawContent}
</div>
)
}
return (
<div className="mr-auto max-w-[85%] rounded-xl border border-zinc-700 bg-zinc-900 p-4">
<div dangerouslySetInnerHTML={{ __html: msg.contentHtml }} />
<div className="mt-3 flex gap-4 text-xs text-zinc-400">
<span>HP {api.variables.health}/100</span>
<span>Gold {api.variables.gold}</span>
</div>
</div>
)
}} />
)
}状态显示与 HUD
固定面板显示生命值、金币、好感度、地点或其他变量。通常放在聊天上方(使用 <Chat> 的 children prop)或旁边(flex 布局)。
何时使用:你的世界追踪玩家需要随时看到的数据——RPG、生存游戏、带好感度计的恋爱模拟。
代码示例:顶部 HUD 栏
export default function MyWorld() {
var api = useYumina()
return (
<Chat>
<div className="shrink-0 px-4 py-2 bg-black/60 backdrop-blur flex gap-4 text-xs text-zinc-300">
<div className="flex items-center gap-1">
<Icons.Heart className="w-3 h-3 text-red-400" />
<span>{api.variables.health || 100}/100</span>
</div>
<div className="flex items-center gap-1">
<Icons.Coins className="w-3 h-3 text-amber-400" />
<span>{api.variables.gold || 0}</span>
</div>
<div className="ml-auto text-zinc-500">
{api.variables.location || "Unknown"}
</div>
</div>
</Chat>
)
}视觉小说布局
全屏场景背景、角色立绘、底部半透明对话框。最具电影感的选项。通常从变量中读取场景和角色数据,AI 通过指令更新这些变量。
何时使用:恋爱、剧情、日常故事——视觉氛围比传统聊天界面更重要的场景。
代码示例:带场景背景的视觉小说框架
export default function MyWorld() {
var api = useYumina()
var bg = api.variables.scene_bg
var sprite = api.variables.character_sprite
var speaker = api.variables.speaker_name
var lastMsg = (api.messages || []).slice(-1)[0]
return (
<div
className="relative w-full h-full bg-cover bg-center"
style={{
backgroundImage: bg
? "url(" + bg + ")"
: "linear-gradient(135deg, #1e293b, #0f172a)"
}}
>
{sprite && (
<img
src={sprite}
className="absolute bottom-0 left-1/2 -translate-x-1/2 max-h-[80%] pointer-events-none"
/>
)}
<div className="absolute inset-x-4 bottom-4">
<div className="rounded-xl border border-white/10 bg-black/70 p-4 backdrop-blur-sm">
{speaker && (
<div className="mb-1 text-sm font-bold text-pink-300">{speaker}</div>
)}
<div className="leading-relaxed text-zinc-100">
{lastMsg ? lastMsg.content : ""}
</div>
</div>
<div className="mt-2">
<MessageInput />
</div>
</div>
</div>
)
}侧边栏游戏面板
左侧聊天,右侧固定面板显示角色信息、状态、道具栏或地图。两全其美:玩家获得完整聊天体验的同时还有持久的游戏信息。
何时使用:RPG、冒险游戏——任何玩家需要在聊天时参考状态或道具栏的世界。
代码示例:聊天 + 侧边栏
export default function MyWorld() {
var api = useYumina()
return (
<div className="flex h-full">
<div className="flex-1 min-w-0">
<Chat />
</div>
<aside className="w-72 shrink-0 border-l border-border bg-card p-4 overflow-y-auto">
<div className="text-sm font-bold mb-3">{api.variables.player_name || "Adventurer"}</div>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">HP</span>
<span>{api.variables.health || 100}/{api.variables.max_health || 100}</span>
</div>
<div className="h-1.5 rounded-full bg-zinc-800 overflow-hidden">
<div
className="h-full bg-red-500 transition-all duration-300"
style={{ width: ((api.variables.health || 100) / (api.variables.max_health || 100) * 100) + "%" }}
/>
</div>
</div>
<div className="mt-4 text-xs text-muted-foreground">
<div className="font-medium mb-2">Inventory</div>
<div className="grid grid-cols-3 gap-1">
{(api.variables.inventory || []).map(function(item, i) {
return (
<div key={i} className="aspect-square rounded border border-border bg-muted flex items-center justify-center text-[10px]">
{item.name || "?"}
</div>
)
})}
</div>
</div>
</aside>
</div>
)
}交互按钮和选项
点击后发送消息或设置变量的按钮。超越打字的最简单交互形式。在开场问候语中特别强大——把它变成角色创建界面、难度选择器或分支故事开场。
何时使用:任何你希望玩家从选项中选择而非(或除了)打字的世界。
代码示例:问候语作为角色创建
export default function MyWorld() {
var api = useYumina()
return (
<Chat renderBubble={function(msg) {
if (msg.messageIndex === 0 && msg.role === "assistant") {
return (
<div className="space-y-4">
<div dangerouslySetInnerHTML={{ __html: msg.contentHtml }} />
<div className="flex gap-3">
<button
onClick={function() {
api.setVariable("class", "Warrior")
api.sendMessage("I choose Warrior")
}}
className="px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-800 transition"
>
Warrior
</button>
<button
onClick={function() {
api.setVariable("class", "Mage")
api.sendMessage("I choose Mage")
}}
className="px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-800 transition"
>
Mage
</button>
</div>
</div>
)
}
return <div dangerouslySetInnerHTML={{ __html: msg.contentHtml }} />
}} />
)
}常见错误
不需要时添加自定义 UI。 默认聊天干净、快速、由平台维护。如果你世界的优势在于文笔质量,默认聊天让玩家保持专注。不要为了复杂而复杂。
忘了处理流式输出。 AI 生成时,msg.isStreaming 为 true 且内容不完整。你的气泡应该优雅地处理部分文本——不要假设内容是完整的来解析它。
不在移动端测试。 很多玩家用手机。如果你的侧边栏 320px 宽,手机屏幕放不下。使用响应式 Tailwind 类(hidden md:block 在小屏幕上隐藏面板)或在窄宽度下测试你的布局。
阻断输入。 如果你使用全应用模式但忘了包含 <MessageInput />(或者你自己的调用 api.sendMessage() 的输入组件),玩家无法与 AI 交谈。始终确保有发送消息的途径。
使用 const 和箭头函数。 沙箱有时在这些上有作用域问题。使用 var 和 function() 代替。Studio AI 自动这样做,但如果你手写代码或从外部 AI 粘贴,要注意这一点。
Import 全局变量。 写 import React from "react" 或 import { useState } from "react" 可能导致错误。React、useYumina、Icons、Chat、MessageList、MessageInput——这些都是全局的。不要 import 它们。
延伸阅读
- API Reference —
useYumina()、ai.complete()、injectContext()和每个 bridge 方法的完整逐方法参考 - 设计游戏状态 — 你的 UI 读取和显示的变量
- 音频设计 — 通过 bridge API 从自定义 UI 播放音频
- AI 指令与宏 — AI 如何更新你的 UI 渲染的状态
机器可读规范 → World Spec: Custom UI
