250 lines
10 KiB
C#
250 lines
10 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.Audio;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Events;
|
||
|
||
namespace BaseGames.Audio
|
||
{
|
||
/// <summary>
|
||
/// 音频管理器。
|
||
/// 职责:BGM 双 Source 交叉淡入淡出、SFX 多源轮转池、AudioMixer 快照切换、音量控制。
|
||
/// 挂在 Persistent 场景 [AudioManager] GameObject 上。
|
||
/// </summary>
|
||
[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<string, AudioEventSO> _sfxLookup;
|
||
private readonly CompositeDisposable _subs = new();
|
||
|
||
private void Awake()
|
||
{
|
||
if (ServiceLocator.GetOrDefault<IAudioService>() != null) { Destroy(gameObject); return; }
|
||
|
||
Debug.Assert(_audioConfig != null, "[AudioManager] _audioConfig 未赋值,请在 Inspector 中指定 AudioConfigSO。", this);
|
||
|
||
_activeBGMSource = _bgmSourceA;
|
||
_inactiveBGMSource = _bgmSourceB;
|
||
|
||
ServiceLocator.Register<IAudioService>(this);
|
||
BuildSFXLookup();
|
||
Initialize();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onPlayerDied?.Subscribe(HandlePlayerDied).AddTo(_subs);
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
_subs.Clear();
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
ServiceLocator.Unregister<IAudioService>(this);
|
||
}
|
||
|
||
// ── IAudioService string-key API ────────────────────────────────────────────
|
||
/// <summary>按 Zone/Boss key 查 AudioConfigSO 播放 BGM。</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>按 key 查 SFX 注册表播放 AudioEventSO。</summary>
|
||
public void PlaySFX(string key)
|
||
{
|
||
if (_sfxLookup != null && _sfxLookup.TryGetValue(key, out var evt))
|
||
{
|
||
evt?.PlayOneShot(NextSFXSource());
|
||
return;
|
||
}
|
||
Debug.LogWarning($"[AudioManager] SFX key '{key}' 未在注册表中找到。");
|
||
}
|
||
|
||
// ── 音量控制 ─────────────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 将指定 Exposed Parameter 设置为 0-1 线性值(内部转换为 dB)。
|
||
/// 唯一音量写入入口(同时满足 IAudioService.SetVolume 接口)。
|
||
/// </summary>
|
||
public void SetVolume(string exposedParam, float linear)
|
||
=> _mixer.SetFloat(exposedParam, LinearToDecibel(linear));
|
||
|
||
/// <summary>读取 SettingsManager 已加载的设置数据并应用四路音量到 AudioMixer。</summary>
|
||
public void Initialize()
|
||
{
|
||
var settings = ServiceLocator.GetOrDefault<ISettingsService>();
|
||
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 ──────────────────────────────────────────────────────────────────
|
||
/// <summary>播放 BGM,使用双 AudioSource 交叉淡入淡出。</summary>
|
||
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));
|
||
}
|
||
|
||
/// <summary>停止 BGM(带淡出)。</summary>
|
||
public void StopBGM(float fadeDuration = 1f)
|
||
{
|
||
if (_crossfadeCoroutine != null) StopCoroutine(_crossfadeCoroutine);
|
||
_crossfadeCoroutine = StartCoroutine(FadeOutCoroutine(_activeBGMSource, fadeDuration));
|
||
}
|
||
|
||
// ── SFX ──────────────────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 一次性播放 SFX,使用轮转多源池避免高密度战斗时音效相互戳断。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>2D 游戏中位置无衰减,统一委托多源池播放。</summary>
|
||
public void PlaySFXAtPosition(AudioClip clip, Vector2 pos, float volumeScale = 1f)
|
||
{
|
||
if (clip == null) return;
|
||
AudioSource.PlayClipAtPoint(clip, pos, volumeScale);
|
||
}
|
||
|
||
// ── 快照切换 ─────────────────────────────────────────────────────────────
|
||
/// <summary>切换 AudioMixer 快照(如 Default / Paused / Dead / BossFight)。</summary>
|
||
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<string, AudioEventSO>(_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;
|
||
}
|
||
}
|