Skip to content

自定义 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 时,添加微妙的心形粒子效果。」

迭代过程

没有人第一次就做到完美。最好的世界是通过反复打磨构建的:

  1. 描述大局 -- 「我想要一个带场景背景和角色立绘的视觉小说布局」
  2. 查看 Canvas 预览 -- 布局感觉对吗?间距合适吗?
  3. 细化细节 -- 「对话框做得更透明些。角色立绘移到右边。对话用衬线字体。」
  4. 加入打磨 -- 「场景切换时加淡入动画。好感度增加时让指示条发光。」

每轮只需几秒。五轮迭代胜过一小时试图一次性描述所有东西。

告诉 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 在哪里

完整的逐方法参考(含类型签名和示例)在两个地方:

构建复杂 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() -- 变量、消息、流式状态等
  • 使用 varfunction() 而非 const/let/箭头函数
  • 不使用 TypeScript 语法(无泛型、无 as 断言、无 interface)

何时使用:复杂的多文件 UI,当你想对对话有更多控制,或者你已经在 AI 代码编辑器中工作时。

路径 3:手写代码(适合有经验的开发者)

打开编辑器,进入 Custom UI,直接写 TSX。实时预览随你输入更新。

何时使用:你是一个用 React 思考的开发者,或者你想精确控制每个细节。


自定义 UI 代码的基本规则

无论走哪条路径都适用。如果你使用 Studio AI,它会自动处理这些——本节是为了理解底层发生了什么,或者在出问题时调试用。

六条规则

1. 入口文件格式

index.tsx 必须导出一个默认函数组件。这是你 UI 的根:

tsx
export default function MyWorld() {
  return <Chat />;
}

2. 全局变量——不要 import 它们

以下在任何地方都已可用,无需 import:ReactuseYuminaIconsChatMessageListMessageInputuseAssetFont,以及所有 Tailwind CSS 类。

import React from "react" 不会报错(会被静默剥离),但没必要。

3. 你自己的文件可以 import

多文件 Root Component 使用 ES module 语法:

tsx
import StatBar from "./stat-bar"
import DialogueBox from "./dialogue-box"

4. 使用 React.useState(),而非 useState()

React 作为模块在作用域内,但单个 hook 没有解构。始终加 React. 前缀:

tsx
var [count, setCount] = React.useState(0)

5. 使用 varfunction(),而非 const/let/箭头函数

沙箱有时在 const/let 和箭头函数上有作用域问题。varfunction() 更稳健:

tsx
// 推荐
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={...} /> 接管气泡渲染,平台处理其他一切(滚动、流式输出、滑动切换、输入)。

何时使用:你想要主题化消息(暗黑恐怖、优雅浪漫、科幻终端)而不需要重建整个聊天。

代码示例:带状态栏的主题气泡
tsx
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 栏
tsx
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 通过指令更新这些变量。

何时使用:恋爱、剧情、日常故事——视觉氛围比传统聊天界面更重要的场景。

代码示例:带场景背景的视觉小说框架
tsx
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、冒险游戏——任何玩家需要在聊天时参考状态或道具栏的世界。

代码示例:聊天 + 侧边栏
tsx
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>
  )
}

交互按钮和选项

点击后发送消息或设置变量的按钮。超越打字的最简单交互形式。在开场问候语中特别强大——把它变成角色创建界面、难度选择器或分支故事开场。

何时使用:任何你希望玩家从选项中选择而非(或除了)打字的世界。

代码示例:问候语作为角色创建
tsx
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 和箭头函数。 沙箱有时在这些上有作用域问题。使用 varfunction() 代替。Studio AI 自动这样做,但如果你手写代码或从外部 AI 粘贴,要注意这一点。

Import 全局变量。import React from "react"import { useState } from "react" 可能导致错误。React、useYumina、Icons、Chat、MessageList、MessageInput——这些都是全局的。不要 import 它们。


延伸阅读

  • API ReferenceuseYumina()ai.complete()injectContext() 和每个 bridge 方法的完整逐方法参考
  • 设计游戏状态 — 你的 UI 读取和显示的变量
  • 音频设计 — 通过 bridge API 从自定义 UI 播放音频
  • AI 指令与宏 — AI 如何更新你的 UI 渲染的状态

机器可读规范 → World Spec: Custom UI