开发笔记

视觉小说就是一组图片。为什么 Builder 会变得如此复杂?

一个名为 Fibber 的小型原型,如何成长为支持协作、版本化发布和共享故事 runtime 的分布式平台。

从外部看,视觉小说几乎简单得不能再简单:一张背景、一个带透明通道的角色、一个文本框、几个指向下一场景的选项,再加上一些音乐。我开始制作第一版时,正是这样理解它的。

我依然认为它的核心很简单。今天,尤其有 AI 相助时,几乎任何开发者都能做出视觉小说播放器。真正的难点始于目标不再是制作一个故事,而是打造一种工具,让另一个人,甚至可能是一个孩子,也能创建、测试、发布并持续完善自己的故事。

第一版只有一个任务

在 Romergo 叫作 Romergo 之前,它叫 Fibber。它把相连的场景组成一个任务,让我用文本和图片填充场景,并支持分支选项。技术栈是 TypeScript、React、Strapi 和 Ant Design。

那个版本完成了任务。流程图让故事变得可见,场景可以编辑,结果也可以预览。对原型来说已经足够,也足以暴露真正的问题。

第一个真正的瓶颈并不是代码

当我们开始为一个真实任务填充文本和图片时,组装一段十分钟的场景几乎要花一整天。创建场景。上传背景。上传角色。摆放它们。添加对白。连接下一个场景。然后重复。

其中一部分是界面问题。我改进了 UX,加快了上传,也让发布更可靠。但最大的成本没有改变:每一个细小操作仍然需要人亲手完成。

如果两个人能同时创作呢?

最初的答案很简单:如果一个人是瓶颈,就让几个人一起处理同一个故事。我从 Liveblocks 开始,学习这种模型,后来逐步转向以 Yjs 为核心的自有协作层。

下面的实验看起来只是两个编辑器窗口同步变化,背后却是产品思维的一次重大转变:编辑不再是彼此孤立的表单提交,而是共享文档的更新。由此才有了在线状态、断线重连、冲突处理、权限,以及项目应当始终处于活动状态的预期。

每一条有用的捷径最终都会成为边界

Strapi 是快速获得 backend 的好方法,但每增加一个故事专用功能,都要求 CMS 少一点像 CMS。Ant Design 为最初的界面带来了速度和一致性,随后也逐渐让产品受到他人视觉体系的限制。

后来基于 Remix 的 serverless 迭代是重要的一步,但我把应用、媒体和部署组合在一起的方式,仍无法提供产品开始需要的横向自由度。这些选择都不是错误,每一个都为我争取到了发现下一个限制所需的时间。

图片不是附件,而是基础设施。

媒体给我上了单独的一课。故事图片应该只上传一次,在需要时转换,安全存储,并快速送达世界各地的玩家。把它与不断增长的应用服务器放在一起,只会让 backend 更沉重,所以最初的独立方案是 Cloudinary。

有一天晚上,我打开控制台,看到一个用户通过数百次上传同一张图片消耗了约 500 MB。我的紧急修复会比较图片哈希并拒绝重复内容。它有效,但频率限制和配额本可以更直接地约束这种行为。更意外的是带宽:一个晚上的测试就可能消耗免费额度的十分之一以上。

那次经历改变了模型。媒体不能再只是场景中的一个字段,它需要自己的存储、去重、分发、缓存和访问 pipeline。

这个简单想法如今的样子

现在的 Romergo 将 Builder 与播放器分开,但两者使用同一个故事 runtime。编辑器内的预览与已发布的游玩过程遵循相同规则,因此更难引入契约偏差。

Yjs 在协作者之间同步可编辑项目。发布会创建经过检查的故事数据和媒体 snapshot,让创作者可以继续修改草稿,却不会悄悄改变玩家已经运行的版本。API 基于 Hono,界面使用 shadcn 和 Radix 原语,云端层提供分布式协调、对象存储和 CDN 分发。

这个共享 runtime 现在会把故事带到浏览器和 PWA、Telegram,以及同步的 Discord 体验中。版本兼容、离线行为和多人状态不再只是页面周围的附带问题,而是独立的产品系统。

当前架构

一个可编辑的故事。一份已发布的契约。

Romergo 当前架构的概括示意图。

1. 可编辑项目

  • Builder UI: 场景 · 流程 · 媒体
  • AI / MCP: 经过身份验证的编辑操作
  • Realtime / Yjs: Durable Objects · 共享工作文档
  • Editor API: CAS 写入 · 草稿实体化
  • 草稿 runtime snapshot: PublishedQuestLocalizedContentV2 · D1 / R2
  • 草稿媒体: R2 对象 · D1 元数据

2. 版本化发布

  • 发布检查: Runtime schema · 作者确认 · 媒体存在性
  • 不可变发布版本 vN: Runtime snapshot · 复制并重新映射的媒体
  • 可见性契约: 公开 · 不公开列出 · 私有

3. 统一执行契约

  • 草稿 snapshot: 最新可编辑状态
  • 发布版本 vN: 稳定的播放器状态
  • 共享 player-runtime: 场景 · 转场 · 条件 · 变量 · 存档
  • Builder 预览: 使用同一引擎运行草稿

4. 播放渠道

  • Web / PWA: 浏览器播放器
  • Telegram Mini App: 嵌入式播放器
  • Discord Activity: 同步群组游玩

5. 平台基础设施

  • Clerk + OAuth: 身份与 MCP 访问
  • Hono Workers: API 和 MCP 服务
  • Cloudflare D1: 项目 · 版本 · 元数据
  • Cloudflare R2: 媒体 · 大型 runtime snapshots
  • Durable Objects: Yjs 房间 · Discord 会话
  • 玩家进度: D1 同步 · 本地离线队列

视觉小说依然只是一组图片

有趣的是,最初的判断仍然成立。视觉小说依然是背景、角色、文本、选择和声音。Romergo 之所以变复杂,是因为这个简单核心周围的一切都必须可靠:协作、媒体、发布、版本、离线游玩,以及体验同一个故事的多种方式。

如果这段演变教会了我一件事,那就是要衡量完整的人的路径,而不只是渲染最终画面的代码。播放器也许只需要五种基础元素,但一个帮助人们把想法变成完整可玩故事的产品,需要一套系统。

返回开发博客