| New file |
| | |
| | | using UnityEngine; |
| | | using System.Collections.Generic; |
| | | |
| | | /// <summary> |
| | | /// 战斗音效管理器 |
| | | /// 职责:播放战斗中的短促技能动作和特效音效 |
| | | /// </summary> |
| | | public class BattleSoundManager |
| | | { |
| | | private BattleField battleField; |
| | | private GameObject audioSourceObject; |
| | | |
| | | // 音效池配置 |
| | | private const int INITIAL_AUDIO_POOL = 15; // 初始池大小 |
| | | private const int MAX_AUDIO_SOURCES = 30; // AudioSource 总数上限 |
| | | private Queue<AudioSource> audioSourcePool = new Queue<AudioSource>(); |
| | | private List<AudioSource> activeAudioSources = new List<AudioSource>(); |
| | | |
| | | // 同一音效同时播放的最大数量 |
| | | private const int MAX_SAME_AUDIO_COUNT = 3; |
| | | // 记录每个音效ID当前播放的AudioSource |
| | | private Dictionary<int, List<AudioSource>> audioIdToSources = new Dictionary<int, List<AudioSource>>(); |
| | | |
| | | // 音频剪辑缓存 |
| | | private Dictionary<int, AudioClip> audioClipCache = new Dictionary<int, AudioClip>(); |
| | | |
| | | // 当前播放速度 |
| | | private float currentSpeedRatio = 1f; |
| | | |
| | | // 是否有焦点 |
| | | private bool hasFocus = true; |
| | | |
| | | public BattleSoundManager(BattleField _battleField) |
| | | { |
| | | battleField = _battleField; |
| | | InitializeAudioSources(); |
| | | |
| | | // 监听战场速度变化 |
| | | if (battleField != null) |
| | | { |
| | | battleField.OnSpeedRatioChange += OnSpeedRatioChanged; |
| | | battleField.OnFocusChange += OnFocusChanged; |
| | | currentSpeedRatio = battleField.speedRatio; |
| | | hasFocus = battleField.IsFocus(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 初始化音频源池 |
| | | /// </summary> |
| | | private void InitializeAudioSources() |
| | | { |
| | | // 使用战场根节点作为 AudioSource 的载体 |
| | | audioSourceObject = battleField.battleRootNode.gameObject; |
| | | |
| | | // 创建初始音频源池 |
| | | for (int i = 0; i < INITIAL_AUDIO_POOL; i++) |
| | | { |
| | | var source = audioSourceObject.AddComponent<AudioSource>(); |
| | | source.playOnAwake = false; |
| | | source.loop = false; |
| | | source.spatialBlend = 0f; // 2D音效 |
| | | audioSourcePool.Enqueue(source); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 每帧运行,清理已完成的音频源 |
| | | /// </summary> |
| | | public void Run() |
| | | { |
| | | // 每帧清理,性能开销很小 |
| | | CleanupFinishedAudioSources(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 预热音频(暂时为空,留给移动端优化) |
| | | /// </summary> |
| | | public void PrewarmAudio(params int[] audioIds) |
| | | { |
| | | // TODO: 移动端优化时实现 |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 预热音频(暂时为空,留给移动端优化) |
| | | /// </summary> |
| | | public void PrewarmAudio(List<int> audioIds) |
| | | { |
| | | // TODO: 移动端优化时实现 |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 播放技能音效 |
| | | /// </summary> |
| | | /// <param name="audioId">音效ID</param> |
| | | public void PlaySkillSound(int audioId) |
| | | { |
| | | if (audioId <= 0) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | PlaySound(audioId); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 播放特效音效 |
| | | /// </summary> |
| | | /// <param name="audioId">音效ID</param> |
| | | public void PlayEffectSound(int audioId) |
| | | { |
| | | if (audioId <= 0) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | PlaySound(audioId); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 核心播放方法 |
| | | /// </summary> |
| | | private void PlaySound(int audioId) |
| | | { |
| | | // 检查是否有焦点,无焦点时不播放 |
| | | if (!hasFocus) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // 检查该音效是否已达到播放上限 |
| | | if (!CanPlayAudio(audioId)) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | var audioClip = GetAudioClip(audioId); |
| | | if (audioClip == null) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // 从池中获取可用的音频源 |
| | | AudioSource source = GetAvailableAudioSource(); |
| | | if (source == null) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // 设置音量(使用音效音量设置) |
| | | source.volume = SystemSetting.Instance.GetSoundEffect(); |
| | | |
| | | // 设置播放速度,使用pitch来控制 |
| | | // pitch范围建议在0.5-2.0之间以避免失真 |
| | | float pitch = Mathf.Clamp(currentSpeedRatio, 0.5f, 2.0f); |
| | | source.pitch = pitch; |
| | | |
| | | // 播放音效 |
| | | source.PlayOneShot(audioClip); |
| | | |
| | | // 标记为活跃 |
| | | if (!activeAudioSources.Contains(source)) |
| | | { |
| | | activeAudioSources.Add(source); |
| | | } |
| | | |
| | | // 记录该音效ID与AudioSource的关联 |
| | | if (!audioIdToSources.ContainsKey(audioId)) |
| | | { |
| | | audioIdToSources[audioId] = new List<AudioSource>(); |
| | | } |
| | | audioIdToSources[audioId].Add(source); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取音频剪辑(优先从缓存获取) |
| | | /// </summary> |
| | | private AudioClip GetAudioClip(int audioId) |
| | | { |
| | | // 先从缓存中查找 |
| | | if (audioClipCache.TryGetValue(audioId, out AudioClip cachedClip)) |
| | | { |
| | | return cachedClip; |
| | | } |
| | | |
| | | // 缓存中没有,则加载并缓存 |
| | | var audioClip = LoadAudioClip(audioId); |
| | | if (audioClip != null) |
| | | { |
| | | audioClipCache[audioId] = audioClip; |
| | | } |
| | | return audioClip; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查是否可以播放该音效 |
| | | /// </summary> |
| | | private bool CanPlayAudio(int audioId) |
| | | { |
| | | if (!audioIdToSources.ContainsKey(audioId)) |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | // 过滤掉已经停止播放的AudioSource |
| | | var sources = audioIdToSources[audioId]; |
| | | sources.RemoveAll(s => s == null || !s.isPlaying); |
| | | |
| | | // 检查同时播放的数量 |
| | | return sources.Count < MAX_SAME_AUDIO_COUNT; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载音频剪辑 |
| | | /// </summary> |
| | | private AudioClip LoadAudioClip(int audioId) |
| | | { |
| | | var config = AudioConfig.Get(audioId); |
| | | if (config != null) |
| | | { |
| | | return AudioLoader.LoadAudio(config.Folder, config.Audio); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取可用的音频源 |
| | | /// </summary> |
| | | private AudioSource GetAvailableAudioSource() |
| | | { |
| | | // 尝试从池中获取 |
| | | if (audioSourcePool.Count > 0) |
| | | { |
| | | return audioSourcePool.Dequeue(); |
| | | } |
| | | |
| | | // 如果池为空,检查是否可以动态创建 |
| | | if (audioSourceObject == null) |
| | | { |
| | | Debug.LogError("BattleSoundManager: audioSourceObject 为空,无法创建 AudioSource"); |
| | | return null; |
| | | } |
| | | |
| | | // 计算当前总的 AudioSource 数量 |
| | | int totalAudioSources = audioSourceObject.GetComponents<AudioSource>().Length; |
| | | |
| | | if (totalAudioSources >= MAX_AUDIO_SOURCES) |
| | | { |
| | | // 达到上限,不再创建,丢弃这次播放请求 |
| | | Debug.LogWarning($"BattleSoundManager: AudioSource 数量已达上限 {MAX_AUDIO_SOURCES},无法播放新音效"); |
| | | return null; |
| | | } |
| | | |
| | | // 在 battleRootNode 上动态创建新的 AudioSource |
| | | var source = audioSourceObject.AddComponent<AudioSource>(); |
| | | source.playOnAwake = false; |
| | | source.loop = false; |
| | | source.spatialBlend = 0f; // 2D音效 |
| | | |
| | | return source; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 清理已播放完成的音频源 |
| | | /// </summary> |
| | | private void CleanupFinishedAudioSources() |
| | | { |
| | | // 清理 activeAudioSources 列表 |
| | | for (int i = activeAudioSources.Count - 1; i >= 0; i--) |
| | | { |
| | | var source = activeAudioSources[i]; |
| | | if (source == null || !source.isPlaying) |
| | | { |
| | | activeAudioSources.RemoveAt(i); |
| | | if (source != null) |
| | | { |
| | | audioSourcePool.Enqueue(source); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 清理 audioIdToSources 中已停止播放的 AudioSource |
| | | foreach (var kvp in audioIdToSources) |
| | | { |
| | | kvp.Value.RemoveAll(s => s == null || !s.isPlaying); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 战场速度变化回调 |
| | | /// </summary> |
| | | private void OnSpeedRatioChanged(float newSpeedRatio) |
| | | { |
| | | currentSpeedRatio = newSpeedRatio; |
| | | |
| | | // 更新所有正在播放的音效的速度 |
| | | foreach (var source in activeAudioSources) |
| | | { |
| | | if (source != null && source.isPlaying) |
| | | { |
| | | float pitch = Mathf.Clamp(newSpeedRatio, 0.5f, 2.0f); |
| | | source.pitch = pitch; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 焦点变化回调 |
| | | /// </summary> |
| | | private void OnFocusChanged(bool isFocus) |
| | | { |
| | | hasFocus = isFocus; |
| | | |
| | | // 失去焦点时,停止所有正在播放的音效 |
| | | if (!hasFocus) |
| | | { |
| | | StopAllSounds(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 停止所有音效 |
| | | /// </summary> |
| | | public void StopAllSounds() |
| | | { |
| | | foreach (var source in activeAudioSources) |
| | | { |
| | | if (source != null && source.isPlaying) |
| | | { |
| | | source.Stop(); |
| | | } |
| | | } |
| | | |
| | | // 清理并回收所有音频源 |
| | | foreach (var source in activeAudioSources) |
| | | { |
| | | if (source != null) |
| | | { |
| | | audioSourcePool.Enqueue(source); |
| | | } |
| | | } |
| | | activeAudioSources.Clear(); |
| | | audioIdToSources.Clear(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 设置音量 |
| | | /// </summary> |
| | | public void SetVolume(float volume) |
| | | { |
| | | volume = Mathf.Clamp01(volume); |
| | | foreach (var source in activeAudioSources) |
| | | { |
| | | if (source != null) |
| | | { |
| | | source.volume = volume; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 释放资源 |
| | | /// </summary> |
| | | public void Release() |
| | | { |
| | | if (battleField != null) |
| | | { |
| | | battleField.OnSpeedRatioChange -= OnSpeedRatioChanged; |
| | | battleField.OnFocusChange -= OnFocusChanged; |
| | | } |
| | | |
| | | StopAllSounds(); |
| | | |
| | | // 销毁所有 AudioSource 组件 |
| | | if (audioSourceObject != null) |
| | | { |
| | | var sources = audioSourceObject.GetComponents<AudioSource>(); |
| | | foreach (var source in sources) |
| | | { |
| | | if (source != null) |
| | | { |
| | | GameObject.Destroy(source); |
| | | } |
| | | } |
| | | audioSourceObject = null; |
| | | } |
| | | |
| | | audioSourcePool.Clear(); |
| | | activeAudioSources.Clear(); |
| | | audioIdToSources.Clear(); |
| | | audioClipCache.Clear(); |
| | | } |
| | | } |