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;
}
}