# cocos2 实现全局音量管理

cocos2 实现全局音量管理

之前做了一个cocos2的项目,项目有一个需求,就是系统里面可能会播放音频或者是视频,现在想在系统邮箱上设置一个音量控制的按钮,点击按钮弹出一个slider滑块,通过拖拽滑块实时设置播放音视频的声音大小。

需求

cocos2 项目,项目中可能播放音视频素材,右上角有一个音量控制滑块,拖拽滑块的时候,需要实时调节正在播放的音视频音量大小。

思考

首先,右上角有一个滑块可以拖动,如果存在多个场景,都需要这个滑块的话,这个滑块可以在每个场景都制作一份,缺点就是麻烦;另一个是使用常驻节点,将滑块作为常驻节点,那个这个滑块在哪个场景下都是可以保存而不被销毁的。关于滑块做不做常驻节点,不是这篇博文的重点,所以不说了,主要说一下全局音量控制。

在这里插入图片描述

关于音量控制这部分,因为系统存在多个场景,每个场景都存在音视频播放的情况,所以说需要将音量大小变量volume保存成全局的,并且通过全局的方法进行控制。

在这里要使用单例模式,不能说我实例化了音量控制的实体之后,在另一个场景在实例一份,他成了一个新的,那不行,要通过单例模式,全局维护一套,确保一个类只有一个实例,并提供一个全局访问点。

再来思考一下这个全局的音量控制怎么做。

首先是cocos的滑块,注意一点,我们使用cocos的Slider滑块的时候,在web端测试可能一切顺利,但是打包发布之后,发现报错,为什么呢?原因很简单,你需要先确认一下 项目 - 项目设置 - 模块设置 - Silder 有没有勾选上,百分之八十的人都是忘记够悬最后打包编译发布上线后,发现滑块用不了。

在这里插入图片描述

在这里插入图片描述

再一个,cocos的滑块范围是 0 - 1 的取值,cocos 播放器音频控制也是 0 - 1 的取值,很好,可以对应起来,不需要多余的处理。

怎么梳理全局音量控制的逻辑呢?

比如我现在正在播放一个音频,我需要拿到这个音频的播放器保存下来,当调节音量滑块的时候,我需要通过方法把这个音频播放器的音量调至与滑块一致。所以说我们在全局音量管理文件里面需要创建一个变量存储音频播放器

在比如,有没有可能我会两个音频或者多个音频同时播放?如果需要的话,那这个存储音频播放器的变量就需要是一个数组。

还得有一个参数volume记录一下当前的音量,比如默认是 1 ,最大声音。而这个volume,需要与滑块当前位置进行绑定,滑块滑动volume需要实时变化,滑块初始化位置在哪里取决于volume的大小

然后我们在播放音频的时候,如果是使用了AudioSource组件,需要把这个组件push到列表mediaElements保存起来,如果使用了VideoPlayer,那么也需要把VideoPlayer组件push到列表mediaElements保存起来,这两个播放器是一类;

但是如果使用的是动态的cc.audioEngine.play() 创建的音频,那个我们把返回的音频ID audioId 单独存储到另一个列表audioIds里面,因为他和其他的播放器处理方式不同,不能混了。

我们存储了这个播放器和音频ID备用。

接下来考虑一下需要哪些方法,

首先需要设置全局音量 setVolume 方法,当滑块滑动的时候,其实要不同的调用这个方法进行同步音量。

然后需要获取全局音量getVolume方法,当滑块初始化的时候,需要看看全局音量现在是多少,然后滑块初始化到当前音量的位置。还有一个就是我开始播放音频或者是视频的时候,我需要设置一下播放的音视频以这个音量大小播放。

再一个方法是注册媒体元素的方法registerMediaElement,简单可以理解成我播放音频了,那么我需要把这个播放音频或者视频的播放器push到全局列表存起来,当全局设置音量的时候,遍历这个列表,把列表里面播放器的音量全部调成新的音量大小。

然后需要一个注册音频ID的方法registerAudioId,和上面一样,只不过动态cc.audioEngine.play() 创建的音频需要单独处理一下。

再一个很重要的就是删除媒体元素的方法unregisterMediaElement,之前我们通过registerMediaElement方法向列表里面添加媒体元素,但是不停的媒体元素列表会越来越多,添加太多小心内存溢出导致程序崩溃,所有我们需要再适当的时间,将用完的媒体删除掉,比如音频播放完成之后可以根据媒体元素删除,或者是返回首页之后统一全部删除之类的。

再来一个全部删除媒体元素的方法unregisterAllMediaElement,调用之后直接清空两个列表。

再来一个方法loadVolumeFromStorage吧,将当前的音量存储到本地,免得每次启动程序的话,之前的音量调的是多少找不到了。

实现

全局音量管理ts文件

上面思考的差不多了,就是实现这几个方法了,记住要单例模式,不然的话,白玩!

首先创建一个文件 AudioManager.ts

在这里插入图片描述

里面的代码我直接粘贴了,我觉得不是很难,不详细说了就:

var AudioManager = (function () {
  var instance;
  var volume = 1.0;
  var mediaElements = [];
  var audioIds = [];

  function init() {
    function updateAllVolumes() {
      // 更新已注册的音频 ID 音量
      audioIds.forEach(function (audioId) {
        if (audioId !== null) {
          cc.audioEngine.setVolume(audioId, volume);
        }
      });

      mediaElements.forEach(function (element) {
        if (!element) return;
        // 支持 cc.VideoPlayer
        if (element instanceof cc.VideoPlayer) {
          if (element.volume) {
            element.volume = volume;
          }
        }
        // 支持 cc.AudioSource
        else if (element instanceof cc.AudioSource) {
          element.volume = volume;
        }
        // 处理Cocos音频引擎的音频
        else if (element.setVolume) {
          element.setVolume(volume);
        }
        // 处理HTML5 Video元素
        else if (element.getComponent && element.getComponent(cc.VideoPlayer)) {
          var videoComp = element.getComponent(cc.VideoPlayer);
          if (videoComp.nativeVideoObj) {
            videoComp.nativeVideoObj.volume = volume;
          }
        }
        // 处理原生HTML5媒体元素
        else if (element.volume !== undefined) {
          element.volume = volume;
        }
      });
    }

    return {
      // 设置全局音量
      setVolume: function (newVolume) {
        volume = Math.max(0.0, Math.min(1.0, newVolume));
        updateAllVolumes();
        cc.sys.localStorage.setItem('audioVolume', volume.toString());
      },
      // 获取全局音量
      getVolume: function () {
        return volume;
      },
      // 注册媒体元素
      registerMediaElement: function (element) {
        if (element && mediaElements.indexOf(element) === -1) {
          mediaElements.push(element);
          // 注册时立即设置当前音量
          if (element.setVolume) {
            element.setVolume(volume);
          } else if (element.getComponent && element.getComponent(cc.VideoPlayer)) {
            var videoComp = element.getComponent(cc.VideoPlayer);
            if (videoComp.nativeVideoObj) {
              videoComp.nativeVideoObj.volume = volume;
            }
          } else if (element.volume !== undefined) {
            element.volume = volume;
          }
        }
      },


      // 新增:注册音频 ID
      registerAudioId: function (audioId) {
        if (audioId !== null && audioIds.indexOf(audioId) === -1) {
          audioIds.push(audioId);
          cc.audioEngine.setVolume(audioId, volume);
        }
      },

      // 删除:取消注册音频 ID
      unregisterMediaElement: function (element) {
        var index = mediaElements.indexOf(element);
        if (index !== -1) {
          mediaElements.splice(index, 1);
        }
      },

      // 删除:取消所有注册音频
      unregisterAllMediaElement: function (element) {
        mediaElements = [];
        audioIds = [];
      },

      // 从本地存储加载音量
      loadVolumeFromStorage: function () {
        var savedVolume = cc.sys.localStorage.getItem('audioVolume');
        if (savedVolume !== null) {
          this.setVolume(parseFloat(savedVolume));
        }
      }
    };
  }

  return {
    // 获取 AudioManager 实例
    getInstance: function () {
      if (!instance) {
        instance = init();
        instance.loadVolumeFromStorage();
      }
      return instance;
    }
  };
})();

export default AudioManager;

滑块逻辑

滑块逻辑就很简单了,无非就是监听一下滑块滑动事件:

在这里插入图片描述

代码也很简单:

const { ccclass, property } = cc._decorator;
import SceneBase from "../SceneBase";
import AudioManager from "./AudioManager";

@ccclass
export default class VolumeSlider extends SceneBase {

  // 滑块节点
  audioManager = null;

  // 初始化
  onLoad() {
    this.audioManager = AudioManager.getInstance(); // 实例化音频管理器
    this.node.getComponent(cc.Slider).progress = this.audioManager.getVolume(); // 获取当前管理器中的音量并赋值给滑块
  }

  // 滑块滑动回调
  onSliderChanged() {
    let progressVal = this.node.getComponent(cc.Slider).progress // 获取当前滑块进度
    this.audioManager.setVolume(progressVal); // 设置音量
  }
}

音视频播放改造

音频改造需要麻烦一些,因为所有音视频播放的地方都需要改,下面音频、视频、动态添加的音频都写一个案例看一下。

当然都需要引入我们全局的文件哈,我用的相对路径,到时候替换成自己的

import AudioManager from "./AudioManager/AudioManager";

音频 AudioSource

如果使用的是 AudioSource 组件,那么是下面这么改造:

	// 获取音频播放器
	let audio = this.node.getComponent(cc.AudioSource);
	// 获取一下当前的音量大小
    const globalVolume = AudioManager.getInstance().getVolume();
    // 播放器设置成当前音量播放
    audio.volume = globalVolume;
    // 把播放器添加到全局列表进行管理维护
    AudioManager.getInstance().registerMediaElement(audio);
    // 播放音频
    audio.play();

视频 videoPlayer

如果使用的是 videoPlayer 组件,那么是下面这么改造,简单写的哈,关键代码都在:

		// 设置一下视频播放开始时间,从头开始
		this.videoPlayer.getComponent(cc.VideoPlayer).currentTime = 0;
		// 获取一下当前的全局音量
		const globalVolume = AudioManager.getInstance().getVolume();
		// 设置视频播放的音量大小为全局音量大小
		this.videoPlayer.volume = globalVolume;
		// 把播放器添加到全局列表进行管理维护
		AudioManager.getInstance().registerMediaElement(this.videoPlayer);
		// 播放视频
		this.videoPlayer.play();

动态音频 cc.audioEngine.play()

如果使用的是 cc.audioEngine.play() ,那么是下面这么改造:

	// 读取音频素材
	cc.resources.load("deepSeekAudio/111.mp4", cc.AudioClip, (err, clip) => {
      if (err) {
        cc.error(err.message || err);
        return;
      }
      // 获取全局音量大小
      const globalVolume = AudioManager.getInstance().getVolume();
      // 创建动态音频,返回音频ID
      this.audioId = cc.audioEngine.play(clip, false, globalVolume);
      // 添加音频ID到全局列表
      AudioManager.getInstance().registerAudioId(this.audioId);
    });

OK,上面的就完事了。

接下来别忘了,在合适的地方从全局列表里面把播放器和音频ID注销掉!

可以通过 AudioManager.getInstance().unregisterMediaElement(ele) 方法在音频播放完成的时候一个一个删除,也可以通过AudioManager.getInstance().unregisterAllMediaElement();在场景销毁或者新加载场景的时候全部删除。

完成了,主要就是这些内容,完事,拜了个拜!

posted @ 2025-06-25 16:33  叫我+V  阅读(28)  评论(0)    收藏  举报