228 lines
7.5 KiB
TypeScript
228 lines
7.5 KiB
TypeScript
import { _decorator, AudioClip, AudioSource, Node, resources, director } from 'cc';
|
||
|
||
export class AudioManager {
|
||
private static _instance: AudioManager = null;
|
||
private _audioNode: Node = null;
|
||
private _bgmAudioSource: AudioSource = null; // 专门用于背景音乐
|
||
private _sfxAudioSources: AudioSource[] = []; // 用于音效的多个AudioSource
|
||
private _bgmVolume: number = 1;
|
||
private _bgmClip: AudioClip = null;
|
||
private _audioClips: Map<string, AudioClip> = new Map();
|
||
private _isMuted: boolean = false;
|
||
private _isPaused: boolean = false;
|
||
// 追踪哪个 AudioSource 正在播放哪个 SFX 名称 + 定时清理
|
||
private _sfxNameBySource: Map<AudioSource, string> = new Map();
|
||
private _sfxTimers: Map<AudioSource, number> = new Map();
|
||
|
||
static get instance(): AudioManager {
|
||
if (!this._instance) {
|
||
this._instance = new AudioManager();
|
||
}
|
||
return this._instance;
|
||
}
|
||
|
||
init() {
|
||
// 创建根节点
|
||
this._audioNode = new Node('AudioManager');
|
||
director.getScene().addChild(this._audioNode);
|
||
|
||
// 创建BGM节点
|
||
let bgmNode = new Node('BGM');
|
||
this._audioNode.addChild(bgmNode);
|
||
this._bgmAudioSource = bgmNode.addComponent(AudioSource);
|
||
this._bgmAudioSource.playOnAwake = false;
|
||
|
||
// 创建多个音效节点
|
||
for (let i = 0; i < 3; i++) {
|
||
let sfxNode = new Node(`SFX_${i}`);
|
||
this._audioNode.addChild(sfxNode);
|
||
let sfxAudioSource = sfxNode.addComponent(AudioSource);
|
||
sfxAudioSource.playOnAwake = false;
|
||
this._sfxAudioSources.push(sfxAudioSource);
|
||
}
|
||
|
||
// 加载音频资源
|
||
this.loadAudioClips();
|
||
}
|
||
|
||
private loadAudioClips() {
|
||
resources.loadDir("audio", AudioClip, (err, clips) => {
|
||
if (err) {
|
||
console.error("加载音频资源失败:", err);
|
||
return;
|
||
}
|
||
clips.forEach(clip => {
|
||
this._audioClips.set(clip.name, clip);
|
||
});
|
||
});
|
||
}
|
||
|
||
playBGM(name: string, loop: boolean = true) {
|
||
if (this._isMuted || !this._bgmAudioSource) return;
|
||
this._bgmAudioSource.stop();
|
||
let clip = this._audioClips.get(name);
|
||
if (!clip) {
|
||
console.warn(`BGM ${name} 不存在`);
|
||
return;
|
||
}
|
||
this._bgmClip = clip;
|
||
this._bgmAudioSource.clip = clip;
|
||
this._bgmAudioSource.loop = loop;
|
||
this._bgmAudioSource.volume = this._bgmVolume;
|
||
this._bgmAudioSource.play();
|
||
this._isPaused = false;
|
||
}
|
||
|
||
// 统一封装:以名称打标签地播放一次性音效,并在结束后清理
|
||
private _playOneShotTagged(source: AudioSource, clip: AudioClip, name: string, volume: number, loop: boolean) {
|
||
source.volume = volume;
|
||
source.clip = clip;
|
||
source.loop = loop;
|
||
source.play();
|
||
// source.playOneShot(clip, volume);
|
||
|
||
this._sfxNameBySource.set(source, name);
|
||
|
||
// 估算时长用于自动清理(优先 getDuration,其次 duration)
|
||
const dur = (clip as any)?.getDuration ? (clip as any).getDuration() : ((clip as any)?.duration ?? 0);
|
||
// 若已有旧定时器,先清理
|
||
const oldTimer = this._sfxTimers.get(source);
|
||
if (oldTimer) {
|
||
clearTimeout(oldTimer);
|
||
this._sfxTimers.delete(source);
|
||
}
|
||
const timer = setTimeout(() => {
|
||
this._sfxNameBySource.delete(source);
|
||
this._sfxTimers.delete(source);
|
||
}, Math.max(0, dur * 1000 + 50)) as unknown as number;
|
||
this._sfxTimers.set(source, timer);
|
||
}
|
||
|
||
playSFX(name: string, volume: number = 1.0, loop: boolean = false) {
|
||
if (this._isMuted) return;
|
||
|
||
let clip = this._audioClips.get(name);
|
||
if (!clip) {
|
||
console.warn(`音效 ${name} 不存在`);
|
||
return;
|
||
}
|
||
|
||
// 找到一个空闲的AudioSource
|
||
let availableSource = this._sfxAudioSources.find(source => !source.playing);
|
||
if (availableSource && this.isSpecialAudio(name)) {
|
||
// 打标签播放(支持后续按名称停止)
|
||
this._playOneShotTagged(availableSource, clip, name, volume, loop);
|
||
} else {
|
||
// 如果没有空闲的AudioSource,创建一个新的
|
||
let sfxNode = new Node(`SFX_${this._sfxAudioSources.length}`);
|
||
this._audioNode.addChild(sfxNode);
|
||
let newSource = sfxNode.addComponent(AudioSource);
|
||
newSource.playOnAwake = false;
|
||
this._sfxAudioSources.push(newSource);
|
||
this._playOneShotTagged(newSource, clip, name, volume, loop);
|
||
}
|
||
}
|
||
|
||
isSpecialAudio(name: string) {
|
||
return name == 'Win_Icon_Up' || name == 'Win_Icon_Up_Free'
|
||
}
|
||
|
||
// 按名称停止当前播放的 SFX。stopAll=true 时停止所有同名实例,false 时仅停止一个。
|
||
stopSFX(name: string, stopAll: boolean = true) {
|
||
if (!name) return;
|
||
|
||
for (const source of this._sfxAudioSources) {
|
||
const tag = this._sfxNameBySource.get(source);
|
||
if (tag === name && source.playing) {
|
||
source.stop();
|
||
// 清理内部状态
|
||
const timer = this._sfxTimers.get(source);
|
||
if (timer) {
|
||
clearTimeout(timer);
|
||
this._sfxTimers.delete(source);
|
||
}
|
||
this._sfxNameBySource.delete(source);
|
||
|
||
if (!stopAll) break;
|
||
}
|
||
}
|
||
}
|
||
|
||
async stopAllSFX(): Promise<void> {
|
||
let promises = this._sfxAudioSources.map(source => {
|
||
return new Promise<void>((resolve) => {
|
||
source.stop();
|
||
// 清理内部状态
|
||
const timer = this._sfxTimers.get(source);
|
||
if (timer) {
|
||
clearTimeout(timer);
|
||
this._sfxTimers.delete(source);
|
||
}
|
||
this._sfxNameBySource.delete(source);
|
||
|
||
// 使用 setTimeout 确保音效停止
|
||
setTimeout(resolve, 100);
|
||
});
|
||
});
|
||
|
||
await Promise.all(promises);
|
||
}
|
||
|
||
stopBGM() {
|
||
if (this._bgmAudioSource) {
|
||
this._bgmAudioSource.stop();
|
||
this._isPaused = false;
|
||
}
|
||
}
|
||
|
||
pauseBGM() {
|
||
if (this._bgmAudioSource && !this._isPaused) {
|
||
this._bgmAudioSource.pause();
|
||
this._isPaused = true;
|
||
}
|
||
}
|
||
|
||
resumeBGM() {
|
||
if (!this._isMuted && this._bgmAudioSource && this._isPaused) {
|
||
this._bgmAudioSource.play();
|
||
this._isPaused = false;
|
||
}
|
||
}
|
||
|
||
setBGMVolume(volume: number) {
|
||
this._bgmVolume = Math.max(0, Math.min(1, volume));
|
||
if (this._bgmAudioSource) {
|
||
this._bgmAudioSource.volume = this._bgmVolume;
|
||
}
|
||
}
|
||
|
||
|
||
setMute(muted: boolean) {
|
||
this._isMuted = muted;
|
||
if (muted) {
|
||
// this.stopBGM();
|
||
this.setBGMVolume(0);
|
||
} else if (this._bgmClip) {
|
||
this.setBGMVolume(1);
|
||
// this.playBGM(this._bgmClip.name);
|
||
}
|
||
}
|
||
|
||
getMuted() {
|
||
return this._isMuted;
|
||
}
|
||
|
||
// 清理资源
|
||
destroy() {
|
||
if (this._audioNode) {
|
||
this._audioNode.destroy();
|
||
}
|
||
this._audioNode = null;
|
||
this._bgmAudioSource = null;
|
||
this._sfxAudioSources = [];
|
||
this._audioClips.clear();
|
||
this._sfxNameBySource.clear();
|
||
this._sfxTimers.forEach(t => clearTimeout(t));
|
||
this._sfxTimers.clear();
|
||
}
|
||
} |