动作如何运行
从场景显示一个动作的那一刻起,Vovó 会怎样对待它:它保证什么,动作又不能指望什么。写给要把动作交给别人或出售的人;怎样写动作见“动作”。
自己的页面
每个动作都是一份独立的文档,放在场景页面给它的框架里:它的 HTML,其中写入了它的 style.css 和 script.js,脚本在最后,所以脚本运行时 HTML 已经就位。一个动作出错不会拖垮其他动作。场景按自己的顺序把动作一层层叠起来,第一个在最下面。
框架不接收点击和按键:叠加层是给人看的,从来不是用来按的。操控直播的人要做的事,在控制板、动作的面板或“自定义”里做,然后作为事件传给动作。
只有在打开的页面显示着动作时,动作才会听到事件,而且只在当前使用的配置文件里。没有页面打开时,没有谁在数关注。
没有网络,浏览器里什么都不保存
动作只能访问自己的文件和 Vovó 提供给它的内容,别的都不行。图片、声音、视频和字体来自动作的文件(按名称),或来自事件给出的地址(表情、封面、在“自定义”中选的文件),绝不来自互联网。它的代码和样式只有 script.js 和 style.css:不能从文件或别处加载脚本或样式表,没有 eval,没有 worker。fetch、XMLHttpRequest 和 WebSocket 都会被拒绝,访问动作自己的文件也一样,所以它需要的数据要写在脚本里。浏览器的存储(localStorage、cookie、IndexedDB)在这里不存在:动作要保存的东西,都保存在 vovo.state 里。
它需要外部的东西时,在它声明的权限范围内向 Vovó 请求:聊天里的一行、按名称调用的 webhook、控制板上的一个声音、官方播放器里的一个剪辑。
每个打开的页面都在运行它
同一个动作会在它的场景打开的每个地方运行:OBS 里、场景预览窗口里、浏览器标签页里、同时在两个来源里。每一份都收到相同的事件,画出相同的内容。所以页面只负责绘制和播放,只该发生一次的事要向 Vovó 请求;对于在处理同一事件时提出请求的各个页面,Vovó 只做一次:聊天里的一行、一次抽奖、一段语音、一个 webhook、控制板上的一个声音、OBS 里的一个来源、动作自己的一个事件。
Vovó 靠正在处理的事件来区分不同页面的调用。在回调返回之后才进行的调用(await 之后、动画结束时、计时器里)要用 { event: e } 指明它回应的事件,也就是回调收到的那个对象;否则 Vovó 分不清这是第二个页面还是第二次。
vovo.on("raid", async (e) =>
{
const gift = await vovo.draw(["a hug", "a song", "a dance"]);
vovo.show("raid", { user: e.user, gift: gift.value });
vovo.chat.send(e.user + " gets " + gift.value + "!", { event: e });
});
页面自己发起的事背后没有事件:动作的 setInterval 在每个页面都会运行,它在加载时做的事也一样。这时 Vovó 只能看请求的内容(5 秒内相同的聊天内容、相同的语音、相同的 webhook 或自有事件算作一次),所以要计数的、或只该发生一次的事,应放在 timer 触发器里:它属于应用,所有页面共用一个。
状态,而不是重放
vovo.state 由 Vovó 保存,而不是页面。页面加载时(OBS 启动、来源被刷新、紧急结束、重新打开 Vovó),脚本运行前就已拿到全部状态;在一个页面里设置的键会立刻、悄悄地传到其他页面,因为它们自己也处理了同一个事件。事件从不重放:突袭到来时不在场的页面,之后也不会得知。所以动作要把之后还得再显示的东西都放进状态(目标的计数、抽奖的名单、倒计时何时结束),并在加载时根据状态来绘制。
function draw()
{
document.getElementById("count").textContent = vovo.state.count || 0;
}
vovo.on("follow", () =>
{
vovo.state.count = (vovo.state.count || 0) + 1;
draw();
});
draw();
- 键里的内容变了,就重新设置这个键:
vovo.state.names = names.concat([user])。在原处修改的列表或对象(push)不会被保存。 - 在回调本身里、任何
await之前修改状态:这样每个页面都从同一状态出发,得到同一结果。await之后别的页面可能已经写过了,所以要写不依赖现有内容的值(获胜者的名字、一个时间),绝不要写“在现有基础上加一”。 - 一个键最多保存 64 KB(按 JSON 计),键名最多 64 个字符。
当前值与持续读数
有四个事件既是某个时刻,也是一个值:obs.scene、media.track、twitch.viewers 和 screen.shared。动作加载时会立刻收到它所听的每一个的最新值,就像刚刚发生一样,所以不用等下一首就能显示正在播放的曲目。每次加载、每个页面都会再收到一次,背后没有事件:根据它来绘制,不要用只该发生一次的事来回应它。
audio.level、screen.color、input.move 和 speech.text 是读数,每秒最多 60 次:送达后即被遗忘,只有在打开的页面显示着监听它们的动作时才会读取。让它们的回调保持轻量,把动效交给 CSS。
加载、重新加载与紧急
页面可能在任何时刻加载,而且不止一次:OBS 在场景显示时启动来源,隐藏时可能关闭它,再次显示时可能刷新它;动作的代码或文件变了,Vovó 也会重新加载它。紧急会一次清空所有场景并重新加载其中的动作,它们正在做的事随之停止;紧急结束后,它们再加载一次,从状态重新开始。紧急期间没有任何东西会传到动作,期间到来的内容之后也不会补播;控件的值会保留,因为它是状态。
所以动作必须在任何时刻仅凭状态就能正确恢复,不指望上一个页面留下的做到一半的事。
舞台
动作在一个以 CSS 像素计的舞台上绘制,舞台短边为 1080:16:9 的直播无论分辨率都是 1920 × 1080(4K 来源会以两倍大小清晰绘制),竖屏直播是 1080 × 1920,其他形状的屏幕则是其他形状。它的文档与舞台一样大,并且是透明的,所以 100vw 乘 100vh 就是整个画面,没画的地方会露出游戏。为别人的直播制作的动作不要假定形状:从边缘出发、用百分比来摆放,竖屏直播用 @media (orientation: portrait)。
OBS 的浏览器
OBS 用自己的浏览器显示场景,比 Vovó 的编辑器和预览窗口里的更旧(OBS 32 中为 Chromium 127),而且可能在没有显卡加速的情况下运行。在依赖新的 CSS 或 JavaScript 特性之前,先对照这个版本确认;交付之前,先在 OBS 里亲自看看这个动作。在那里声音无需点击就会立刻播放,并进入直播:只给主播自己听的东西不应放进动作。
页面在 OBS 里没有控制台。动作没有捕获的错误,以及 Vovó 对其调用的说明,会以动作的名称连同行号出现在 Vovó 的控制台里;console.log 不会。要看它,请在浏览器中打开预览的地址(vovo dev 会打印出来),使用浏览器的开发者工具。
节奏与限制
- 聊天:一行 500 个字符;所有动作合计 30 秒内 20 行,每个动作每秒一行。
- Webhook:16 KB 的 JSON;每分钟 30 次,每个动作每秒一次。
- 剪辑和视频:每个动作最多 3 个等待批准。
- 摄像头:同时显示 4 个,每个动作最多 4 个摄像头设置。
- 语音通话:最长 300 秒。
- 显示为进行中的按钮:最长 15 分钟。
- 状态:每个键 64 KB。
- 一个包:200 个文件,每个 32 MB,合计 128 MB;
index.html、style.css和script.js各 2 MB。
超出节奏的调用会被丢弃,从不排队:它的 promise 会被拒绝,并说明原因。
交付之前
- 在它做各种事的中途重新加载场景:它能根据状态正确恢复。
- 把场景打开两次,OBS 里和预览窗口里,然后触发每个触发器:聊天里只有一行、只响一次、只计一次。
- 在它显示内容时开启再关闭紧急。
- 在竖屏配置文件里试一试,也在 OBS 里试一试。
- 只请求它用到的权限,并在“行为”里说明它是什么:作者、网站和一行介绍。
- 给它一个 id,每次交给别人时写上更高的版本,这样更新才能找到要替换的动作,并说明带来的是哪个版本。
- 说明你按什么许可证把它交给别人;条款是你自己写的话,附上全文。