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}`);
});
},
});事件一览
| 事件 | 负载 | 说明 |
|---|---|---|
preloadComplete | void | 第一轮预加载完成。会重放——订阅晚了也照样触发 |
firstSceneReady | void | 第一个场景已挂载并完成首次绘制。会重放 |
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 一样它跟的是执行而不是首次抵达,所以拿它做的事要幂等 |
gameEnd | void | 故事走到了结局:一行 /ending 跑了,或者动作栈在存在存档上下文的情况下清空。两者都算,是故意的 —— 作者写的结局就是游戏结束,只盯栈清空的插件会在工程开始标结局的那一天开始什么都听不到 |
endingReached | { endingId: string; name: string } | 一行 /ending 跑了,并说出它声明的结局。能告诉你是哪个结局的就是它 |
beforeRestore | void | 即将恢复一个存档 |
afterRestore | void | 存档已恢复 |
fullscreenChanged | boolean | 新的全屏状态 |
closeRequested | void | 玩家请求关闭窗口。仅桌面 |
saveWritten | { id: string } | 写入了一个存档槽位 |
sceneEnter / sceneExit 是渲染事件,不是“故事走到了这个场景”。一次重新挂载会为玩家根本没有再次进入的场景再触发一遍 sceneEnter。如果你要的是故事进度,请从故事本身(一个蓝图节点)驱动,而不是从挂载驱动
检查可用性,不要假定有外壳
有些事件在某些环境里根本不可能存在。closeRequested 就是最典型的一个:Web 导出没有窗口可关,所以在那里注册的监听器永远不会触发——这种 bug 只会在一个目标上暴露出来
if (app.game.events?.available("closeRequested")) {
app.game.events.on("closeRequested", () => { /* 冲刷点什么 */ });
} else {
// web:改为在 `dialogueEnd` 时冲刷,或者按你自己的节奏来
}closeRequested 的监听器是观察者,不是否决权。插件会被并行询问,且彼此隔离;从你的监听器里抛错会被读作“无异议”,而不是取消关闭的请求。插件做什么都无法让窗口留下来
重放与实时
preloadComplete 和 firstSceneReady 会重放:如果插件在它们已经发生之后才订阅,监听器会立即触发。其余所有事件都只是实时的——事后注册的监听器听不到任何过去的事
这一点很重要,因为 setup 可以是异步的。一个先 await 再订阅的插件会错过这期间触发的实时事件,但仍然能收到那两个会重放的事件