几句熟悉的台词能让小蛇热闹起来,但吃东西、开技能和敌人出招挤在一起时,也很容易变成谁都听不清。

给声音安排一个入口

项目里用 VoiceDirector 管理同一个 audio 元素。游戏事件提出“想播哪一句”,管理器再决定现在播放、排队,还是忽略。音效则交给另一条通道,人声和音效可以分别关闭。

排队的台词也会过时

队列最多保留两项,按优先级排序;正在播放和已经排队的同一句台词不会重复加入。每项还带一个过期时间,轮到它时如果已经太晚,就直接丢掉。躲过一刀的反应,没必要隔了好几秒才突然补播。

next() {
  if (!this.enabled || this.held) return;
  const item = this.queue.find(item => item.expires >= this.now());
  this.queue = this.queue.filter(candidate =>
    candidate !== item && candidate.expires >= this.now()
  );
  if (item) this.start(item);
  else delete this.player.dataset.clip;
}

一次旧的播放失败,别打断下一句

audio.play() 返回 Promise。切换台词后,前一次播放的失败结果仍可能回来。项目用一个递增的 generation 标记区分播放请求:过期请求的报错不再处理,避免它把已经开始的新台词停掉。

暂停和静音,各自做什么?

暂停游戏时保留当前台词的位置,但清掉尚未播放的队列;继续后再尝试接着播放。关闭人声则清空播放状态和队列。这样不会出现重新开启声音后,把很久以前的事件重新念一遍。

浏览器还要求播放经过用户交互,因此人声虽然默认开启,真正发声仍从点击“开吃”等操作之后开始。播放失败时,页面会给出可操作的提示。

素材来源也一起记下来

游戏代码开源,并不意味着原声素材的权利也随之改变。仓库里的 NOTICE.md 单独记录音频等第三方素材说明,游戏帮助里也保留了对应出处。介绍视频和私人视频工程没有放进公开仓库。