NLDClient-yudde/ProjectNLD/Assets/Code/Scripts/Framework/Audio/AudioStoppedPool.cs

295 lines
8.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using System;
using System.Collections.Generic;
using System.Linq;
using Framework;
using PhxhSDK;
/// <summary>
/// 音频停止池 - 使用 LRU 算法管理停止的 AudioPlayer
/// - 按 key 索引,提高复用率
/// - 分类型限制,防止内存泄漏
/// - LRU 淘汰,自动清理长期不用的 AudioPlayer
/// </summary>
public class AudioStoppedPool
{
#region Fields
// 按 key 索引(用于快速查找匹配的 AudioPlayer)
// key 格式: "音频类型_配置key",如 "Music_bgm_01"
private Dictionary<string, LinkedList<AudioPlayer>> _poolByKey;
// 按 ID 索引(用于快速查找和移除)
private Dictionary<int, AudioPlayer> _poolById;
// 当前各类型的数量
private Dictionary<EAudioType, int> _currentCount;
// 各类型的上限
private Dictionary<EAudioType, int> _maxLimit;
#endregion
#region Init
public AudioStoppedPool()
{
_poolByKey = new Dictionary<string, LinkedList<AudioPlayer>>();
_poolById = new Dictionary<int, AudioPlayer>();
_currentCount = new Dictionary<EAudioType, int>
{
{ EAudioType.Music, 0 },
{ EAudioType.Sound, 0 },
{ EAudioType.Speak, 0 }
};
_maxLimit = new Dictionary<EAudioType, int>
{
{ EAudioType.Music, 5 }, // Music 停止池上限 5
{ EAudioType.Sound, 50 }, // Sound 停止池上限 50
{ EAudioType.Speak, 3 } // Speak 停止池上限 3
};
}
#endregion
#region Public Methods
/// <summary>
/// 设置停止池上限
/// </summary>
public void SetLimit(EAudioType type, int limit)
{
_maxLimit[type] = limit;
}
/// <summary>
/// 添加 AudioPlayer 到停止池(带 LRU 淘汰)
/// <para>如果超过该类型的上限,自动淘汰最久未使用的 AudioPlayer</para>
/// </summary>
/// <param name="type">音频类型</param>
/// <param name="key">音频配置 key</param>
/// <param name="audio">要添加的 AudioPlayer</param>
/// <returns>被淘汰的 AudioPlayer 列表(需要调用方 Dispose)</returns>
public List<AudioPlayer> AddStopped(EAudioType type, string key, AudioPlayer audio)
{
if (audio == null || string.IsNullOrEmpty(key))
return new List<AudioPlayer>();
var poolKey = MakePoolKey(type, key);
// 1. 添加到 key 索引
if (!_poolByKey.TryGetValue(poolKey, out var list))
{
list = new LinkedList<AudioPlayer>();
_poolByKey[poolKey] = list;
}
list.AddFirst(audio); // 添加到链表头部(最近使用)
// 2. 添加到 ID 索引
_poolById[audio.AudioId] = audio;
_currentCount[type]++;
// 3. 检查是否超过上限,执行 LRU 淘汰
var toDestroy = new List<AudioPlayer>();
if (_currentCount[type] > _maxLimit[type])
{
int excessCount = _currentCount[type] - _maxLimit[type];
toDestroy = EvictOldest(type, excessCount);
}
return toDestroy;
}
/// <summary>
/// 尝试从停止池获取缓存的 AudioPlayer(精确匹配)
/// <para>只返回完全匹配的 audio(同类型 + 同 key),找不到返回 null</para>
/// <para>设计原则:停止池只用于精确复用,不跨 key 复用</para>
/// </summary>
/// <param name="type">音频类型</param>
/// <param name="key">音频配置 key</param>
/// <returns>找到返回缓存的 AudioPlayer,否则返回 null</returns>
public AudioPlayer TryGetCached(EAudioType type, string key)
{
if (string.IsNullOrEmpty(key))
return null;
var poolKey = MakePoolKey(type, key);
// 只返回精确匹配的 audio(同类型 + 同 key)
if (_poolByKey.TryGetValue(poolKey, out var list) && list.Count > 0)
{
var audio = list.First.Value;
RemoveFromPool(audio);
return audio;
}
// 找不到精确匹配,返回 null
// 让 AudioManager 决定是创建新的(播放池未满)还是复用最旧的(播放池已满)
return null;
}
/// <summary>
/// 通过 ID 查询停止池中的 AudioPlayer
/// </summary>
/// <param name="id">AudioPlayer ID</param>
/// <returns>找到返回 AudioPlayer,否则返回 null</returns>
public AudioPlayer GetById(int id)
{
return _poolById.TryGetValue(id, out var audio) ? audio : null;
}
/// <summary>
/// 通过 ID 从停止池移除 AudioPlayer
/// </summary>
/// <param name="id">AudioPlayer ID</param>
/// <returns>找到并移除返回 AudioPlayer,否则返回 null</returns>
public AudioPlayer RemoveById(int id)
{
if (!_poolById.TryGetValue(id, out var audio))
return null;
RemoveFromPool(audio);
return audio;
}
/// <summary>
/// 获取当前各类型的统计信息
/// </summary>
/// <returns>(Music数量, Sound数量, Speak数量)</returns>
public (int music, int sound, int speak) GetStats()
{
return (_currentCount[EAudioType.Music],
_currentCount[EAudioType.Sound],
_currentCount[EAudioType.Speak]);
}
/// <summary>
/// 清空停止池(不释放 AudioPlayer 资源)
/// </summary>
public void Clear()
{
_poolByKey.Clear();
_poolById.Clear();
ResetCounts();
}
/// <summary>
/// 释放所有 AudioPlayer 资源并清空停止池
/// <para>适用于场景卸载、关卡退出等需要彻底清理的场景</para>
/// </summary>
/// <returns>释放的 AudioPlayer 数量</returns>
public int DisposeAll()
{
int count = 0;
// 释放所有 AudioPlayer 资源
foreach (var list in _poolByKey.Values)
{
foreach (var audio in list)
{
audio.Dispose();
count++;
}
}
// 清空池子
_poolByKey.Clear();
_poolById.Clear();
ResetCounts();
return count;
}
#endregion
#region Private Methods
/// <summary>
/// 从停止池移除 AudioPlayer
/// </summary>
private void RemoveFromPool(AudioPlayer audio)
{
if (audio == null)
return;
// 从 ID 索引移除
_poolById.Remove(audio.AudioId);
// 从 key 索引移除
var poolKey = MakePoolKey(audio.AudioType, audio.CurrentPlayingKey);
if (_poolByKey.TryGetValue(poolKey, out var list))
{
list.Remove(audio);
if (list.Count == 0)
{
_poolByKey.Remove(poolKey);
}
}
// 更新计数
_currentCount[audio.AudioType]--;
}
/// <summary>
/// LRU 淘汰:移除指定类型中最久未使用的 N 个 AudioPlayer
/// <para>从各个 key 的链表尾部收集(尾部 = 最久未使用)</para>
/// </summary>
/// <param name="type">音频类型</param>
/// <param name="count">要淘汰的数量</param>
/// <returns>被淘汰的 AudioPlayer 列表</returns>
private List<AudioPlayer> EvictOldest(EAudioType type, int count)
{
var evicted = new List<AudioPlayer>(count);
var typePrefix = $"{type}_";
// 从所有匹配类型的 key 中收集最久未使用的 AudioPlayer
foreach (var kv in _poolByKey)
{
if (!kv.Key.StartsWith(typePrefix))
continue;
// 从链表尾部开始收集(尾部 = 最久未使用)
var node = kv.Value.Last;
while (node != null && evicted.Count < count)
{
evicted.Add(node.Value);
node = node.Previous;
}
// 已收集足够数量,提前退出
if (evicted.Count >= count)
break;
}
// 从池中移除
foreach (var audio in evicted)
{
RemoveFromPool(audio);
}
return evicted;
}
/// <summary>
/// 生成池 key(格式:类型_配置key)
/// </summary>
private string MakePoolKey(EAudioType type, string key)
{
return $"{type}_{key}";
}
/// <summary>
/// 重置所有类型的计数
/// </summary>
private void ResetCounts()
{
_currentCount[EAudioType.Music] = 0;
_currentCount[EAudioType.Sound] = 0;
_currentCount[EAudioType.Speak] = 0;
}
#endregion
}