把视觉小说工具做成工作室能用的东西
在2024年的那个炎热的夏天,我和我的朋友开始构建一款视觉小说游戏。我发现了现有的很多主流方案 都古老且过时,难以找到所有方面都符合要求的引擎。于是NarraLeaf-React,一款简单的使用现代Web栈 的视觉小说播放器,诞生了。而在这之后的两年时间,它经过了大量的迭代,成为了一个更远大的项目, 也就是NarraLeaf Project的基石。NarraLeaf Project逐渐发展出了三条完整的分支,分别是React 播放器、名为NarraLeaf Desktop的基于Electron的桌面视觉小说框架,以及一款真正的编辑器 NarraLeaf Studio。
也许是心头一热,又或者是其他视觉小说引擎的体验实在是无法让我满意,我想要的并不是与大量的特性 搏斗和挣扎于组织代码。我需要的也不是什么极致的性能、漂亮的界面或强大的视觉效果,我真正想要做 的是将所有东西合在一起,成为一个“项目”,或者说一个工程,而非零散的代码。我逐渐懂得了取舍, 将目光放在了真正简化创作者的领域,而不是卷边际帧数和磁盘空间。
一个工程需要跟着工程该有的思路走。要能版本管理,要能被人接手,要能扩展和产出专业的工作流。 更重要的是,需要方便和直观。从一开始,我就将“直观”贯彻到了整个项目之中。每一个接口, 每一个概念,每一片界面,我和我的团队都在不断打磨其直观性。
这篇博客讲的是这个项目想改变什么,以及到现在为止解决了哪些问题。实现层面的东西我打算之后 单独写。
封闭的工具
我用过的引擎里有一类是封闭的。素材和逻辑放进去之后就只属于那个工具了,而它和外面的世界之间 没有接口。
最明显的是迁移成本。一个做到一半的项目如果想换工具,通常只有原始素材能带走。剧本结构、变量、 分支关系、界面布局全都得重做,因为它们从来没有以一种别的程序能读懂的形式存在过。可视化工具 尤其如此,它把你在界面上做的每一个决定存进自己的工程文件,而这个文件的格式是它自己的事, 不承诺给任何人看。
第二个问题是它们和现代软件开发之间没有接口。没有命令行就进不了CI,没有类型系统就没有编辑器 提示和安全的重构,没有lint就没有统一的规范。一个五个人的团队想约定提交前先跑一遍检查, 在这类工具上根本无从谈起,因为没有可以跑的东西。
Ren'Py要分开说。它的脚本是纯文本,能进git,能diff,这一点比大多数可视化工具都好。它的问题 不在能不能读,而在于它的DSL是唯一入口。所有逻辑最终都要用这门语言表达,于是一门本来只为叙事 设计的语言被迫长成一个不完整的通用语言,而通用语言该有的东西,类型系统、模块化、包管理、 工具链,它都得从头补一遍。更细的部分我在NarraLeaf React背后的设计理念 里写过。
第三个问题是扩展。想加一个引擎没提供的功能,通常只剩两条路:改引擎源码,或者放弃。开源的引擎 至少还有第一条,但改完之后你就维护着一个分支,引擎每升一次级就是一次合并冲突。没有插件体系, 所有人的定制都停在自己那台机器上。
简单编辑器停在哪里
还有一类工具的方向完全相反,它们把入门门槛压到了几乎为零。装好、打开、跟着引导走,一两个小时 就能有一个跑得起来的demo,中间不需要写一行代码,也不需要知道构建是什么。这是真本事,也是 它们存在的理由。问题出现在项目继续长下去之后。
第一件是协作。这类工具大多默认使用者只有一个人。项目就是本地的一个文件夹或者一个工程文件, 没有内建的协作概念。我听说过的真实做法是团队用网盘共享整个项目目录,靠约定谁在什么时候动 哪一部分来避免互相覆盖。这不是使用者的问题,工具本身没有给出别的选项。
第二件是规模。几十个场景的时候一切都好,到了几百个场景、上千句台词,问题就变成了找不到东西。 一个角色改了名字,哪些地方引用了它;一张立绘删掉之后,还有没有别的场景在用;同一段逻辑重复了 五遍,改的时候会不会漏掉一处。这些在写代码的世界里属于编辑器的基本功能,在这类工具里往往没有。
第三件是工程化的管理手段。资源、本地化的键、变量、角色表这些东西需要一个统一的登记处, 否则只能靠人记。靠人记的东西会忘,团队里每个人记的也往往不一样。
第四件是逻辑的上限。这类工具能表达的通常是线性叙事加选项,再往上就没有出口了。两个具体的 例子:一个是自定义玩法,好感度、卡牌、推理环节这些需要真正写代码才做得出来的东西;另一个是 和外部系统打交道,存档云同步、成就系统、远程配置,这些要能发网络请求、能处理异步、能引第三方 库。没有代码入口,这两类需求从一开始就不在选项里。
NarraLeaf的回应
上面那些问题背后是同一件事:工具有没有把项目当成一件会长大的软件工程来对待。
项目是可读的,也是可以版本控制的
Studio的工程不是一个黑盒文件。角色表、变量注册表、本地化的键、资源元数据、故事图,每一种都是 结构化文档,每一种都有自己的差异比较和三路合并。git合并的是文本行,而一个剧本文档是结构化的, 按行合并会产生语法合法但语义错误的结果,所以每一种文档都需要自己的合并逻辑。
版本控制本身建在Epic的Lore上,一个内容寻址的后端,
所以二进制资源也能有真正的增量。远端是一台lore://地址的服务器,可以连接、上传、获取;
两边都改过的时候,同步会把冲突报告出来然后停下,不会把作者直接拖进一个解决冲突的界面。
界面上的词是刻意不照抄git的。作者交出去的东西叫版本,另一头叫服务器,Studio按时间自动记下的 那些叫检查点。用服务器而不用远端,是因为没接触过版本控制的人知道服务器是什么,而远端这个词 要先懂模型才有意义。默认的历史是一条线性的轨道,合并只做标记不展开,因为一条线性列表里如果 混进了不加标记的合并,读它的人会以为这些版本是依次发生的。需要分支的时候分支仍然在, 只是不摆在默认路径上。
版本控制是可选能力,并非所有机器上都有。Lore的原生后端不覆盖macOS Intel和Windows ARM64, 这两个平台上没有这个功能。
它接得进你已经在用的东西
核心用TypeScript,不发明新语言。类型检查、编辑器补全、安全的重构、lint、包管理器,这些东西 不需要谁专门为视觉小说再造一遍,它们本来就在。命令行工具负责脚手架和跨平台打包,所以构建可以 进CI,而不是靠某个人在自己机器上点一个按钮。一个团队要约定提交前先跑一遍检查,前提是有东西 可以跑。
界面和逻辑都留了出口
界面这边,narraleaf-react不用声明式模板去定义元素,而是提供接口直接覆写整个组件,没有命名 约定,CSS的全部能力都在开发者手里。Studio这边是一个接近原型工具的界面编辑器,配一套叫 blueprint的可视化逻辑系统:节点和连线组成一个由运行时执行的程序,每个blueprint都知道自己属于 谁,可能是整个项目,可能是某个界面、某个控件,甚至是某个控件的某一个属性,也因此知道自己能够 到什么。
逻辑这边,blueprint的节点分类里有流程控制、函数、数据、事件,不只是分支和选项。而在它之下 TypeScript一直是开着的。所以前面说的那两类事现在有了出口:自定义玩法可以写,和外部系统打交道 也可以,因为网络请求、异步和第三方库都还在原来的位置上。
扩展不用改引擎
插件在这套体系里是一等公民。一个插件就是一个带manifest的目录,声明自己需要什么、挂在哪一层。 官方仓库里每个插件独立解析依赖、独立打tag发布,并生成一份机器可读的索引,供Studio里的插件 浏览器拉取。
这样定制就不再是私有的。过去改引擎源码得到的东西只属于改它的那个人,引擎升一次级就是一次合并 冲突,现在做出来的东西可以发出去,也可以跟着升级。
三条入口
进入NarraLeaf有三条路线,选哪一条取决于你要控制到哪一层。
Studio是零代码的那条,面向创作者和团队。你在一个可视化的环境里搭故事结构、场景和界面, 版本控制、资源管理和打包分发都在里面。第一件事是写内容而不是处理应用内部结构的时候, 从这里开始最合适。
NarraLeaf Desktop面向开发者。它包含运行时、渲染器和命令行,你自己搭工程、自己写TypeScript, 掌握整个应用,包括本地开发、打包和交付流程。要做的是一个完整的桌面应用、并且需要控制到这一层 的时候,从这里开始。
narraleaf-react是最里面那一层,一个轻量的React播放器。它适合你已经有一个前端产品,想把 视觉小说这段体验嵌进去,或者想把播放器界面改成自己的样子。
这三条不是平行的三个产品,而是包含关系:Studio之下是NarraLeaf Desktop,NarraLeaf Desktop 之下是播放器。这个结构的好处是三条路线共享同一个方向,不会各自长成互不相干的东西。代价是换 路线不是换个开关,从播放器那一层往外补出一个Studio工程并不免费。选哪一条最好在开始的时候 想清楚。
现在到哪一步
Studio仍然处于早期阶段。上面讲到的能力都已经在代码里,不是路线图上的条目,但边角还在动, 接口还会变。拿它开一个真实项目之前要先接受这一点。