NarraLeaf

events

订阅游戏生命周期与故事事件,并检查当前环境是否可能触发某个事件

app.game.events 让插件不必接进图里也能对运行中的游戏做出反应——预加载完成、一句台词结束、玩家做出选择、窗口被关闭

{
  "contributes": {
    "runtimeCapabilities": ["events"]
  }
}

方法

方法签名
on<K>(event: K, listener: (payload) => void) => RuntimePluginCleanup
available(event) => boolean

on 返回一个移除该订阅的清理函数。宿主同样会追踪每个监听器,所以出错的插件不会把它们泄漏掉

export default defineRuntimePlugin({
    setup(app) {
        app.game.events?.on("choiceMade", ({ text }) => {
            app.game.log("info", `player chose: ${text}`);
        });
    },
});

事件一览

事件负载说明
preloadCompletevoid第一轮预加载完成。会重放——订阅晚了也照样触发
firstSceneReadyvoid第一个场景已挂载并完成首次绘制。会重放
sceneEnter{ sceneId: string | null }一个场景组件挂载了。见下方警告
sceneExit{ sceneId: string | null }对应的卸载
dialogueEnd{ textId: string | null }一句对白显示完毕。textId 是这一行稳定的文本 id —— 与翻译表、引擎的 voiceId 用的是同一个键 —— 宿主叫不出名字的行则为 null。插件自己记“玩家听过什么”时,记的就是它
choiceMade{ text: string }玩家选了一个选项
choiceShown{ options: { index: number; text: string; disabled: boolean }[] }一个选项菜单上了屏,带着它提供的全部选项 —— choiceMade 的另一半,那个只说选走了哪一个。菜单每次挂载都会触发,所以回退到同一个选择会再报一次。index 是引擎的索引,也是选中时寻址用的;被条件藏掉的选项不在列表里且不会把它前移,所以索引可能有缺口
characterPrompt{ character: string | null; text: string }显示了一句角色台词
audioPlayed{ assetId: string }故事开始播放一个音频资产:一行 /bgm/sound,或者一个配置了背景音乐的场景在挂载时起播。报的是故事起的那一声,不是游戏发出的每一声 —— 页面通过 Play Sound 起的属于界面,不会出现在这里。与 sceneEnter 一样它跟的是执行而不是首次抵达,所以拿它做的事要幂等
gameEndvoid故事走到了结局:一行 /ending 跑了,或者动作栈在存在存档上下文的情况下清空。两者都算,是故意的 —— 作者写的结局就是游戏结束,只盯栈清空的插件会在工程开始标结局的那一天开始什么都听不到
endingReached{ endingId: string; name: string }一行 /ending 跑了,并说出它声明的结局。能告诉你是哪个结局的就是它
beforeRestorevoid即将恢复一个存档
afterRestorevoid存档已恢复
fullscreenChangedboolean新的全屏状态
closeRequestedvoid玩家请求关闭窗口。仅桌面
saveWritten{ id: string }写入了一个存档槽位

sceneEnter / sceneExit渲染事件,不是“故事走到了这个场景”。一次重新挂载会为玩家根本没有再次进入的场景再触发一遍 sceneEnter。如果你要的是故事进度,请从故事本身(一个蓝图节点)驱动,而不是从挂载驱动

检查可用性,不要假定有外壳

有些事件在某些环境里根本不可能存在。closeRequested 就是最典型的一个:Web 导出没有窗口可关,所以在那里注册的监听器永远不会触发——这种 bug 只会在一个目标上暴露出来

if (app.game.events?.available("closeRequested")) {
    app.game.events.on("closeRequested", () => { /* 冲刷点什么 */ });
} else {
    // web:改为在 `dialogueEnd` 时冲刷,或者按你自己的节奏来
}

closeRequested 的监听器是观察者,不是否决权。插件会被并行询问,且彼此隔离;从你的监听器里抛错会被读作“无异议”,而不是取消关闭的请求。插件做什么都无法让窗口留下来

重放与实时

preloadCompletefirstSceneReady 会重放:如果插件在它们已经发生之后才订阅,监听器会立即触发。其余所有事件都只是实时的——事后注册的监听器听不到任何过去的事

这一点很重要,因为 setup 可以是异步的。一个先 await 再订阅的插件会错过这期间触发的实时事件,但仍然能收到那两个会重放的事件

本页目录