using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio; using BaseGames.Core; using BaseGames.Core.Events; namespace BaseGames.Audio { /// /// 音频管理器。 /// 职责:BGM 双 Source 交叉淡入淡出、SFX 多源轮转池、AudioMixer 快照切换、音量控制。 /// 挂在 Persistent 场景 [AudioManager] GameObject 上。 /// [DefaultExecutionOrder(-500)] public class AudioManager : MonoBehaviour, IAudioService { [Header("AudioMixer")] [SerializeField] private AudioMixer _mixer; [Header("BGM Sources(双 Source 交叉淡入淡出)")] [SerializeField] private AudioSource _bgmSourceA; [SerializeField] private AudioSource _bgmSourceB; [Header("SFX Pool(建议 6 个,均路由到 SFX MixerGroup)")] [Tooltip("轮转池大小:同帧触发数超过此值时最旧的音效会被打断。建议 6~8 个。")] [SerializeField] private AudioSource[] _sfxSources; [Header("Audio Config(BGM 映射)")] [SerializeField] private AudioConfigSO _audioConfig; [Header("SFX 注册表(key → AudioEventSO)")] [SerializeField] private AudioEventEntry[] _sfxRegistry; [System.Serializable] public struct AudioEventEntry { public string Key; public AudioEventSO Event; } [Header("Event Channels - Subscribe")] [SerializeField] private VoidEventChannelSO _onPlayerDied; private AudioSource _activeBGMSource; private AudioSource _inactiveBGMSource; private Coroutine _crossfadeCoroutine; private int _sfxRoundRobin; private Dictionary _sfxLookup; private readonly CompositeDisposable _subs = new(); private void Awake() { if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } Debug.Assert(_audioConfig != null, "[AudioManager] _audioConfig 未赋值,请在 Inspector 中指定 AudioConfigSO。", this); _activeBGMSource = _bgmSourceA; _inactiveBGMSource = _bgmSourceB; ServiceLocator.Register(this); BuildSFXLookup(); Initialize(); } private void OnEnable() { _onPlayerDied?.Subscribe(HandlePlayerDied).AddTo(_subs); } private void OnDisable() { _subs.Clear(); } private void OnDestroy() { ServiceLocator.Unregister(this); } // ── IAudioService string-key API ──────────────────────────────────────────── /// 按 Zone/Boss key 查 AudioConfigSO 播放 BGM。 public void PlayBGM(string key) { var clip = _audioConfig.GetZoneBGM(key) ?? _audioConfig.GetBossBGM(key); if (clip == null) { Debug.LogWarning($"[AudioManager] BGM key '{key}' 在 AudioConfigSO 中未找到。"); return; } PlayBGM(clip); } /// 按 key 查 SFX 注册表播放 AudioEventSO。 public void PlaySFX(string key) { if (_sfxLookup != null && _sfxLookup.TryGetValue(key, out var evt)) { evt?.PlayOneShot(NextSFXSource()); return; } Debug.LogWarning($"[AudioManager] SFX key '{key}' 未在注册表中找到。"); } // ── 音量控制 ───────────────────────────────────────────────────────────── /// /// 将指定 Exposed Parameter 设置为 0-1 线性值(内部转换为 dB)。 /// 唯一音量写入入口(同时满足 IAudioService.SetVolume 接口)。 /// public void SetVolume(string exposedParam, float linear) => _mixer.SetFloat(exposedParam, LinearToDecibel(linear)); /// 读取 SettingsManager 已加载的设置数据并应用四路音量到 AudioMixer。 public void Initialize() { var settings = ServiceLocator.GetOrDefault(); GlobalSettingsData data = settings?.Current ?? new GlobalSettingsData(); SetVolume(AudioMixerKeys.Master, data.MasterVolume); SetVolume(AudioMixerKeys.BGM, data.BGMVolume); SetVolume(AudioMixerKeys.SFX, data.SFXVolume); SetVolume(AudioMixerKeys.Ambient, data.AmbientVolume); } // ── BGM ────────────────────────────────────────────────────────────────── /// 播放 BGM,使用双 AudioSource 交叉淡入淡出。 public void PlayBGM(AudioClip clip, float fadeOutDur = 1f, float fadeInDur = 1f) { if (clip == null) return; if (_crossfadeCoroutine != null) StopCoroutine(_crossfadeCoroutine); _crossfadeCoroutine = StartCoroutine(CrossfadeCoroutine(clip, fadeOutDur, fadeInDur)); } /// 停止 BGM(带淡出)。 public void StopBGM(float fadeDuration = 1f) { if (_crossfadeCoroutine != null) StopCoroutine(_crossfadeCoroutine); _crossfadeCoroutine = StartCoroutine(FadeOutCoroutine(_activeBGMSource, fadeDuration)); } // ── SFX ────────────────────────────────────────────────────────────────── /// /// 一次性播放 SFX,使用轮转多源池避免高密度战斗时音效相互戳断。 /// public void PlaySFX(AudioClip clip, float volumeScale = 1f) { if (clip == null) return; var src = NextSFXSource(); if (src == null) return; src.volume = volumeScale; src.PlayOneShot(clip); } /// 2D 游戏中位置无衰减,统一委托多源池播放。 public void PlaySFXAtPosition(AudioClip clip, Vector2 pos, float volumeScale = 1f) { if (clip == null) return; AudioSource.PlayClipAtPoint(clip, pos, volumeScale); } // ── 快照切换 ───────────────────────────────────────────────────────────── /// 切换 AudioMixer 快照(如 Default / Paused / Dead / BossFight)。 public void TransitionToSnapshot(string snapshotName, float transitionTime = 0.5f) { var snapshot = _mixer.FindSnapshot(snapshotName); if (snapshot != null) snapshot.TransitionTo(transitionTime); else Debug.LogWarning($"[AudioManager] Snapshot '{snapshotName}' not found in mixer."); } // ── 内部实现 ───────────────────────────────────────────────────────────── private void HandlePlayerDied() { TransitionToSnapshot("Dead", 1.5f); } private void BuildSFXLookup() { _sfxLookup = new Dictionary(_sfxRegistry?.Length ?? 0); if (_sfxRegistry == null) return; foreach (var entry in _sfxRegistry) if (!string.IsNullOrEmpty(entry.Key) && entry.Event != null) _sfxLookup[entry.Key] = entry.Event; } private AudioSource NextSFXSource() { if (_sfxSources == null || _sfxSources.Length == 0) { Debug.LogError("[AudioManager] SFX Source 池为空,请在 Inspector 中为 _sfxSources 赋值。"); return null; } return _sfxSources[_sfxRoundRobin++ % _sfxSources.Length]; } private IEnumerator CrossfadeCoroutine(AudioClip newClip, float fadeOutDur, float fadeInDur) { // 淡出当前活跃 Source float startVolume = _activeBGMSource.volume; float elapsed = 0f; while (elapsed < fadeOutDur) { elapsed += Time.unscaledDeltaTime; _activeBGMSource.volume = Mathf.Lerp(startVolume, 0f, elapsed / fadeOutDur); yield return null; } _activeBGMSource.Stop(); _activeBGMSource.volume = 0f; // 切换到非活跃 Source 播放新曲目 var temp = _activeBGMSource; _activeBGMSource = _inactiveBGMSource; _inactiveBGMSource = temp; _activeBGMSource.clip = newClip; _activeBGMSource.volume = 0f; _activeBGMSource.Play(); // 淡入新 Source elapsed = 0f; while (elapsed < fadeInDur) { elapsed += Time.unscaledDeltaTime; _activeBGMSource.volume = Mathf.Lerp(0f, 1f, elapsed / fadeInDur); yield return null; } _activeBGMSource.volume = 1f; _crossfadeCoroutine = null; } private IEnumerator FadeOutCoroutine(AudioSource source, float duration) { float startVolume = source.volume; float elapsed = 0f; while (elapsed < duration) { elapsed += Time.unscaledDeltaTime; source.volume = Mathf.Lerp(startVolume, 0f, elapsed / duration); yield return null; } source.Stop(); source.volume = 0f; _crossfadeCoroutine = null; } private static float LinearToDecibel(float linear) => linear > 0.0001f ? 20f * Mathf.Log10(linear) : -80f; } }