using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Audio;
public class SimpleAudioManager : MonoBehaviour
{
public static SimpleAudioManager Instance { get; private set; }
[Header("Settings")]
[Tooltip("同时存在的最大声音数量")]
public int poolSize = 32;
public AudioMixerGroup sfxMixerGroup;
public AudioMixerGroup musicMixerGroup;
[Header("Music")]
public AudioSource musicSource;
// 简单的音效配置类 (实际项目中建议用 ScriptableObject 管理)
[System.Serializable]
public class SoundProfile
{
public string id; // 音效ID (如 "Attack_Sword")
public AudioClip clip;
[Range(0f, 1f)] public float volume = 1f;
[Range(0.1f, 3f)] public float pitch = 1f;
[Range(0f, 0.5f)] public float randomPitch = 0.1f; // 音调随机范围 +/-
public float cooldown = 0.1f; // 最小播放间隔 (秒)
public bool is3D = false; // 是否为 3D 音效
}
[Header("Library")]
public List<SoundProfile> soundLibrary; // 在 Inspector 里配置所有音效
// 运行时数据
private Dictionary<string, SoundProfile> _libraryLookup;
private Dictionary<string, float> _lastPlayedTimes;
private List<AudioSource> _sourcePool;
private int _poolIndex = 0;
void Awake()
{
// 单例初始化
if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); }
else { Destroy(gameObject); return; }
InitializeLibrary();
InitializePool();
}
private void InitializeLibrary()
{
_libraryLookup = new Dictionary<string, SoundProfile>();
_lastPlayedTimes = new Dictionary<string, float>();
foreach (var s in soundLibrary)
{
if (!_libraryLookup.ContainsKey(s.id))
{
_libraryLookup.Add(s.id, s);
_lastPlayedTimes.Add(s.id, 0f);
}
}
}
private void InitializePool()
{
_sourcePool = new List<AudioSource>();
GameObject poolRoot = new GameObject("SFX_Pool");
poolRoot.transform.SetParent(transform);
for (int i = 0; i < poolSize; i++)
{
GameObject obj = new GameObject($"SFX_Source_{i}");
obj.transform.SetParent(poolRoot.transform);
AudioSource src = obj.AddComponent<AudioSource>();
src.playOnAwake = false;
src.outputAudioMixerGroup = sfxMixerGroup;
_sourcePool.Add(src);
}
}
/// <summary>
/// 播放音效的核心方法
/// </summary>
/// <param name="id">音效配置ID</param>
/// <param name="position">世界坐标 (如果是 null 则为 2D 声音)</param>
public void PlaySFX(string id, Vector3? position = null)
{
if (!_libraryLookup.TryGetValue(id, out SoundProfile profile))
{
Debug.LogWarning($"Audio: Sound ID '{id}' not found!");
return;
}
// 1. 冷却检测 (Throttling)
if (Time.time < _lastPlayedTimes[id] + profile.cooldown) return;
_lastPlayedTimes[id] = Time.time;
// 2. 获取对象池中的 Source
AudioSource source = GetNextSource();
// 3. 应用配置
source.clip = profile.clip;
// 随机化处理
float finalPitch = profile.pitch + Random.Range(-profile.randomPitch, profile.randomPitch);
// 防止 Pitch 变成负数或 0
source.pitch = Mathf.Max(0.1f, finalPitch);
source.volume = profile.volume;
// 4. 空间化处理
if (position.HasValue && profile.is3D)
{
source.spatialBlend = 1f; // 3D
source.transform.position = position.Value;
// 简单的衰减设置
source.minDistance = 1f;
source.maxDistance = 20f;
}
else
{
source.spatialBlend = 0f; // 2D
}
// 5. 播放
source.Play();
}
private AudioSource GetNextSource()
{
// 简单的循环覆盖策略 (Round Robin)
// 如果池子满了,会强行切断最久远的那个声音
AudioSource src = _sourcePool[_poolIndex];
_poolIndex = (_poolIndex + 1) % poolSize;
return src;
}
// 背景音乐淡入淡出 (简单版)
public void PlayMusic(AudioClip clip, float fadeDuration = 1f)
{
if (musicSource.clip == clip) return;
// 此处省略 Coroutine 淡入淡出逻辑,实际项目中建议加上 Tween
musicSource.clip = clip;
musicSource.Play();
}
}