Files
zeling_v2/Assets/Scripts/Audio/AudioManager.cs
2026-05-08 11:04:00 +08:00

201 lines
8.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
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")]
[SerializeField] private AudioSource[] _sfxSources;
[Header("Event Channels - Subscribe")]
[SerializeField] private VoidEventChannelSO _onPlayerDied;
private AudioSource _activeBGMSource;
private AudioSource _inactiveBGMSource;
private Coroutine _crossfadeCoroutine;
private int _sfxRoundRobin;
// ── 遗留单例(已废弃;新代码请使用 ServiceLocator.Get<IAudioService>())────────────
[System.Obsolete("Use ServiceLocator.Get<IAudioService>() instead.")]
public static AudioManager Instance { get; private set; }
private void Awake()
{
#pragma warning disable CS0618
if (Instance != null) { Destroy(gameObject); return; }
Instance = this;
#pragma warning restore CS0618
_activeBGMSource = _bgmSourceA;
_inactiveBGMSource = _bgmSourceB;
// ServiceLocator 注册(覆盖 GameServiceRegistrar 的 NullAudioService 兜底)
ServiceLocator.Register<IAudioService>(this);
}
private void OnEnable()
{
if (_onPlayerDied != null)
_onPlayerDied.OnEventRaised += HandlePlayerDied;
}
private void OnDisable()
{
if (_onPlayerDied != null)
_onPlayerDied.OnEventRaised -= HandlePlayerDied;
}
// ── IAudioService string-key APIPhase 2 接入 AudioEventSO 后完整实现)─────────────
/// <summary>
/// 按 Addressable key 播放 BGM。Phase 2 接入 AudioEventSO 前为占位警告。
/// </summary>
public void PlayBGM(string key)
=> Debug.LogWarning($"[AudioManager] PlayBGM(key) 尚未接入 AudioEventSOPhase 2。key={key}");
/// <summary>
/// 按 Addressable key 播放 SFX。Phase 2 接入 AudioEventSO 前为占位警告。
/// </summary>
public void PlaySFX(string key)
=> Debug.LogWarning($"[AudioManager] PlaySFX(key) 尚未接入 AudioEventSOPhase 2。key={key}");
// ── 音量控制 ─────────────────────────────────────────────────────────────
/// <summary>
/// 将指定 Exposed Parameter 设置为 0-1 线性值(内部转换为 dB
/// 唯一音量写入入口(同时满足 IAudioService.SetVolume 接口)。
/// </summary>
public void SetVolume(string exposedParam, float linear)
=> _mixer.SetFloat(exposedParam, LinearToDecibel(linear));
/// <summary>读取 GlobalSettings 并应用所有音量初始值。</summary>
public void Initialize()
{
// TODO: 从 SettingsManager / PlayerPrefs 读取保存的音量值并应用
}
// ── 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();
src.volume = volumeScale;
src.PlayOneShot(clip);
}
/// <summary>2D 游戏中位置无衰减,统一委托多源池播放。</summary>
public void PlaySFXAtPosition(AudioClip clip, Vector2 pos, float volumeScale = 1f)
=> PlaySFX(clip, 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 AudioSource NextSFXSource()
{
if (_sfxSources == null || _sfxSources.Length == 0) return _bgmSourceA;
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;
}
}