CocosCreator 3.x 自定义声音模块

  • Creator 版本:3.6.1
  • 官方文档说明:

Cocos Creator 3.x 移除了 v2.x 中的 audioEngine API,统一使用 AudioSource 组件播放音频。
Cocos Creator 3.6 手册 - AudioSource 组件参考

项目升级,因官方移除了2.x的 audioEngine 接口,原来写的模块不能用了,重新基于3.x 编写了一个新的声音模块,附上代码,如有问题,可以指正出来,积极完善ing

import { assert, assetManager, AudioClip, AudioSource, find, isValid, log, Node, resources, warn } from "cc";

import Tools from "./Tools";

export enum AUDIO_LOCAL_KEY {

    MUSIC_VOLUME = "MUSIC_VOLUME",

    EFFECT_VOLUME = "EFFECT_VOLUME"

}

export class AudioManager {

    private static _instance: AudioManager = null;

    public static get Instance(): AudioManager {

        this._instance = this._instance || new AudioManager();

        return this._instance;

    }

    //声音资源的缓存

    private _cachedAudioClipMap: Record<string, AudioClip> = {};

    //音乐节点

    private m_musicPlayerNode: Node = null;

    //音乐节点组件

    private m_musicSource?: AudioSource;

    private m_soundPlayingNodes: Array<Node> = [];

    private m_soundWaitingNodes: Array<Node> = [];

    //音乐和音效的音量大小

    private m_musicVolume: number = -1;

    private m_soundVolume: number = -1;

    //音乐音效是否静音

    // private m_musicMute: boolean = false;

    // private m_soundMute: boolean = false;

    //模块初始化

    public init() {
        let musicVolume_record = Number(Tools.getLocalRecord(AUDIO_LOCAL_KEY.EFFECT_VOLUME));

        let soundVolume_record = Number(Tools.getLocalRecord(AUDIO_LOCAL_KEY.MUSIC_VOLUME));

        this.m_musicVolume = (this.m_musicVolume === -1 && musicVolume_record === 0) ? 0.2 : musicVolume_record;

        this.m_soundVolume = (this.m_soundVolume === -1 && soundVolume_record === 0) ? 0.7 : soundVolume_record;

    }

    //***********************************音乐********************************************************** */

    //获取音乐节点

    private getMusicPlayerNode(): Node {

        let musicNode: Node = find('Canvas/MusicPlayer');

        if (!musicNode) {

            musicNode = new Node('MusicPlayer');

            musicNode.addComponent(AudioSource);

            musicNode.parent = find('Canvas');

        }

        return musicNode;

    }

    /**

     * 播放音乐

     * @param {String} name  音乐名称

     * @param {Number} volumeScale 音乐声音大小

     * @param {Boolean} loop 是否循环播放

     */

    public playMusic(name: string, volumeScale: number = 1, loop: boolean = true) {

        if (this.musicVolume === 0) return;

        if (!isValid(this.m_musicPlayerNode)) {

            this.m_musicPlayerNode = this.getMusicPlayerNode();

            this.m_musicSource = this.m_musicPlayerNode.getComponent(AudioSource);

        }

        const musicAudioSource = this.m_musicSource!;

        if (musicAudioSource.clip && musicAudioSource.clip.name === name) {

            musicAudioSource.loop = loop;

            if (!musicAudioSource.playing) {

                let volume = Math.min(1, Math.max(0, this.musicVolume * volumeScale));

                musicAudioSource.volume = volume;

                musicAudioSource.play();

            }

        } else {

            let clip: AudioClip = this._cachedAudioClipMap[name];

            if (clip) {

                musicAudioSource.clip = clip;

                musicAudioSource.loop = loop;

                let volume = Math.min(1, Math.max(0, this.musicVolume * volumeScale));

                musicAudioSource.volume = volume;

                musicAudioSource.play();

            } else {

                let path = 'sounds/' + name;

                resources.load(path, AudioClip, (err: Error, audioClip: AudioClip) => {

                    if (err) {

                        warn(`load audioClip ${name} failed: `, err.message);

                        return;

                    }

                    this._cachedAudioClipMap[name] = audioClip;

                    musicAudioSource.clip = audioClip;

                    musicAudioSource.loop = loop;

                    let volume = Math.min(1, Math.max(0, this.musicVolume * volumeScale));

                    musicAudioSource.volume = volume;

                    musicAudioSource.play();

                })

            }

        }

    }

    /** 音量 */

    get musicVolume(): number { return this.m_musicVolume; }

    set musicVolume(value: number) {

        this.m_musicVolume = Math.min(1, Math.max(0, value));

        this.m_musicSource.volume = value;

    }

    public stopMusic() {

        if (this.m_musicSource.clip) {

            this.m_musicSource.stop();

        }

    }

    public pauseMusic(): void {

        if (this.m_musicSource.playing) {

            this.m_musicSource.pause();

        }

    }

    public resumeMusic(): void {

        if (this.m_musicSource.clip) {

            this.m_musicSource.play();

        }

    }

    //***********************************音效********************************************************** */

    private createSoundPlayerNode(): Node {

        let soundNode = new Node('SoundPlayer');

        soundNode.addComponent(AudioSource);

        soundNode.parent = find('Canvas');

        return soundNode;

    }

    private getAudioNode(): Node {

        let node = this.m_soundWaitingNodes.pop();

        if (!isValid(node)) {

            node = this.createSoundPlayerNode();

        }

        this.m_soundPlayingNodes.push(node);

        return node;

    }

    private putAudioNode(node: Node) {

        const idx = this.m_soundPlayingNodes.indexOf(node);

        if (idx !== -1) this.m_soundPlayingNodes.splice(idx, 1);

        this.m_soundWaitingNodes.push(node);

    }

    /**

    * 播放音效

    * @param {String} name 音效名称

    * @param {Number} volumeScale 音效声音缩放值

    * @param {Function} callback 回调函数

    */

    public playSound(name: string, volumeScale: number = 1, callback: Function = null) {

        if (this.soundVolume === 0) return;

        let soundNode = this.getAudioNode();

        let soundAudioSource = soundNode.getComponent(AudioSource);

        let clip = this._cachedAudioClipMap[name];

        if (clip) {

            soundNode.on(AudioSource.EventType.ENDED, () => {

                this.putAudioNode(soundNode);

                if (callback) callback();

            }, this);

            soundAudioSource.loop = false;

            let volume = Math.min(1, Math.max(0, this.soundVolume * volumeScale));

            soundAudioSource.playOneShot(clip, volume);

        } else {

            let path = 'sounds/' + name;

            resources.load(path, AudioClip, (err: Error, audioClip: AudioClip) => {

                if (err) {

                    warn(`load audioClip ${name} failed: `, err.message);

                    return;

                }

                this._cachedAudioClipMap[name] = audioClip;

                soundNode.on(AudioSource.EventType.ENDED, () => {

                    this.putAudioNode(soundNode);

                    if (callback) callback();

                }, this);

                soundAudioSource.loop = false;

                let volume = Math.min(1, Math.max(0, this.soundVolume * volumeScale));

                soundAudioSource.playOneShot(audioClip, volume);

            })

        }

    }

    /** 音量 */

    get soundVolume(): number { return this.m_soundVolume; }

    set soundVolume(value: number) {

        this.m_soundVolume = Math.min(1, Math.max(0, value));

    }

    public clearAllSoundNode() {

        this.m_soundPlayingNodes = [];

        this.m_soundWaitingNodes = [];

    }

}

备注:
1,Tools 为工具脚本,基于字符串进行数据本地化存储,默认返回值为0,所以需要初始化声音大小。
2,声音资源存储在本地,“resources/sounds/” 其下,所以是直接动态加载。
3,clearAllSoundNode 需要在场景主线脚本的onDestroy 生命周期函数中调用一次,避免因场景销毁,创建的音效节点读取问题。
4,模块是分别控制背景音乐和音效的,采用滑动的方式进行控制,所以静音属性未编写控制函数。

1赞
  1. 一个node可以挂载多个AudioSource,似乎没必要创建多个node;
  2. 每个平台有自己的同时播放音效数量的上限,最好处理下,cocos是直接停止以前的;
  3. 可以考虑由用户指定bundle,因为resources是游戏启动就加载的,对小游戏不友好;
  4. 因为是异步加载的,可以考虑下如果还没加载完,就调用暂停方法,会怎么样。
1赞

其实用一个节点就够了,在同一个节点添加两个AudioSource组件,然后也不需要重复创建销毁,音效可以通过AudioSource.playOneShot 方法来播放,初次实例化之后,直接添加为常驻节点。

static _Instance: AudioManager;
static get Instance(): AudioManager {

    if (this._Instance == null) {
        let node = new Node();
        let comp = node.addComponent(AudioManager);
        node.name= "AudioManager";

        //加入场景节点
        let scene = director.getScene();
        scene.addChild(node);
        director.addPersistRootNode(node);

        //播放音效的组件
        comp.curAudioSE = comp.addComponent(AudioSource);
        comp.curAudioSE.loop = false;

        //播放BGM的组件
        comp.curAudioBGM = comp.addComponent(AudioSource);
        comp.curAudioBGM.loop = true;
        this._Instance = comp;
    }

    return this._Instance;
}

一个节点是可以重复添加同一个组件的吗,那如果需要暂停,怎么去区分

好的,我会参考继续完善修改

3.x 版本是基于场景存在,然后才有的canvas,scene.addChild(node) 添加上去就是与canvas同级节点吗

comp.curAudioSE = comp.addComponent(AudioSource);
comp.curAudioBGM = comp.addComponent(AudioSource);
添加组件的时候分别引用就行,
然后分开调用音效的 comp.curAudioSE, 和音乐的 comp.curAudioBGM

但是这是音效组件,不是2D渲染组件,所以并不需要放到Canvas里

通过结合上面两位大佬的建议 @whitesheep @dream_chou93 修改后的简易版本,还有问题望指出

import { assert, AssetManager, assetManager, AudioClip, AudioSource, Component, director, find, isValid, log, Node, resources, warn } from "cc";

import Tools from "./Tools";

export enum AUDIO_LOCAL_KEY {

    MUSIC_VOLUME = "MUSIC_VOLUME",

    EFFECT_VOLUME = "EFFECT_VOLUME"

}

export class AudioManager extends Component {

    private static _instance: AudioManager = null;

    public static get Instance(): AudioManager {

        this._instance = this._instance || new AudioManager();

        return this._instance;

    }

    // bundle 资源

    private _soundBundle?: AssetManager.Bundle;

    //声音资源的缓存

    private _cachedAudioClipMap: Record<string, AudioClip> = {};

    //音乐节点组件

    private m_musicSource?: AudioSource;

    //音效组件

    private m_soundSource?: AudioSource;

    //音乐和音效的音量大小

    private m_musicVolume: number = -1;

    private m_soundVolume: number = -1;

    //音乐音效是否静音

    // private m_musicMute: boolean = false;

    // private m_soundMute: boolean = false;

    //模块初始化

    public init() {

        let musicVolume_record = Number(Tools.getLocalRecord(AUDIO_LOCAL_KEY.EFFECT_VOLUME));

        let soundVolume_record = Number(Tools.getLocalRecord(AUDIO_LOCAL_KEY.MUSIC_VOLUME));

        this.m_musicVolume = (this.m_musicVolume === -1 && musicVolume_record === 0) ? 0.2 : musicVolume_record;

        this.m_soundVolume = (this.m_soundVolume === -1 && soundVolume_record === 0) ? 0.7 : soundVolume_record;

        this.addSoundPersistRootNode();

    }

    //初始化常驻节点

    private addSoundPersistRootNode() {

        let node = new Node("AudioManagerNode");

        let comp = node.addComponent(AudioManager);

        //加入场景节点

        let scene = director.getScene();

        scene.addChild(node);

        director.addPersistRootNode(node);

        //播放音效的组件

        this.m_soundSource = comp.addComponent(AudioSource);

        this.m_soundSource.loop = false;

        //播放BGM的组件

        this.m_musicSource = comp.addComponent(AudioSource);

        this.m_musicSource.loop = true;

    }

    //加载本地bundle包

    private loadSoundBundle() {

        return new Promise((resolve, reject) => {

            if (this._soundBundle) {

                resolve(this._soundBundle);

            } else {

                assetManager.loadBundle('sounds', (err, bundle) => {

                    if (err) {

                        return reject(err);

                    }

                    this._soundBundle = bundle;

                    resolve(bundle);

                })

            }

        });

    }

    //***********************************音乐********************************************************** */

    /**

     * 播放音乐

     * @param {String} name  音乐名称

     * @param {Number} volumeScale 音乐声音大小

     */

    public playMusic(name: string, volumeScale: number = 1) {

        if (this.musicVolume === 0) return;

        const musicAudioSource = this.m_musicSource!;

        if (musicAudioSource.clip && musicAudioSource.clip.name === name) {

            if (!musicAudioSource.playing) {

                let volume = Math.min(1, Math.max(0, this.musicVolume * volumeScale));

                musicAudioSource.volume = volume;

                musicAudioSource.play();

            }

        } else {

            this.stopMusic();

            let clip: AudioClip = this._cachedAudioClipMap[name];

            if (clip) {

                musicAudioSource.clip = clip;

                let volume = Math.min(1, Math.max(0, this.musicVolume * volumeScale));

                musicAudioSource.volume = volume;

                musicAudioSource.play();

            } else {

                this.loadSoundBundle().then((bundle: AssetManager.Bundle) => {

                    bundle.load(name, AudioClip, (err: Error, audioClip: AudioClip) => {

                        if (err) {

                            warn(`load audioClip ${name} failed: `, err.message);

                            return;

                        }

                        this._cachedAudioClipMap[name] = audioClip;

                        musicAudioSource.clip = audioClip;

                        let volume = Math.min(1, Math.max(0, this.musicVolume * volumeScale));

                        musicAudioSource.volume = volume;

                        musicAudioSource.play();

                    });

                })

            }

        }

    }

    /** 音量 */

    get musicVolume(): number { return this.m_musicVolume; }

    set musicVolume(value: number) {

        this.m_musicVolume = Math.min(1, Math.max(0, value));

        this.m_musicSource.volume = value;

    }

    public stopMusic() {

        if (this.m_musicSource.clip) {

            this.m_musicSource.stop();

        }

    }

    public pauseMusic(): void {

        if (this.m_musicSource.playing) {

            this.m_musicSource.pause();

        }

    }

    public resumeMusic(): void {

        if (this.m_musicSource.clip) {

            this.m_musicSource.play();

        }

    }

    //***********************************音效********************************************************** */

    /**

    * 播放音效

    * @param {String} name 音效名称

    * @param {Number} volumeScale 音效声音缩放值

    */

    public playSound(name: string, volumeScale: number = 1) {

        if (this.soundVolume === 0) return;

        let clip = this._cachedAudioClipMap[name];

        const soundAudioSource = this.m_soundSource!;

        if (clip) {

            let volume = Math.min(1, Math.max(0, this.soundVolume * volumeScale));

            soundAudioSource.playOneShot(clip, volume);

        } else {

            this.loadSoundBundle().then((bundle: AssetManager.Bundle) => {

                bundle.load(name, AudioClip, (err: Error, audioClip: AudioClip) => {

                    if (err) {

                        warn(`load audioClip ${name} failed: `, err.message);

                        return;

                    }

                    this._cachedAudioClipMap[name] = audioClip;

                    let volume = Math.min(1, Math.max(0, this.soundVolume * volumeScale));

                    soundAudioSource.playOneShot(audioClip, volume);

                })

            })

        }

    }

    /** 音量 */

    get soundVolume(): number { return this.m_soundVolume; }

    set soundVolume(value: number) {

        this.m_soundVolume = Math.min(1, Math.max(0, value));

    }

}
1赞