乐趣区

关于前端:教你做小游戏-只用几行原生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 噢~我有空了会分享做游戏的相干技术。

退出移动版