rp_11009/assets/Main/Scripts/managers/AudioManager.ts
2026-04-01 11:10:33 +08:00

224 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) {
source.volume = volume;
source.clip = clip;
source.loop = false;
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) {
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._playOneShotTagged(availableSource, clip, name, volume);
} 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);
}
}
// 按名称停止当前播放的 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();
}
}