SoundManager.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import { AudioClip, AudioSource, director, Node } from 'cc';
  2. import { LOCAL_MUSIC_SWITCH, LOCAL_SOUND_SWITCH } from '@scripts/constants/Localkey';
  3. import { BUNDLE_ID } from '@scripts/define/ResDefine';
  4. import { localStorageGet, localStorageSet } from '@scripts/utils/Helper';
  5. import { ResourceManager } from '../resource/ResourceManager';
  6. const DEFAULT_SWITCH_VALUE = '1';
  7. export interface SoundConfig {
  8. path: string;
  9. bundle: string;
  10. }
  11. /** 新音频资源在此登记,播放时传入对应 key 即可。 */
  12. export const SOUND_CONFIGS: Record<string, SoundConfig> = {
  13. bgm: { path: 'sounds/bgm', bundle: BUNDLE_ID.Lobby },
  14. button: { path: 'sounds/button', bundle: BUNDLE_ID.Lobby },
  15. };
  16. /** 管理跨场景的背景音乐、音效及其本地开关。 */
  17. export class SoundManager {
  18. private readonly musicSource: AudioSource;
  19. private readonly soundSource: AudioSource;
  20. private readonly clips = new Map<string, AudioClip>();
  21. private _musicEnabled: boolean;
  22. private _soundEnabled: boolean;
  23. constructor(private readonly resources: ResourceManager) {
  24. this._musicEnabled = this.readSwitch(LOCAL_MUSIC_SWITCH);
  25. this._soundEnabled = this.readSwitch(LOCAL_SOUND_SWITCH);
  26. const root = new Node('SoundManager');
  27. const musicNode = new Node('Music');
  28. const soundNode = new Node('Sound');
  29. root.addChild(musicNode);
  30. root.addChild(soundNode);
  31. this.musicSource = musicNode.addComponent(AudioSource);
  32. this.musicSource.playOnAwake = false;
  33. this.soundSource = soundNode.addComponent(AudioSource);
  34. this.soundSource.playOnAwake = false;
  35. director.addPersistRootNode(root);
  36. }
  37. get musicEnabled(): boolean {
  38. return this._musicEnabled;
  39. }
  40. set musicEnabled(enabled: boolean) {
  41. this.setMusicEnabled(enabled);
  42. }
  43. get soundEnabled(): boolean {
  44. return this._soundEnabled;
  45. }
  46. set soundEnabled(enabled: boolean) {
  47. this.setSoundEnabled(enabled);
  48. }
  49. /**
  50. * 预加载指定音频;不传 key 时预加载全部已配置音频。
  51. */
  52. async preload(keys: string | readonly string[] = Object.keys(SOUND_CONFIGS)): Promise<void> {
  53. const soundKeys = typeof keys === 'string' ? [keys] : keys;
  54. await Promise.all(soundKeys.map((key) => this.loadClip(key)));
  55. }
  56. /** 播放背景音乐。关闭音乐时会记住当前曲目,重新开启后自动继续播放。 */
  57. async playMusic(key: string, loop = true, volume = 1): Promise<void> {
  58. const clip = await this.loadClip(key);
  59. if (!clip) return;
  60. const shouldPlay = this.musicSource.clip !== clip || !this.musicSource.playing;
  61. this.musicSource.clip = clip;
  62. this.musicSource.loop = loop;
  63. this.musicSource.volume = this.normalizeVolume(volume);
  64. if (this._musicEnabled && shouldPlay) {
  65. this.musicSource.play();
  66. }
  67. }
  68. /** 播放一次音效;允许同一音效重叠播放。 */
  69. async playSound(key: string, volume = 1): Promise<void> {
  70. if (!this._soundEnabled) return;
  71. const clip = await this.loadClip(key);
  72. if (!clip || !this._soundEnabled) return;
  73. this.soundSource.playOneShot(clip, this.normalizeVolume(volume));
  74. }
  75. setMusicEnabled(enabled: boolean): void {
  76. this._musicEnabled = Boolean(enabled);
  77. localStorageSet(LOCAL_MUSIC_SWITCH, this._musicEnabled ? '1' : '0');
  78. if (this._musicEnabled) {
  79. if (this.musicSource.clip) this.musicSource.play();
  80. } else {
  81. this.musicSource.pause();
  82. }
  83. }
  84. setSoundEnabled(enabled: boolean): void {
  85. this._soundEnabled = Boolean(enabled);
  86. localStorageSet(LOCAL_SOUND_SWITCH, this._soundEnabled ? '1' : '0');
  87. }
  88. private readSwitch(key: string): boolean {
  89. const value = String(localStorageGet(key, DEFAULT_SWITCH_VALUE)).toLowerCase();
  90. return value !== '0' && value !== 'false';
  91. }
  92. private async loadClip(key: string): Promise<AudioClip | null> {
  93. const cachedClip = this.clips.get(key);
  94. if (cachedClip?.isValid) return cachedClip;
  95. const config = SOUND_CONFIGS[key];
  96. if (!config) {
  97. console.warn(`[SoundManager] Unknown sound key: ${key}`);
  98. return null;
  99. }
  100. const clip = await this.resources.load(config.bundle, config.path, AudioClip);
  101. if (clip) this.clips.set(key, clip);
  102. return clip;
  103. }
  104. private normalizeVolume(volume: number): number {
  105. if (!Number.isFinite(volume)) return 1;
  106. return Math.min(1, Math.max(0, volume));
  107. }
  108. playBgm() {
  109. this.playMusic('bgm');
  110. }
  111. playBtnClick() {
  112. this.playSound("button")
  113. }
  114. }