CocosCreator的音频管理以及如何打断音效、如何切换BGM

音频管理

Cocos Creator 3.x 使用 AudioSource 控制音频的播放。AudioSource 是组件,可以添加到场景中,由 编辑器 设置,也可以在 脚本 中进行调用。
这里就演示如何在脚本中创建音频管理模块。

  1. 创建一个空项目
  2. 创建音频管理脚本AudioMgr
  3. 把AudioMgr设为单例模式提供全局访问接口
  4. 在构造函数中创建一个空节点,并挂载AudioSource组件,把该节点设置为常驻节点
  5. 提供播放背景音乐的接口并实现背景音乐的播放
  6. 提供播放音效的接口并实现音效的播放
  7. 提供一个设置开关的接口并实现音频的打开和关闭
  8. 提供音频暂停的接口并实现音频的暂停
  9. 提供音频恢复的接口并实现音频的恢复
  • 音频管理脚本
import { _decorator, AudioClip, AudioSource, Component, director, Node, resources } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('AudioMgr')
export class AudioMgr {

    //设计成单例
    private static _instance: AudioMgr;
    public static get Instance(): AudioMgr {
        if (!this._instance) {
            this._instance = new AudioMgr();
        }
        return this._instance;
    }
    //AudioSource组件用于控制音频播放
    private _audioSource: AudioSource;

    private _volume: number = 0.8;//默认音量(部分web平台(ios)限制并不会生效)
    private _sound_on: boolean = true;//音效开关


    constructor() {
        //创建一个空节点
        let audio_node = new Node();
        audio_node.name = '__audio_node__';
        //把创建的节点添加到场景中
        director.getScene().addChild(audio_node);
        //设置为常驻节点
        director.addPersistRootNode(audio_node);
        //添加AudioSource组件
        this._audioSource = audio_node.addComponent(AudioSource);
        //把音频设置为非加载完自动播放(部分web平台不允许未交互就有音频播放)
        this._audioSource.playOnAwake = false;
    }

    public get AudioSource(): AudioSource {
        return this._audioSource;
    }

    /**
     * 播放背景音乐
     * @param sound 音频剪辑或者resources的音频路径
     * @param volume 
     */
    playBgm(clip: AudioClip | string, volume: number = 1.0) {
        if (!this._sound_on) {
            this._volume = 0;
        } else {
            this._volume = volume;
        }
        let sound = clip;
        if (!sound) {
            console.error("背景音乐为空");
            return;
        }
        if (sound instanceof AudioClip) {
            this._audioSource.stop();
            this._audioSource.clip = sound; //考虑到切换背景音乐时,先停止后播放,防止出现播放叠加或不播放的问题
            this._audioSource.play();
            this._audioSource.loop = true;
            this._audioSource.volume = this._volume;
        } else {
            //(实际上不放在这里,一般来说会在ResMgr中预先加载)
            resources.load(sound, (err, audio: AudioClip) => {
                if (err) {
                    console.error("背景音乐加载失败", sound);
                } else {
                    this._audioSource.stop();
                    this._audioSource.clip = audio;
                    this._audioSource.play();
                    this._audioSource.loop = true;
                    this._audioSource.volume = this._volume;
                }
            });
        }
    };

    /**
     * 播放音效1 预加载方式
     * @param soundtype 音频类型(定义)
     * @param volume 
     */
    playEffect(sound_clip: AudioClip | string, volume: number = 1.0) {
        if (!this._sound_on) return;
        let sound = sound_clip;
        if (!sound) {
            console.error("音效为空");
            return;
        }
        //判断是不是音频剪辑
        if (sound instanceof AudioClip) {
            this._audioSource.playOneShot(sound, this._volume);
        } else {
            //动态加载(实际上不放在这里,一般来说会在ResMgr中预先加载)
            resources.load(sound, (err, audio: AudioClip) => {
                if (err) {
                    console.error("音效加载失败", sound);
                } else {
                    this._audioSource.playOneShot(audio, this._volume);
                }
            });
        }
    }

    /**
     * 客户端音效开关
     * @param soundOpen 
     */
    setMenu(soundOpen: boolean) {
        this._sound_on = soundOpen;
        if (!this._sound_on) {
            this._audioSource.volume = 0;
            this._volume = 0;
        } else {
            this._audioSource.volume = 1.0;
            this._volume = 1.0;
        }
    }
    /**
     * 停止背景音乐
     */
    stopBgm() {
        this._audioSource.stop();
    }

    /**
     * 暂停背景音乐
     */
    pauseBgm() {
        this._audioSource.pause();
    }
    /**
     * 恢复背景音乐
     */
    resumeBgm() {
        this._audioSource.play();
    }
}


  • 测试脚本
import { _decorator, AudioClip, Button, Component, EventTouch, Label, Node, Sprite, SpriteFrame } from 'cc';
import { AudioMgr } from './AudioMgr';
const { ccclass, property } = _decorator;

@ccclass('AudioCtrl')
export class AudioCtrl extends Component {
    @property({ type: AudioClip, displayName: "背景音乐1" })
    bgm1: AudioClip = null;
    @property({ type: AudioClip, displayName: "背景音乐2" })
    bgm2: AudioClip = null;
    @property({ type: AudioClip, displayName: "音效" })
    se: AudioClip = null;
    @property({ type: SpriteFrame, displayName: "音效开" })
    soundOn: SpriteFrame = null;
    @property({ type: SpriteFrame, displayName: "音效关" })
    soundOff: SpriteFrame = null;
    @property(Node)
    firstInactiveNode: Node = null;

    _soundOn: boolean = true;
    _bgmIndex: number = 1;
    start() {
        this.firstInactiveNode.once(Node.EventType.TOUCH_START, this.onFirstInactive, this);
    }
    onFirstInactive(event: EventTouch) {
        event.preventSwallow = true;//事件向下传递
        this.firstInactiveNode.active = false;
        AudioMgr.Instance.playBgm(this.bgm1);
    }

    /**
     * 播放/停止背景音乐
     */
    onBgm1(btn: Button) {
        let btn_node = btn.target;
        if (AudioMgr.Instance.AudioSource.playing) {
            btn_node.getChildByName("Label").getComponent(Label).string = "播放背景音乐";
            AudioMgr.Instance.stopBgm();
        } else {
            AudioMgr.Instance.playBgm(this.bgm1);
            btn_node.getChildByName("Label").getComponent(Label).string = "停止背景音乐";
        }
    }
    /**
     * 暂停/恢复背景音乐
     */
    onBgm2(btn: Button) {
        let btn_node = btn.target;
        if (AudioMgr.Instance.AudioSource.playing) {
            btn_node.getChildByName("Label").getComponent(Label).string = "恢复背景音乐";
            AudioMgr.Instance.pauseBgm();
        } else {
            AudioMgr.Instance.resumeBgm();
            btn_node.getChildByName("Label").getComponent(Label).string = "暂停背景音乐";
        }
    }

    /**
     * 播放音效
     */
    onSe(btn: Button) {
        let btn_node = btn.target;
        AudioMgr.Instance.playEffect(this.se);
    }
    /**
     * 音频开关设置
     */
    onSeSet(btn: Button) {
        let btn_node = btn.target;
        this._soundOn = !this._soundOn;
        if (this._soundOn) {
            btn_node.getComponent(Sprite).spriteFrame = this.soundOn;
        } else {
            btn_node.getComponent(Sprite).spriteFrame = this.soundOff;
        };
        AudioMgr.Instance.setMenu(this._soundOn);
    }

    onChangeBgm() {
        this._bgmIndex = this._bgmIndex == 1 ? 2 : 1;
        AudioMgr.Instance.playBgm(this["bgm" + this._bgmIndex]);
    }
    update(deltaTime: number) {

    }
}


这里需要注意一点,在切换背景音乐时,需要先停止当前的audioSource,再赋值新的clip,再播放,顺序不要搞错了,如果搞错了,音频会不播放或不同的背景音乐重叠播放。
大概像这样:
image

但是在播放音效时遇到了一个问题,播放短音效时不明显,短时间内播放几个长音效时会重叠,这是因为音效的播放统一使用了官方推荐的playOneShot ,但playOneShot 是一次性播放操作,播放后的音效无法暂停或停止播放,也无法监听播放结束的事件回调。
image
不仅体现在无法暂停或停止播放,音效按钮的开关也不会影响到正在播放到音效,直到它被播放完毕。

这在实际应用中显得不那么灵活,比如释放技能类的(长/大于1s)音效反馈我们希望它能播放完毕,所以使用playOneShot没问题。但想一些互动语音反馈,我们希望在下一次交互时终端上一次的语音播放,这里如果再去使用playOneShot就会出现上述无法终止的问题。

  • 完善音频管理
    这里的想法是一些短音效或不需要被打断的音效依然使用用playOneShot播放。
    一些需要即时可以打断的音效,沿用上面播放/切换bgm的函数实现,只需要把audioSource的loop设置为false,注意需要另外创建一个audioSource,不要和Bgm使用同一个,否则会把背景音乐也给打断(切换)掉。
    实现的函数也另外定义一个比如叫“playEffectCanBreak”。
import { _decorator, AudioClip, AudioSource, Component, director, Node, resources } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('AudioMgr')
export class AudioMgr {

    //设计成单例
    private static _instance: AudioMgr;
    public static get Instance(): AudioMgr {
        if (!this._instance) {
            this._instance = new AudioMgr();
        }
        return this._instance;
    }
    //AudioSource组件用于控制音频播放
    private _audioSource: AudioSource;

    private _volume: number = 0.8;//默认音量(部分web平台(ios)限制并不会生效)
    private _sound_on: boolean = true;//音效开关

    //特别:用于播放可以被打断的音效
    private _audioSource2: AudioSource;


    constructor() {
        //创建一个空节点
        let audio_node = new Node();
        audio_node.name = '__audio_node__';
        //把创建的节点添加到场景中
        director.getScene().addChild(audio_node);
        //设置为常驻节点
        director.addPersistRootNode(audio_node);
        //添加AudioSource组件
        this._audioSource = audio_node.addComponent(AudioSource);
        //把音频设置为非加载完自动播放(部分web平台不允许未交互就有音频播放)
        this._audioSource.playOnAwake = false;

        let audio_node2 = new Node();
        audio_node2.name = '__audio_node2__';
        director.getScene().addChild(audio_node2);
        director.addPersistRootNode(audio_node2);
        this._audioSource2 = audio_node2.addComponent(AudioSource);
        this._audioSource2.playOnAwake = false;
    }

    public get AudioSource(): AudioSource {
        return this._audioSource;
    }

    public get AudioSource2(): AudioSource {
        return this._audioSource2;
    }

    /**
     * 播放背景音乐
     * @param sound 音频剪辑或者resources的音频路径
     * @param volume 
     */
    playBgm(clip: AudioClip | string, volume: number = 1.0) {
        if (!this._sound_on) {
            this._volume = 0;
        } else {
            this._volume = volume;
        }
        let sound = clip;
        if (!sound) {
            console.error("背景音乐为空");
            return;
        }
        if (sound instanceof AudioClip) {
            this._audioSource.stop();
            this._audioSource.clip = sound; //考虑到切换背景音乐时,先停止后播放,防止出现播放叠加或不播放的问题
            this._audioSource.play();
            this._audioSource.loop = true;
            this._audioSource.volume = this._volume;
        } else {
            //(实际上不放在这里,一般来说会在ResMgr中预先加载)
            resources.load(sound, (err, audio: AudioClip) => {
                if (err) {
                    console.error("背景音乐加载失败", sound);
                } else {
                    this._audioSource.stop();
                    this._audioSource.clip = audio;
                    this._audioSource.play();
                    this._audioSource.loop = true;
                    this._audioSource.volume = this._volume;
                }
            });
        }
    };

    /**
     * 播放音效1 预加载方式
     * @param soundtype 音频类型(定义)
     * @param volume 
     */
    playEffect(sound_clip: AudioClip | string, volume: number = 1.0) {
        if (!this._sound_on) return;
        let sound = sound_clip;
        if (!sound) {
            console.error("音效为空");
            return;
        }
        //判断是不是音频剪辑
        if (sound instanceof AudioClip) {
            this._audioSource.playOneShot(sound, this._volume);
        } else {
            //动态加载(实际上不放在这里,一般来说会在ResMgr中预先加载)
            resources.load(sound, (err, audio: AudioClip) => {
                if (err) {
                    console.error("音效加载失败", sound);
                } else {
                    this._audioSource.playOneShot(audio, this._volume);
                }
            });
        }
    }

    /**
     * 播放音效2 可以被打断的音效
     * @param sound_clip 
     * @param volume 
     */
    playEffectCanBreak(sound_clip: AudioClip | string, volume: number = 1.0) {
        if (!this._sound_on) {
            this._volume = 0;
        } else {
            this._volume = volume;
        }
        let sound = sound_clip;
        if (!sound) {
            console.error("音效为空");
            return;
        }
        if (sound instanceof AudioClip) {
            this._audioSource2.stop();
            this._audioSource2.clip = sound; 
            this._audioSource2.play();
            this._audioSource2.loop = false;
            this._audioSource2.volume = this._volume;
        } else {
            resources.load(sound, (err, audio: AudioClip) => {
                if (err) {
                    console.error("音效加载失败", sound);
                } else {
                    this._audioSource2.stop();
                    this._audioSource2.clip = audio;
                    this._audioSource2.play();
                    this._audioSource2.loop = false;
                    this._audioSource2.volume = this._volume;
                }
            });
        }
    }

    /**
     * 客户端音效开关
     * @param soundOpen 
     */
    setMenu(soundOpen: boolean) {
        this._sound_on = soundOpen;
        if (!this._sound_on) {
            this._audioSource.volume = 0;
            this._audioSource2.volume = 0;
            this._volume = 0;
        } else {
            this._audioSource.volume = 1.0;
            this._audioSource2.volume = 1.0;
            this._volume = 1.0;
        }
    }
    /**
     * 停止背景音乐
     */
    stopBgm() {
        this._audioSource.stop();
    }

    /**
     * 暂停背景音乐
     */
    pauseBgm() {
        this._audioSource.pause();
    }
    /**
     * 恢复背景音乐
     */
    resumeBgm() {
        this._audioSource.play();
    }
}


posted @ 2024-11-22 10:52  EricShx  阅读(952)  评论(0)    收藏  举报