剧情写成 JSON,
引擎专注执行。一行代码完成挂载
RNE 是一个跑在浏览器里的剧情运行时:剧情就是 JSON 文件, 指令、条件、UI 这些能力由 JS 或 Rust 插件在运行时注册。 内核 255KB(gzip 后 50KB),没有运行时依赖。
439 个测试用例守护 · 手写安全表达式解析器 · 绝不使用 eval
适用:视觉小说·文字冒险·互动教学·世界模拟·NPC 跨周目记忆
{ "start": "s1", "nodes": { "s1": { "type": "say", "speaker": "旁白", "text": "铁门被推开,灰尘在灯影里浮沉。", "next": "s2" }, "s2": { "type": "say", "speaker": "铁颚", "text": "外头雨大。炉火还温着——进来,或者继续走你的。", "next": "s3" }, "s3": { "type": "choice", "options": [ { "text": "推门,借一夜炉火", "goto": "s4" }, { "text": "道谢,走进雨里", "goto": "s_end" } ] } } }
export default { init(ctx) { // 注册表达式:剧本里写 {{ roll(6) }} 就能掷骰 ctx.expressions.register("roll", (args) => { const n = Math.max(2, Math.floor(Number(args && args[0]) || 6)); return 1 + Math.floor(Math.random() * n); }, "掷 n 面骰子"); // 注册命令:剧情节点与 UI 都能调用 ctx.commands.register("点灯", () => { ctx.state.setFlag("flag.灯亮了", true); return { ok: true }; }); } };
引擎与剧情解耦
剧情是纯 JSON 数据,替换文件即替换内容;玩法由插件在运行时注册。两侧独立演进,互不牵制。
核心引擎 CORE ENGINE
-
安全表达式 DSL
手写词法 + 递归下降语法分析,绝不使用 eval / new Function。标识符规则支持 Unicode ——
var.金币 >= 10可以直接写。 -
存档全家桶
localStorage 多槽位 + 节点级自动存档;带信封的存档文件可导出 / 导入;逐级版本迁移、老档可读、拿错文件明确报错,多端冲突能比较出「分叉」。
-
世界模拟器
WorldLoop以固定节拍广播world:tick;剧情写一个observe节点就能被动听世界说话 —— 世界持续运转,剧情永不结束。 -
实时帧循环
固定步长逻辑(帧率无关)+ 渲染帧插值,没有订阅就不烧帧。性能看门狗实测帧耗时、自动降档带防抖,
?perf=1挂只读浮层。
扩展能力 EXTENSIBILITY
-
双形态插件体系
JS 插件(ES Module 动态
import())与 Rust WASM 插件(裸 C ABI,不用 wasm-bindgen)统一管理,注册命令、条件、效果、自定义节点与 UI 组件。 -
21 项权限 + 运行期审计
manifest 声明权限,宿主对每一次能力调用做校验;支持作用域限定(如
state:write:var.arena)。每次尝试都留痕 —— 拒绝也记。 -
确定性与回放
种子化随机(同种子必然复现)、稳定运行指纹、无头回放 CLI —— 在另一台宿主跑同一条命令比对指纹,跨宿主一致性可被验证。
-
错误隔离与熔断
单个插件崩溃 / 超时 / 连续失败自动熔断,都拖不垮主引擎与其它插件;切剧情后的「漏网定时器」能被
leakReport()点名。
工具与生态 TOOLING & ECOSYSTEM
-
给编剧的剧本编辑器
表单 / 文本 / JSON 三种模式;结构体检(断头路、无结局、条件写错)、一键试读、撤销重做、可打印剧本 HTML 导出,产物一定过引擎校验。
-
库形态 + iframe 嵌入
import { Runtime } from 'plugin-story-engine'直接用(克隆即用、免构建);或 iframe + postMessage 桥,父页不 import 引擎也能驱动剧情。 -
认领式输入 + 触屏
插件「认领」键盘与指针,引擎独占捕获层仲裁(优先级 + 作用域);触屏控件把手指翻译成相同键码,插件零改动支持移动端。
-
可观测性
状态来源追踪(
explain('var.金币')→ 谁写的、哪一步)、每插件开销归因、诊断包一键导出 —— 玩家说卡住了,让他导出,你就能远程复现。
三层架构,职责清晰
引擎按 type 分派节点处理器;插件拿不到引擎内部对象,只能通过经过权限校验的 ctx 说话。
静态权限清单是「许诺」,运行期 AuditLog 是「证据」—— 插件拿不到引擎内部对象,每一次能力调用都被宿主校验。
十一种内置节点
内置节点不可被插件覆盖;插件还能注册自己的节点类型,与内置节点平权分派。
say显示一句对白,等待玩家继续;支持 speaker、auto、delayMschoice显示选项等玩家选;选项可带条件与效果,支持限时jump显式跳转到指定节点set写入状态:var / flag / item / actor 四个命名空间if条件分支,条件即安全表达式(认中文标识符)wait等待若干毫秒;可标记为可跳过emit广播事件给所有监听者(插件 / 宿主)plugin调用插件命令,结果可渲染、可落地效果ui挂载插件注册的 UI 组件,可选等待玩家确认observe被动听世界事件,事件到达时跳转 —— 世界模拟器的核心end结束剧情,带一句结束摘要……你的插件用 ctx.nodes.register() 注册自定义节点类型
五分钟跑起来
Node.js ≥ 18、npm ≥ 9 即可;只有构建 WASM 插件才需要 Rust。
npm install git+https://gitcode.com/rincjiyu/rne.git#main
从源码跑起来
# 克隆仓库(含构建好的库产物,离线也能用) git clone https://gitcode.com/rincjiyu/rne.git cd rne && npm install npm run dev # http://localhost:5173,开箱即玩 npm test # 439 个用例全绿 npm run create-plugin -- com.example.demo # 生成能跑的「插件+剧情+测试」四件套
- 五个页面入口:引擎演示、剧本编辑器、嵌入演示、展示页…
- 推送前自动跑类型检查 + 全量测试(pre-push 钩子)
当库引入(免构建)
# dist-lib 随仓库提交:克隆即用,无需构建、离线也行 npm install git+https://gitcode.com/rincjiyu/rne.git#main # 或打成 tarball 离线分发 npm run prepack && npm pack # → plugin-story-engine-1.5.0.tgz // ── 然后 ────────────────────────────── import { mount } from 'plugin-story-engine' const app = mount('#app', { story: '/stories/demo.json' })
- ESM 产物 145 KB(gzip 约 50 KB)+ 43 份类型声明
- 构建产物已在 node 里实测跑通整条剧情链路
无需编程,
也能创作剧情
编辑器把引擎概念翻译成编剧的词汇:场、对白、选择、条件分支、跳转、结局。写坏了也不会丢东西 —— 解析不通过只报错(带行号),不动你写的内容。
-
一
结构体检当场点名
断头路、没人进的场、一个结局都没有、选项点了莫名结束、条件写错 —— 顶栏一行直接报出来。
-
二
一键试读
复用引擎本身试读全剧,还能「从这一场开始」;改编完马上能看到玩家眼中的样子。
-
三
两种交付稿
可打印剧本 HTML(A4、人物名居中、对白缩进 → Ctrl+P 存 PDF 给导演)与增强版 Markdown(进仓库、能 diff)。
-
四
三种模式看同一份剧情
表单 / 文本 / JSON 能看能改引擎真正加载的那份;含插件的剧情往返保真,工程节点原样保留。
示例故事
内置示例剧情风格完全不同 —— 它们的作用不是好看,而是证明引擎抽象。全部在 node 里被确定性跑通。
未寄出的信
纯对白与分歧:三封信、四个结局,只用内置节点。文本插值 {{ var.readCount }} 直接写在台词里。
潮池
剧情不主动推进 —— 插件在 world:tick 上演化生态,潮池自己长起来或塌掉,剧情用 observe 被动接住。
轨道
固定步长物理 + 输入认领 + 受管画布。引擎侧复杂(物理/帧/输入/画布),玩家侧只有两个键。
归途
结束时告诉你「你没走的路」与「因为你」;NPC 跨周目记得你。两个插件都不需要引擎改任何东西。
两种开场
同一场两个版本按玩家分流(一个玩家只分一次、可复现)—— 用来测「哪一版读者走得更远」。
世界模拟器演示
WASM 模拟物种、JS 弹警示、JSON observe 转向 —— 三种形态的插件在同一个世界里平权协作。
在线体验
不装环境,不注册。页面里跑的就是引擎本体:255KB 的 rne.js。
文档中心
与仓库 docs/*.md 逐字同步(CI 自动构建,新文档自动收录),读完即可上手;文档与代码的一致性由 23 个检查器用例兜底。
总览 · 阅读路线按目的选择入口:写剧情、写插件、嵌入 UI、部署上线剧情格式剧情 JSON、11 种节点、表达式 DSL、四个状态命名空间文本剧本语法给编剧的写作格式:幕、对白、选择、条件分支(编辑器同源)JS 插件开发manifest、ctx 全部 21 个能力、生命周期、状态读写竞态WASM 插件开发Rust + wasm32 裸 C ABI:导出、宿主 imports、内存约定权限模型21 项细粒度权限:按能力授予,越权即被拦截架构总览四层结构、插件生命周期、错误隔离三道防线更新日志每个版本的能力、修复与破坏性变更FAQ按现象排查:加载失败、剧情不更新、observe 不触发、错误码
其余 6 篇(事件总线 · WASM ABI · 跨域嵌入 · 前端集成 · 构建 · 测试)在文档总览;全部源文件在 GitCode docs/。元规则:src/ 下每个 .ts 都必须在文档里被提到 —— 由文档一致性检查器强制执行,文档不会烂在实现后面。
现状与限制
先说清楚这个引擎不擅长什么、边界在哪里 —— 合脚,再上路。
- 没有可视化节点编辑器 —— 剧情目前就是手写 JSON,工作台的编辑器只做格式校验和试读,不拖拽。
- 节点图只支持内置示例 —— 还不能渲染任意剧情的分支结构。
- WASM 插件文档比 JS 侧薄 —— 构建工具链要自己搭,示例少。
- 移动端能玩,但没做专属交互 —— 没有虚拟按键,触屏只是能用。
- 存档格式还在演进 —— 大版本升级可能要求跑一次迁移。
几个高频问题
接入前被问得最多的几个;更细的排错(错误码、加载失败、observe 不触发)在文档 FAQ。
一定要写插件才能用吗?
minimal.json 只用内置节点(say / choice / jump / set / if / wait / emit / plugin / ui / observe / end)。什么时候想要新玩法,什么时候再装插件。表达式安全吗?支持中文吗?
var.单价 * var.数量、flag.门开了 都能直接写 —— 这是面向中文的引擎应有的样子。存档会不会丢?
为什么 WASM 插件只支持 Rust?
wasm32-unknown-unknown,直接走 C ABI(alloc / dealloc),不使用 wasm-bindgen —— 依赖面更小、ABI 更可测(UTF-8 JSON 跨边界通信)。精确约定见 docs/ABI.md,宿主能力(wasm / simd / 线程)可用 ctx.capabilities 显式探测降级。怎么接入我自己的 UI / 项目?
mount() 一行挂载用默认界面;② 实现 RuntimeUi 接口 + new Runtime({ ui })(见 docs/FRONTEND.md);③ iframe + postMessage 嵌入,父页完全不 import 引擎。安装走 git 直装或 tarball:npm install git+https://gitcode.com/rincjiyu/rne.git#main。能部署到哪里?要注意什么?
开源,且活得认真
代码、剧本、测试、文档全部公开在 GitCode;问题与建议走 Issues,48 小时内必有回应。