import { AudioClip, AudioSource, director, Node } from 'cc'; import { LOCAL_MUSIC_SWITCH, LOCAL_SOUND_SWITCH } from '@scripts/constants/Localkey'; import { BUNDLE_ID } from '@scripts/define/ResDefine'; import { localStorageGet, localStorageSet } from '@scripts/utils/Helper'; import { ResourceManager } from '../resource/ResourceManager'; const DEFAULT_SWITCH_VALUE = '1'; export interface SoundConfig { path: string; bundle: string; } /** 新音频资源在此登记,播放时传入对应 key 即可。 */ export const SOUND_CONFIGS: Record = { bgm: { path: 'sounds/bgm', bundle: BUNDLE_ID.Lobby }, button: { path: 'sounds/button', bundle: BUNDLE_ID.Lobby }, }; /** 管理跨场景的背景音乐、音效及其本地开关。 */ export class SoundManager { private readonly musicSource: AudioSource; private readonly soundSource: AudioSource; private readonly clips = new Map(); private _musicEnabled: boolean; private _soundEnabled: boolean; constructor(private readonly resources: ResourceManager) { this._musicEnabled = this.readSwitch(LOCAL_MUSIC_SWITCH); this._soundEnabled = this.readSwitch(LOCAL_SOUND_SWITCH); const root = new Node('SoundManager'); const musicNode = new Node('Music'); const soundNode = new Node('Sound'); root.addChild(musicNode); root.addChild(soundNode); this.musicSource = musicNode.addComponent(AudioSource); this.musicSource.playOnAwake = false; this.soundSource = soundNode.addComponent(AudioSource); this.soundSource.playOnAwake = false; director.addPersistRootNode(root); } get musicEnabled(): boolean { return this._musicEnabled; } set musicEnabled(enabled: boolean) { this.setMusicEnabled(enabled); } get soundEnabled(): boolean { return this._soundEnabled; } set soundEnabled(enabled: boolean) { this.setSoundEnabled(enabled); } /** * 预加载指定音频;不传 key 时预加载全部已配置音频。 */ async preload(keys: string | readonly string[] = Object.keys(SOUND_CONFIGS)): Promise { const soundKeys = typeof keys === 'string' ? [keys] : keys; await Promise.all(soundKeys.map((key) => this.loadClip(key))); } /** 播放背景音乐。关闭音乐时会记住当前曲目,重新开启后自动继续播放。 */ async playMusic(key: string, loop = true, volume = 1): Promise { const clip = await this.loadClip(key); if (!clip) return; const shouldPlay = this.musicSource.clip !== clip || !this.musicSource.playing; this.musicSource.clip = clip; this.musicSource.loop = loop; this.musicSource.volume = this.normalizeVolume(volume); if (this._musicEnabled && shouldPlay) { this.musicSource.play(); } } /** 播放一次音效;允许同一音效重叠播放。 */ async playSound(key: string, volume = 1): Promise { if (!this._soundEnabled) return; const clip = await this.loadClip(key); if (!clip || !this._soundEnabled) return; this.soundSource.playOneShot(clip, this.normalizeVolume(volume)); } setMusicEnabled(enabled: boolean): void { this._musicEnabled = Boolean(enabled); localStorageSet(LOCAL_MUSIC_SWITCH, this._musicEnabled ? '1' : '0'); if (this._musicEnabled) { if (this.musicSource.clip) this.musicSource.play(); } else { this.musicSource.pause(); } } setSoundEnabled(enabled: boolean): void { this._soundEnabled = Boolean(enabled); localStorageSet(LOCAL_SOUND_SWITCH, this._soundEnabled ? '1' : '0'); } private readSwitch(key: string): boolean { const value = String(localStorageGet(key, DEFAULT_SWITCH_VALUE)).toLowerCase(); return value !== '0' && value !== 'false'; } private async loadClip(key: string): Promise { const cachedClip = this.clips.get(key); if (cachedClip?.isValid) return cachedClip; const config = SOUND_CONFIGS[key]; if (!config) { console.warn(`[SoundManager] Unknown sound key: ${key}`); return null; } const clip = await this.resources.load(config.bundle, config.path, AudioClip); if (clip) this.clips.set(key, clip); return clip; } private normalizeVolume(volume: number): number { if (!Number.isFinite(volume)) return 1; return Math.min(1, Math.max(0, volume)); } playBgm() { this.playMusic('bgm'); } playBtnClick() { this.playSound("button") } }