NarraLeaf React背后的设计理念
NarraLeaf-React是我从零设计并实现的视觉小说框架。这篇博客会简单讲述两点其设计理念。
NarraLeaf-React是一个基于React的轻量级视觉小说播放器框架。关于完整的桌面视觉小说应用引擎"NarraLeaf Project",请参阅Narraleaf
NarraLeaf-React在设计之初就考虑了现有视觉小说引擎的痛点与需求缺口。我注意到大多数视觉小说引擎都有一些问题: 需要学习新的语言、难以维护和协作,以及较低的可定制性
就Ren'Py举例,内嵌Python语法和语法糖为叙事铺平了道路,允许开发者兼顾可读性和可扩展性。
但这也带来了相当多的问题:全局引用污染、编译前类型不严格以及一定的语言学习成本。
同时,Ren'Py的界面使用声明式语法来单独定义每个元素。这可能导致代码难以阅读,产生大量预期之外的布局特性并且可定制性弱。
而NarraLeaf-React从这些痛点出发,尽力构造了一个真正面向开发者的框架。
为什么使用TypeScript
TypeScript的优势显而易见。使用TypeScript,我们可以获得JavaScript面向对象设计和模块化设计的全部优势。这些特性使得代码更好管理和协作,成员访问权限明确,并且语言特性和包管理器为其带来极端高的逻辑自由。
TypeScript严格的类型检查也杜绝了大部分因为数据操作失误或意外忽略导致的类型错误。
相较于独立的DSL语言,TypeScript(NodeJS)强大的社区生态和包管理器为应用带来更强的功能性支持,而这是大部分视觉小说DSL不具备的。
在NarraLeaf-React中,除了与框架交互的接口,剩下的逻辑完全由应用控制。这意味着应用可以自由使用其他库和创造自定义逻辑,而NarraLeaf-React只负责表演的部分。
使用JSX
NarraLeaf-React的React部分是使其不同于其他视觉小说引擎的重要部分。通过与React集成,开发者可以直接使用JSX语法来定义界面,自定义样式和掌控整个应用。
而使用例如Tailwind CSS这样的样式工具库,定义界面变得轻松且易于更改。
想要更复杂的视觉效果吗?CSS的全部潜力都在开发者手里。基础的效果包括3D效果,复杂渐变和CSS支持的全部样式字段。
而JSX可以帮助开发者创建可重用的强大组件,横跨应用的同时保持样式一致性和可维护性。
在NarraLeaf-React中,和使用DSL来声明元素不同,NarraLeaf-React提供了接口来直接覆写整个组件,并且没有命名约定。
import { Dialog, Texts, Nametag, useGame } from "narraleaf-react";
function GameDialog() {
return (
<Dialog className="bg-white">
<Nametag className="font-bold" />
<Texts className="text-lg" />
</Dialog>
);
}
随后通过代码接口和钩子就可以轻松地应用组件样式。
同时,HTML结构下,布局和定位都将遵守Web成熟的标准。这意味着意外定位和难以理解的声明式嵌套结构将不复存在。
叙事接口
NarraLeaf-React最引以为傲的地方就是它的叙事接口了。经过了数个版本迭代,NarraLeaf-React构建故事的方式变得越来越直观和方便。
scene.action([
scene.background.char("/bg_park.png", new Dissolve(500)),
"阳光温柔地透过树叶洒落。",
"已经早上9点了,不知为何我今天醒来感觉比平时更精神。",
Emma.show({ duration: 500 })
.char("emma_happy.png"),
E`早上好,Alex!`,
"一个女孩站在我身边,一如既往地开朗。",
"我不知道她是怎么做到的,但Emma似乎总是心情很好。",
A`Emma,我告诉过你多少次${b("不要", "#ff0000")}不打招呼就偷偷进我的房间?`,
E`你门没关——我怎么能忍住不来呢?`,
Menu.prompt("我该怎么办?")
.choose("想去吃早餐吗?", [
E`真的吗?你终于邀请我出去了?那我们走吧!`,
])
.choose("太早了。我们在这里休息一下吧。", [
E`唉,你早上总是这么无聊。`,
E`不过好吧,我们坐会儿。其实我带了点东西想给你看。`,
]),
]);
也许这第一眼看过去有些抽象,这是因为接口简化后依旧会受到TypeScript语言限制。
经过抽象的元素接口使用链式调用以省略重复的名字。而标签函数则允许编写者在常规文本中轻松嵌入插值。
而NarraLeaf-React也提供了接口来直接在游戏过程中中执行TypeScript代码。
NarraLeaf-React完整的代码支持使得其拥有制作RPG等技术要求更高的游戏。
协作
TypeScript将NarraLeaf-React与其他引擎划分的一个重要特性就是其具有真正的商业项目潜力。 通过CI/CD工具链、Lint工具和真实生态系统集成,NarraLeaf-React项目(或带有该引擎的前端项目)能够遵守严格的组织级开发规范。
序列化与游戏流程
由于完全依赖于JavaScript和浏览器环境,NarraLeaf-React的游戏流程系统受到了极大的限制。尽管如此,它还是尽可能简化了这一过程。
操作与节点树
NarraLeaf-React通过代码接口来建立静态的节点树,然后在进入游戏后执行节点树。而树分叉则是逻辑判断和游戏选项。
这样做的好处是框架可以在不读取脚本文件的情况下静态分析脚本。
不过这意味着代码无法直接使用同步脚本来声明所有游戏过程中才会执行到的步骤,而是需要回调包装。例如,使用Script
实例来创建自定义代码操作。
不过框架也提供了一些快捷操作,可以以近似同步的语法来创建异步执行的条件语句,例如:
scene.action([
persis.set("flag", true),
Condition
.If(persis.isTrue("flag"), [
character1.say("该标志为真")
])
]);
而节点树的节点,则是动作(Action
)。大部分的NarraLeaf-React元素都通过接口创建动作。例如,char.say(something)
会返回一个链式动作。
链式动作上可以继续调用其他方法以返回串联的更多动作。
在执行阶段,链式动作会被还原为同样顺序的多个动作。
StackModel与Timeline模型
NarraLeaf-React在最初的设计中使用一个单一的引用来指向当前正在执行的节点。而后,随着需求扩张,这个方法很快就过时了。
游戏中,当执行嵌套的操作时,当前节点的引用会指向嵌套内的节点。但在此时进行序列化,则会丢失嵌套外的操作。
因此,NarraLeaf-React使用Timeline
作为操作原子包装器,以及StackModel
作为时间线管理器。这个组合允许游戏从任何正在执行的步骤中序列化和反序列化。
详细来讲,Timeline
是一组按顺序执行的步骤或子时间线,在所有操作完成后解析。StackModel
包含了多个Timeline
或子栈模型。
StackModel
可以是all
模式,即等待全部执行,也可以是any
模型,即等待任意元素完成。在序列化时,游戏会从根栈模型向下请求序列化。
栈模型会返回序列化的Action
标识符或已序列化的StackModel
。如果当前操作是异步操作并且无法从中断操作中完全恢复,StackModel
会抛弃这个步骤,
转而回滚到上一个步骤并且储存当前异步操作的标识符。
在反序列化时,所有的StackModel
都会反序列化,每一个元素都会通过标识符还原到元素实例(这通过扫描节点树完成),并且从最后一个元素标识符开始执行。
游离的StackModel
,也就是不处于任何一个根栈模型的子级的栈模型,会被单独序列化和反序列化。
这些机制允许NarraLeaf-React摆脱Promise的限制,建立一个强大且统一的游戏流程模型。
有关游戏流程的详细解析,请访问DeepWiki
未来方向
为了降低开发门槛和提供更完整的编辑支持,NarraLeaf引擎正在全力开发中。NarraLeaf是完整的桌面视觉小说应用解决方案,通过跨平台桌面技术来帮助简化游戏分发流程。
同时,NarraLeaf-React会不断优化模板系统和插件系统,以允许社区对播放器进行更自由的定制。
包含在NarraLeaf Project中并且正在规划的项目包括:
- NarraLang
- NarraUI
- NarraLeaf Editor
- NarraLang VSCode Extension
总结
NarraLeaf-React通过类型安全、IDE 支持和与 CI/CD 流程的无缝集成,为更智能的开发提供动力。其完全可定制的界面和组件化结构允许开发者就像构建现代化Web应用一样创建视觉小说。
而其可扩展性和无语言依赖性允许开发者使用强大的逻辑和真实生态系统。
有关该框架的更多信息,请访问react.narraleaf.com