开发笔记
视觉小说就是一组图片。为什么 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 之所以变复杂,是因为这个简单核心周围的一切都必须可靠:协作、媒体、发布、版本、离线游玩,以及体验同一个故事的多种方式。
如果这段演变教会了我一件事,那就是要衡量完整的人的路径,而不只是渲染最终画面的代码。播放器也许只需要五种基础元素,但一个帮助人们把想法变成完整可玩故事的产品,需要一套系统。