Vovó 帮助

动作如何运行

从场景显示一个动作的那一刻起,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();

当前值与持续读数

有四个事件既是某个时刻,也是一个值: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 会打印出来),使用浏览器的开发者工具。

节奏与限制

超出节奏的调用会被丢弃,从不排队:它的 promise 会被拒绝,并说明原因。

交付之前