409 lines
14 KiB
C#
409 lines
14 KiB
C#
|
|
using System.Collections.Generic;
|
|||
|
|
using UnityEngine;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 音频播放池 - 动态大小(有上限),只包含活跃音频(播放中/暂停)
|
|||
|
|
/// <para>职责:</para>
|
|||
|
|
/// <para>- 按类型管理正在播放/暂停的 AudioPlayer(Music/Sound/Speak)</para>
|
|||
|
|
/// <para>- 动态上限,超出时停止最早的(移到停止池)</para>
|
|||
|
|
/// <para>- 追踪播放顺序(用于 LRU 淘汰)</para>
|
|||
|
|
/// <para>- 提供高效的查询、添加、移除操作</para>
|
|||
|
|
/// <para>- 已停止的音频会移到停止池,不会留在播放池</para>
|
|||
|
|
/// </summary>
|
|||
|
|
public class AudioPlayingPool
|
|||
|
|
{
|
|||
|
|
#region Fields
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 按类型分字典存储正在播放的 AudioPlayer
|
|||
|
|
/// Key: AudioType (Music/Sound/Speak)
|
|||
|
|
/// Value: Dictionary of AudioId -> AudioPlayer
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly Dictionary<PhxhSDK.EAudioType, Dictionary<int, AudioPlayer>> _audioByType;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 播放顺序追踪(用于找到最早播放的)
|
|||
|
|
/// Key: AudioType
|
|||
|
|
/// Value: Queue of AudioId (先进先出,队首是最早的)
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly Dictionary<PhxhSDK.EAudioType, Queue<int>> _playOrderByType;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 各类型的播放上限
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly Dictionary<PhxhSDK.EAudioType, int> _maxLimit;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 临时容器(复用,避免频繁 GC)
|
|||
|
|
/// 用于批量操作时复制集合,避免遍历时修改集合导致异常
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly List<AudioPlayer> _tempAudioList;
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Init
|
|||
|
|
|
|||
|
|
public AudioPlayingPool()
|
|||
|
|
{
|
|||
|
|
// 初始化三种类型的字典
|
|||
|
|
_audioByType = new Dictionary<PhxhSDK.EAudioType, Dictionary<int, AudioPlayer>>
|
|||
|
|
{
|
|||
|
|
{ PhxhSDK.EAudioType.Music, new Dictionary<int, AudioPlayer>() },
|
|||
|
|
{ PhxhSDK.EAudioType.Sound, new Dictionary<int, AudioPlayer>() },
|
|||
|
|
{ PhxhSDK.EAudioType.Speak, new Dictionary<int, AudioPlayer>() }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 初始化播放顺序队列
|
|||
|
|
_playOrderByType = new Dictionary<PhxhSDK.EAudioType, Queue<int>>
|
|||
|
|
{
|
|||
|
|
{ PhxhSDK.EAudioType.Music, new Queue<int>() },
|
|||
|
|
{ PhxhSDK.EAudioType.Sound, new Queue<int>() },
|
|||
|
|
{ PhxhSDK.EAudioType.Speak, new Queue<int>() }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 初始化默认上限
|
|||
|
|
_maxLimit = new Dictionary<PhxhSDK.EAudioType, int>
|
|||
|
|
{
|
|||
|
|
{ PhxhSDK.EAudioType.Music, AudioConstant.MusicPlayingLimit }, // 默认 1
|
|||
|
|
{ PhxhSDK.EAudioType.Sound, AudioConstant.SoundPlayingLimit }, // 默认 100
|
|||
|
|
{ PhxhSDK.EAudioType.Speak, AudioConstant.SpeakPlayingLimit } // 默认 1
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 初始化临时容器(预分配容量,避免扩容)
|
|||
|
|
// 最大容量 = Music(1) + Sound(100) + Speak(1) = 102
|
|||
|
|
_tempAudioList = new List<AudioPlayer>(capacity: 128);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Public Methods - Configuration
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 设置播放池上限
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="type">音频类型</param>
|
|||
|
|
/// <param name="limit">上限数量</param>
|
|||
|
|
public void SetLimit(PhxhSDK.EAudioType type, int limit)
|
|||
|
|
{
|
|||
|
|
if (limit < 0)
|
|||
|
|
{
|
|||
|
|
Debug.LogWarning($"[AudioPlayingPool] Invalid limit {limit} for {type}, using 0");
|
|||
|
|
limit = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_maxLimit[type] = limit;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取播放池上限
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetLimit(PhxhSDK.EAudioType type)
|
|||
|
|
{
|
|||
|
|
return _maxLimit[type];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Public Methods - Pool Management
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 检查播放池是否已满
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="type">音频类型</param>
|
|||
|
|
/// <returns>是否已满</returns>
|
|||
|
|
public bool IsFull(PhxhSDK.EAudioType type)
|
|||
|
|
{
|
|||
|
|
return _audioByType[type].Count >= _maxLimit[type];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 添加到播放池
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="audio">要添加的 AudioPlayer</param>
|
|||
|
|
public void Add(AudioPlayer audio)
|
|||
|
|
{
|
|||
|
|
if (audio == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogWarning("[AudioPlayingPool] Cannot add null audio");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var type = audio.AudioType;
|
|||
|
|
var id = audio.AudioId;
|
|||
|
|
|
|||
|
|
// 检查是否已存在
|
|||
|
|
if (_audioByType[type].ContainsKey(id))
|
|||
|
|
{
|
|||
|
|
// 已存在,只需更新播放顺序
|
|||
|
|
// 注意:这里不移除旧的顺序,因为 Queue 不支持高效移除
|
|||
|
|
// 在 GetOldest 时会处理重复的情况
|
|||
|
|
_playOrderByType[type].Enqueue(id);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加入字典
|
|||
|
|
_audioByType[type][id] = audio;
|
|||
|
|
|
|||
|
|
// 记录播放顺序
|
|||
|
|
_playOrderByType[type].Enqueue(id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 从播放池移除
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="id">AudioPlayer 的 ID</param>
|
|||
|
|
/// <returns>是否成功移除</returns>
|
|||
|
|
public bool Remove(int id)
|
|||
|
|
{
|
|||
|
|
foreach (var typeDict in _audioByType.Values)
|
|||
|
|
{
|
|||
|
|
if (typeDict.Remove(id))
|
|||
|
|
{
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取最早播放的 AudioPlayer(用于复用)
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="type">音频类型</param>
|
|||
|
|
/// <returns>最早的 AudioPlayer,如果池为空则返回 null</returns>
|
|||
|
|
public AudioPlayer GetOldest(PhxhSDK.EAudioType type)
|
|||
|
|
{
|
|||
|
|
var orderQueue = _playOrderByType[type];
|
|||
|
|
var audioDict = _audioByType[type];
|
|||
|
|
|
|||
|
|
if (orderQueue.Count == 0)
|
|||
|
|
return null;
|
|||
|
|
|
|||
|
|
// 循环查找最早的有效 AudioPlayer
|
|||
|
|
// (因为 Queue 中可能有重复或已移除的 ID)
|
|||
|
|
while (orderQueue.Count > 0)
|
|||
|
|
{
|
|||
|
|
int oldestId = orderQueue.Dequeue();
|
|||
|
|
|
|||
|
|
if (audioDict.TryGetValue(oldestId, out var audio))
|
|||
|
|
{
|
|||
|
|
return audio;
|
|||
|
|
}
|
|||
|
|
// 如果 ID 已不在字典中,继续查找下一个
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Public Methods - Query
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 通过 ID 获取 AudioPlayer
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="id">AudioPlayer 的 ID</param>
|
|||
|
|
/// <returns>找到的 AudioPlayer,未找到则返回 null</returns>
|
|||
|
|
public AudioPlayer GetById(int id)
|
|||
|
|
{
|
|||
|
|
foreach (var typeDict in _audioByType.Values)
|
|||
|
|
{
|
|||
|
|
if (typeDict.TryGetValue(id, out var audio))
|
|||
|
|
return audio;
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 检查 AudioPlayer 是否在播放池中
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="id">AudioPlayer 的 ID</param>
|
|||
|
|
/// <returns>是否存在</returns>
|
|||
|
|
public bool Contains(int id)
|
|||
|
|
{
|
|||
|
|
foreach (var typeDict in _audioByType.Values)
|
|||
|
|
{
|
|||
|
|
if (typeDict.ContainsKey(id))
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取指定类型的播放中数量
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetCount(PhxhSDK.EAudioType type)
|
|||
|
|
{
|
|||
|
|
return _audioByType[type].Count;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取统计信息
|
|||
|
|
/// </summary>
|
|||
|
|
/// <returns>(Music 数量, Sound 数量, Speak 数量)</returns>
|
|||
|
|
public (int music, int sound, int speak) GetStats()
|
|||
|
|
{
|
|||
|
|
return (_audioByType[PhxhSDK.EAudioType.Music].Count,
|
|||
|
|
_audioByType[PhxhSDK.EAudioType.Sound].Count,
|
|||
|
|
_audioByType[PhxhSDK.EAudioType.Speak].Count);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Public Methods - Batch Operations
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 停止某类型的所有音频
|
|||
|
|
/// 注意:使用复用容器,避免 Stop() 触发事件 → MoveToStoppedPool → 修改集合导致异常
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="type">音频类型</param>
|
|||
|
|
public void StopAllOfType(PhxhSDK.EAudioType type)
|
|||
|
|
{
|
|||
|
|
var audioDict = _audioByType[type];
|
|||
|
|
|
|||
|
|
// ✅ 使用复用容器,避免 GC
|
|||
|
|
_tempAudioList.Clear();
|
|||
|
|
_tempAudioList.AddRange(audioDict.Values);
|
|||
|
|
|
|||
|
|
foreach (var audio in _tempAudioList)
|
|||
|
|
{
|
|||
|
|
audio.Stop(); // Stop 触发 OnStopped 事件 → OnAudioStopped → MoveToStoppedPool
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 暂停某类型的所有音频
|
|||
|
|
/// 注意:使用复用容器,避免潜在的集合修改
|
|||
|
|
/// </summary>
|
|||
|
|
public void PauseAllOfType(PhxhSDK.EAudioType type)
|
|||
|
|
{
|
|||
|
|
var audioDict = _audioByType[type];
|
|||
|
|
|
|||
|
|
// ✅ 使用复用容器,避免 GC
|
|||
|
|
_tempAudioList.Clear();
|
|||
|
|
_tempAudioList.AddRange(audioDict.Values);
|
|||
|
|
|
|||
|
|
foreach (var audio in _tempAudioList)
|
|||
|
|
{
|
|||
|
|
audio.Pause();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 恢复某类型的所有音频
|
|||
|
|
/// 注意:使用复用容器,避免潜在的集合修改
|
|||
|
|
/// </summary>
|
|||
|
|
public void ResumeAllOfType(PhxhSDK.EAudioType type)
|
|||
|
|
{
|
|||
|
|
var audioDict = _audioByType[type];
|
|||
|
|
|
|||
|
|
// ✅ 使用复用容器,避免 GC
|
|||
|
|
_tempAudioList.Clear();
|
|||
|
|
_tempAudioList.AddRange(audioDict.Values);
|
|||
|
|
|
|||
|
|
foreach (var audio in _tempAudioList)
|
|||
|
|
{
|
|||
|
|
audio.Resume();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 停止所有音频
|
|||
|
|
/// 注意:使用复用容器,避免 Stop() 触发事件 → MoveToStoppedPool → 修改集合导致异常
|
|||
|
|
/// </summary>
|
|||
|
|
public void StopAll()
|
|||
|
|
{
|
|||
|
|
// ✅ 使用复用容器收集所有 AudioPlayer
|
|||
|
|
_tempAudioList.Clear();
|
|||
|
|
foreach (var typeDict in _audioByType.Values)
|
|||
|
|
{
|
|||
|
|
_tempAudioList.AddRange(typeDict.Values);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
foreach (var audio in _tempAudioList)
|
|||
|
|
{
|
|||
|
|
audio.Stop(); // Stop 触发 OnStopped 事件 → OnAudioStopped → MoveToStoppedPool
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取指定类型的所有 AudioPlayer(用于遍历)
|
|||
|
|
/// 注意:返回副本,避免外部在遍历时修改集合导致异常
|
|||
|
|
/// 说明:此方法调用频率低,每次创建新 List 可接受
|
|||
|
|
/// </summary>
|
|||
|
|
public IEnumerable<AudioPlayer> GetAllOfType(PhxhSDK.EAudioType type)
|
|||
|
|
{
|
|||
|
|
// ⚠️ 不能使用 _tempAudioList(会在下次调用时被 Clear)
|
|||
|
|
// ✅ 返回新副本,避免外部在遍历时 Stop 导致集合修改
|
|||
|
|
return new List<AudioPlayer>(_audioByType[type].Values);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 更新所有播放中的音频
|
|||
|
|
/// 注意:使用复用容器,避免 Update 中音频自动停止时修改集合
|
|||
|
|
/// 性能关键:此方法每帧调用,必须使用复用容器
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="dt">Delta time</param>
|
|||
|
|
public void Update(float dt)
|
|||
|
|
{
|
|||
|
|
// ✅ 使用复用容器收集所有 AudioPlayer(每帧调用,减少 GC)
|
|||
|
|
_tempAudioList.Clear();
|
|||
|
|
foreach (var typeDict in _audioByType.Values)
|
|||
|
|
{
|
|||
|
|
_tempAudioList.AddRange(typeDict.Values);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
foreach (var audio in _tempAudioList)
|
|||
|
|
{
|
|||
|
|
audio.Update(dt); // Update 可能导致音频自动停止,触发 OnStopped 事件
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 清空播放池(不销毁 AudioPlayer)
|
|||
|
|
/// </summary>
|
|||
|
|
public void Clear()
|
|||
|
|
{
|
|||
|
|
foreach (var typeDict in _audioByType.Values)
|
|||
|
|
{
|
|||
|
|
typeDict.Clear();
|
|||
|
|
}
|
|||
|
|
foreach (var queue in _playOrderByType.Values)
|
|||
|
|
{
|
|||
|
|
queue.Clear();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 释放所有 AudioPlayer 资源并清空播放池
|
|||
|
|
/// <para>适用于场景卸载、关卡退出等需要彻底清理的场景</para>
|
|||
|
|
/// </summary>
|
|||
|
|
/// <returns>释放的 AudioPlayer 数量</returns>
|
|||
|
|
public int DisposeAll()
|
|||
|
|
{
|
|||
|
|
int count = 0;
|
|||
|
|
|
|||
|
|
// 遍历所有类型的音频
|
|||
|
|
foreach (var typeDict in _audioByType.Values)
|
|||
|
|
{
|
|||
|
|
// 释放每个 AudioPlayer
|
|||
|
|
foreach (var audio in typeDict.Values)
|
|||
|
|
{
|
|||
|
|
audio.Dispose();
|
|||
|
|
count++;
|
|||
|
|
}
|
|||
|
|
typeDict.Clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清空播放顺序队列
|
|||
|
|
foreach (var queue in _playOrderByType.Values)
|
|||
|
|
{
|
|||
|
|
queue.Clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return count;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|
|||
|
|
|