• [教你做小游戏] 只用几行原生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>
    
    • 1

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

    const bgmEl = document.getElementById('bgm');
    
    • 1

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

    const bgmEl = document.createElement('audio');
    bgmEl.setAttribute('loop', '');
    bgmEl.setAttribute('type', '音乐类型');
    bgmEl.setAttribute('src', '你的音乐的地址');
    document.body.appendChild(bgmEl);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    设置开始播放的时机

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    实现切换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;
        }
      });
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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,
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    其中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,
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    当某个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);
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

    2.png

    写在最后

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

  • 相关阅读:
    Rocky9-Linux上安装KVM虚拟机
    如何将jmap打进docker容器内
    保姆级银河麒麟V10高级服务器离线安装mysql5.7数据库
    您应该使用的14个最佳WordPress SEO插件和工具
    Xray联动burp进行渗透测试
    [附源码]JAVA毕业设计机房预约系统(系统+LW)
    浅谈电能质量监测装置在某半导体公司的应用
    41、Docker(容器操作)
    中科创达邹鹏程:与openEuler的结缘并非偶然,操作系统的未来离不开创新
    技术分享 | ClickHouse 冷热存储分离方案线上实践
  • 原文地址:https://blog.csdn.net/kd_2015/article/details/126844866