Cocos Creator音频播放管理器

2,216 阅读3分钟

由于 Cocos Creator 3.x 移除了 v2.x cc.audioEngine 系列的 API,统一使用 AudioSource 控制音频播放。

根据cocos官网给的例子按自己实际使用修改的一个小工具🔧

先看官网给的例子,官方现在推荐使用的是audioSource, 播放音乐用 play, 播放音效用 playOneShot。但是一个audioSource只能同时操作单个音频,所以当我们有多个音频需要同时播放时就需要做一些“加工”了。

//AudioMgr.ts
import { Node, AudioSource, AudioClip, resources, director } from 'cc';
/**
 * @en
 * this is a sington class for audio play, can be easily called from anywhere in you project.
 * @zh
 * 这是一个用于播放音频的单件类,可以很方便地在项目的任何地方调用。
 */ 
export class AudioMgr {
    private static _inst: AudioMgr;
    public static get inst(): AudioMgr {
        if (this._inst == null) {
            this._inst = new AudioMgr();
        }
        return this._inst;
    }

    private _audioSource: AudioSource;
    constructor() {
        //@en create a node as audioMgr
        //@zh 创建一个节点作为 audioMgr
        let audioMgr = new Node();
        audioMgr.name = '__audioMgr__';

        //@en add to the scene.
        //@zh 添加节点到场景
        director.getScene().addChild(audioMgr);

        //@en make it as a persistent node, so it won't be destroied when scene change.
        //@zh 标记为常驻节点,这样场景切换的时候就不会被销毁了
        director.addPersistRootNode(audioMgr);

        //@en add AudioSource componrnt to play audios.
        //@zh 添加 AudioSource 组件,用于播放音频。
        this._audioSource = audioMgr.addComponent(AudioSource);
    }

    public get audioSource() {
        return this._audioSource;
    }

    /**
     * @en
     * play short audio, such as strikes,explosions
     * @zh
     * 播放短音频,比如 打击音效,爆炸音效等
     * @param sound clip or url for the audio
     * @param volume 
     */
    playOneShot(sound: AudioClip | string, volume: number = 1.0) {
        if (sound instanceof AudioClip) {
            this._audioSource.playOneShot(sound, volume);
        }
        else {
            resources.load(sound, (err, clip: AudioClip) => {
                if (err) {
                    console.log(err);
                }
                else {
                    this._audioSource.playOneShot(clip, volume);
                }
            });
        }
    }

    /**
     * @en
     * play long audio, such as the bg music
     * @zh
     * 播放长音频,比如 背景音乐
     * @param sound clip or url for the sound
     * @param volume 
     */
    play(sound: AudioClip | string, volume: number = 1.0) {
        if (sound instanceof AudioClip) {
            this._audioSource.stop();
            this._audioSource.clip = sound;
            this._audioSource.play();
            this.audioSource.volume = volume;
        }
        else {
            resources.load(sound, (err, clip: AudioClip) => {
                if (err) {
                    console.log(err);
                }
                else {
                    this._audioSource.stop();
                    this._audioSource.clip = clip;
                    this._audioSource.play();
                    this.audioSource.volume = volume;
                }
            });
        }
    }

    /**
     * stop the audio play
     */
    stop() {
        this._audioSource.stop();
    }

    /**
     * pause the audio play
     */
    pause() {
        this._audioSource.pause();
    }

    /**
     * resume the audio play
     */
    resume(){
        this._audioSource.play();
    }
}

那么我们需要做哪些改动呢?其实也很简单,就是一个音频对应一个audioSource,这里我用Map存储了音效组件AudioSource

/**
* 一个音频对应一个audioSource
*/
private effectList: Map<string, AudioSource> = new Map();

接下来在播放音频时,如果是第一次播放那就创建一个节点用来控制该音频,同时添加AudioSource组件到节点,然后把加载好的音频Clip绑定到AudioSource组件上,最后就是往effectListset新创建的音频组件。这样一顿操作,第二次播放时就可以直接复用已经存在的组件了,节省了系统开销。

async playEffect(sound: string, volume: number = 1.0) {
    let audioSource: AudioSource | undefined = this.effectList.get(sound);
    if (!audioSource) {
        const audioClip = await BundleManager.get().loadAudio(sound);
        if (!audioClip) return;  // 如果加载音频失败,则提前返回
        const audioNode = new Node();
        audioNode.name = sound;
        audioNode.parent = this.audioMgr;
        audioSource = audioNode.addComponent(AudioSource);
        audioSource.name = sound;
        audioSource.clip = audioClip;
        audioSource.volume = volume;
        this.effectList.set(sound, audioSource);
    }
    if (audioSource.clip) {
        //播放短音效使用playOneShot
        audioSource.playOneShot(audioSource.clip);
    }
}

那么长音频该怎么处理呢?

我这里以背景音乐举例,首先定义一个属性😂

/**
 * 背景音
 */
private bgMusic: AudioSource

很简单,直接看代码

async playMusic(sound: string, loop: boolean = true, volume: number = 1.0) {
    if (!this.bgMusic) {
        const audioClip = await BundleManager.get().loadAudio(sound);
        if (!audioClip) return;  // 如果加载音频失败,则提前返回
        const audioNode = new Node();
        audioNode.name = sound;
        audioNode.parent = this.audioMgr;
        const audioSource = audioNode.addComponent(AudioSource);
        audioSource.name = sound;
        audioSource.clip = audioClip;
        audioSource.volume = volume;
        audioSource.loop = loop;
        this.bgMusic = audioSource
    }
    if (this.bgMusic.clip) {
        this.bgMusic.play();
    }
}

最后完整代码奉上

import { Node, AudioSource, director } from 'cc';
import BundleManager from '../bundle/BundleManager';

export class AudioManager {
    private static _inst: AudioManager;
    public static get inst(): AudioManager {
        if (this._inst == null) {
            this._inst = new AudioManager();
        }
        return this._inst;
    }

    /**
     * 音频root节点
     */
    private audioMgr: Node;

    /**
     * 一个音频对应一个audioSource
     */
    private effectList: Map<string, AudioSource> = new Map();

    /**
     * 背景音
     */
    private bgMusic: AudioSource

    constructor() {
        this.audioMgr = new Node();
        this.audioMgr.name = '__audioMgr__';
        director.getScene().addChild(this.audioMgr);
        director.addPersistRootNode(this.audioMgr);
    }

    async playEffect(sound: string, volume: number = 1.0) {
        let audioSource: AudioSource | undefined = this.effectList.get(sound);
        if (!audioSource) {
            const audioClip = await BundleManager.get().loadAudio(sound);
            if (!audioClip) return;  // 如果加载音频失败,则提前返回
            const audioNode = new Node();
            audioNode.name = sound;
            audioNode.parent = this.audioMgr;
            audioSource = audioNode.addComponent(AudioSource);
            audioSource.name = sound;
            audioSource.clip = audioClip;
            audioSource.volume = volume;
            this.effectList.set(sound, audioSource);
        }
        if (audioSource.clip) {
            audioSource.playOneShot(audioSource.clip);
        }
    }

    async playMusic(sound: string, loop: boolean = true, volume: number = 1.0) {
        if (!this.bgMusic) {
            const audioClip = await BundleManager.get().loadAudio(sound);
            if (!audioClip) return;  // 如果加载音频失败,则提前返回
            const audioNode = new Node();
            audioNode.name = sound;
            audioNode.parent = this.audioMgr;
            const audioSource = audioNode.addComponent(AudioSource);
            audioSource.name = sound;
            audioSource.clip = audioClip;
            audioSource.volume = volume;
            audioSource.loop = loop;
            this.bgMusic = audioSource
        }
        if (this.bgMusic.clip) {
            this.bgMusic.play();
        }
    }

    public stopMusic(): void {
        if (!this.bgMusic || !this.bgMusic.playing) return;
        this.bgMusic.stop();
    }

    public stopEffect(sound: string): void {
        let audioSource: AudioSource | undefined = this.effectList.get(sound);
        if (!audioSource || !audioSource.playing) return;
        audioSource.stop();
    }

}