写点什么

[教你做小游戏] 只用几行原生 JS,写一个函数,播放音效、播放 BGM、切换 BGM

作者:HullQin
  • 2022 年 9 月 03 日
    广东
  • 本文字数:3241 字

    阅读完需:约 11 分钟

[教你做小游戏] 只用几行原生JS,写一个函数,播放音效、播放BGM、切换BGM

我是 HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,发送加微信,交个朋友),转发本文前需获得作者 HullQin 授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费没广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加 Game Jam 2022。喜欢可以关注我 HullQin 噢~我有空了会分享做游戏的相关技术。

问题描述

要做小游戏,播放音效、BGM 是必须的。如何实现呢?


首先我们区分 2 个概念:背景音乐(Background Music 简称 BGM)和音效(Sound Effect 简称 SE)。


背景音乐是需要循环播放的,是很长的音乐,可能中途有暂停、切歌的诉求。同一时间一般只有 1 首 BGM 在播放。


音效是在需要时单次播放,比较短的声音,一般随着动画、用户操作一同触发。同一时间可能叠加很多个 SE。播放完,就结束了。


所以,二者诉求不同,我们最好分别实现。

前提知识

浏览器如何播放声音

目前,前端可以通过audio这个标签,来播放声音,介绍几个重要的属性:


  • src:声音资源的 URL。

  • type:声音资源的类型,会用该方式解码。例如.mp3 应该用audio/mpeg,而.ogg 则用audio/ogg,而.wav 是audio/wav

  • loop:是否循环播放,若有该属性(不需要赋值),则表示循环播放。否则播放一次后就结束了。


此外,audio 对应的 element 还有属性是volume,可以通过 JS 设置和修改,0 表示没声音,1 表示 100%,即音乐真实音量。

浏览器播放声音的限制

浏览器有个限制:只有用户跟网页发生了交互(按键盘、鼠标都算交互),才允许播放声音。所以当你打开视频网站时、或者打开某个直播间时,网页上往往会提示「点此取消静音」,其实是网页开发者对该限制做的妥协,也是相关协议制定者期望的表现。


如果你在用户发生交互前,调用 APIaudio.play()播放了音乐,会有报错:


Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first. https://goo.gl/xX8pDD

播放 BGM

定义 audio 标签

因为全局同时只有 1 个 BGM 在播放,我们可以在 html 文件中定义这个 BGM 的 audio 标签:


<audio id="bgm" loop src="你的音乐的地址" type="音乐类型"></audio>
复制代码


之后可以获取这个 dom 节点:


const bgmEl = document.getElementById('bgm');
复制代码


当然,你也可以用 JS 生成这个 html:


const bgmEl = document.createElement('audio');bgmEl.setAttribute('loop', '');bgmEl.setAttribute('type', '音乐类型');bgmEl.setAttribute('src', '你的音乐的地址');document.body.appendChild(bgmEl);
复制代码

设置开始播放的时机

let bgmStarted = false;const startPlayBGM = () => {  if (bgmStarted) return;  bgmStarted = true;  bgmEl.play();  document.body.removeEventListener('click', startPlayBGM);  window.removeEventListener('keydown', startPlayBGM);};document.body.addEventListener('click', startPlayBGM);window.addEventListener('keydown', startPlayBGM);
复制代码


可以看到,我们监听了鼠标事件和键盘事件,只要用户发生了交互,就可以开始播放了~

实现切换 BGM

我在《我们用 48h,合作创造了一款 Web 游戏:Dice Crush,参加国际赛事》游戏中,做了这种效果:


用户主动切换游戏速度时(Slow、Normal、Fast),BGM 也会随着切换。是点击时立马切换的。此外,为了避免每次切换后,BGM 都从头开始,让玩家听腻。所以我直接设置了 3 个audio标签,每个 audio 标签各自循环播放 1 首 BGM(一共 3 首)。那么切换 BGM 函数只需要做这件事:设置其它 2 个audio音量=0,要播放的 BGM 的audio音量=1。这就保证了每次切换,都是对应歌曲的不同播放位置,让玩家没有厌烦感。



let current = 0;const changeBGM = (num) => {  if (!startPlayBGM) {    current = num;    return;  }  // 注:audios是3个audio结点组成的数组。  audios.forEach((audio, index) => {    if (num === index) {      audio.volume = 1;      audio.play(); // 可有可无。根据你希望达成的效果,可删掉或留着。    } else {      audio.volume = 0;    }  });};
复制代码


changeBGM函数有个小细节:如果当前还没发生交互,那么会把当前的音乐编号存到current变量。当然startPlayBGM函数也有一些变化:初始化时,所有 audio 的 volume 都是 0,用户发生交互后,把current对应的 BGM 的 volume 设置为 1,并且调用它的audio.play()。你思考下,为什么这么实现?


因为可能有时候 changeBGM 调用时,还没发生交互。需要把当前的 BGM 存下来。然后发生交互时,播放current即可。

播放音效

定义音效常量

因为音效很多,文件比较多,建议用一个配置文件,定义项目中所有的音效:


例如:


const SE = {  Drop: {    path: 'audio/se/Shot1.ogg',    type: 'audio/ogg',    duration: 1500,    volume: 0.75,  },  Roll: {    path: 'audio/se/roll.wav',    type: 'audio/wav',    duration: 1500,    volume: 0.75,  },  Crush: {    path: 'audio/se/crush.wav',    type: 'audio/wav',    duration: 1500,    volume: 0.75,  },  Lose: {    path: 'audio/se/lose.mp3',    type: 'audio/mpeg',    duration: 5500,    volume: 1,  },};
复制代码


其中 SE 对象的 key 是音效的名字,值中 path 会赋值给src。duration 表示这个 SE 的时长(毫秒),建议大于等于音效的时长,但不要太大。

定义播放音效的容器

因为音效可能会并发,我们提前定义 16 个audio标签,最多可支持 16 个音效同时播放。而这些audio是允许重复利用的。


const seList = [];
for (let i = 0; i < 16; i++) { const audioElement = document.createElement('audio'); document.body.appendChild(audioElement); seList.push({ dom: audioElement, finishTime: 0, });}
复制代码


当某个 SE 播放开始过了duration毫秒后,表明这个audio任务完成了,处于「闲置」状态了。


这种逻辑你会怎么实现呢?使用 16 个 setTimeout 吗?


不要频繁使用setTimeout,我们完全可以通过finishTime记录它的播放完成时间。每次播放时,计算是否空闲即可。


此外,有些动作类游戏,可能会密集的播放音效,如果太密集,我们 16 个并发的audio也无法支撑住了,所以最好加个「防抖」,将 80ms 内重复播放的音效合并,但是如果合并了,我们给音效音量加大。合并的越多,音效越响亮。


const broadcastSe = (se) => {  // 获取当前时间  const now = new Date().getTime();  // 判断是否需要防抖处理(同一个类型的音效、且播放时间差小于80ms)  const sameItem = seList.find((item) => item.dom.getAttribute('src') === se.path && Math.abs(item.finishTime - now - se.duration) < 80);  // 相同音效,就把音量加大,最大值为1,并结束函数。  if (sameItem) {    sameItem.dom.volume = Math.min(sameItem.dom.volume + 0.1, 1);    return;  }  // 不同音效,寻找空闲的audio,要求dom的finishTime小于现在时间戳,说明它是空闲的  const potentialDom = seList.find((item) => item.finishTime < now);  if (potentialDom) {    potentialDom.dom.setAttribute('src', se.path);    potentialDom.dom.setAttribute('type', se.type);    potentialDom.dom.volume = se.volume;    potentialDom.finishTime = now + se.duration;    // 这里要等src设置完毕后,加载好音效后再播放。所以注册了一个延迟执行的任务。否则,会播放旧的src资源    setTimeout(() => potentialDom.dom.play(), 0);  } else {    console.log('资源不足,无法播放', se.path);  }};
复制代码


播放多个 SE 时,效果如下:


写在最后

我是 HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,发送加微信,交个朋友),转发本文前需获得作者 HullQin 授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费没广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加 Game Jam 2022。喜欢可以关注我 HullQin 噢~我有空了会分享做游戏的相关技术。

发布于: 刚刚阅读数: 4
用户头像

HullQin

关注

公众号【线下聚会游戏】 2020.10.07 加入

game.hullqin.cn 我做了一些联机桌游网页:支持2-10人联机的UNO、2-4人联机的斗地主、2人联机的五子棋。无需下载,点开即玩!叫上朋友,即刻开局!不看广告,不做任务,享受「纯粹」的游戏!

评论

发布
暂无评论
[教你做小游戏] 只用几行原生JS,写一个函数,播放音效、播放BGM、切换BGM_CSS_HullQin_InfoQ写作社区