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

413 lines
13 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;
using UnityEngine;
/// <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;
// 记录每个 AudioPlayer 进入停止池的时间戳(毫秒)
private Dictionary<int, long> _stoppedTimeById;
// 各类型的超时时间(秒)- 长时间未使用则释放
private Dictionary<EAudioType, float> _timeoutByType;
// 超时检查定时器(降低检查频率)
private float _checkTimer;
// 临时列表(复用,避免频繁分配)
private readonly List<int> _tempKeysToRemove;
#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
};
_stoppedTimeById = new Dictionary<int, long>();
_timeoutByType = new Dictionary<EAudioType, float>
{
{ EAudioType.Music, AudioConstant.MusicStoppedTimeout }, // 60 秒
{ EAudioType.Sound, AudioConstant.SoundStoppedTimeout }, // 30 秒
{ EAudioType.Speak, AudioConstant.SpeakStoppedTimeout } // 20 秒
};
_checkTimer = 0f;
_tempKeysToRemove = new List<int>();
}
#endregion
#region Public Methods
/// <summary>
/// 设置停止池上限
/// </summary>
public void SetLimit(EAudioType type, int limit)
{
_maxLimit[type] = limit;
}
/// <summary>
/// 设置停止池超时时间(秒)
/// <para>长时间未使用的音频会在超时后自动释放</para>
/// </summary>
/// <param name="type">音频类型</param>
/// <param name="timeout">超时时间(秒)</param>
public void SetTimeout(EAudioType type, float timeout)
{
if (timeout < 0)
{
Debug.LogWarning($"[AudioStoppedPool] Invalid timeout {timeout} for {type}, using 0");
timeout = 0;
}
_timeoutByType[type] = timeout;
}
/// <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);
// 禁用 GameObject对象池优化
audio.SetActive(false);
// 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]++;
// 记录进入停止池的时间戳(毫秒)
_stoppedTimeById[audio.AudioId] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
// 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>
/// 更新停止池(检查并释放超时的音频)
/// <para>降低检查频率,避免每帧都检查</para>
/// </summary>
/// <param name="dt">帧时间(秒)</param>
/// <returns>被释放的 AudioPlayer 列表(需要调用方 Dispose如果没有则返回 null</returns>
public List<AudioPlayer> Update(float dt)
{
// 降低检查频率(每秒检查一次)
_checkTimer += dt;
if (_checkTimer < AudioConstant.StoppedPoolCheckInterval)
return null; // 返回 null避免分配空列表
_checkTimer = 0f;
var toDispose = new List<AudioPlayer>();
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
// 复用临时列表
_tempKeysToRemove.Clear();
// 直接遍历字典,收集要删除的 key避免 ToList 的内存分配)
foreach (var kvp in _stoppedTimeById)
{
var audioId = kvp.Key;
var stoppedTime = kvp.Value;
// 获取音频(可能已被移除)
if (!_poolById.TryGetValue(audioId, out var audio))
{
// 收集无效的时间戳,稍后批量删除
_tempKeysToRemove.Add(audioId);
continue;
}
var type = audio.AudioType;
var timeoutMs = (long)(_timeoutByType[type] * 1000);
// 检查是否超时(超时时间 > 0 才启用)
if (timeoutMs > 0 && currentTime - stoppedTime >= timeoutMs)
{
_tempKeysToRemove.Add(audioId);
toDispose.Add(audio);
}
}
// 批量删除无效的时间戳
foreach (var key in _tempKeysToRemove)
{
_stoppedTimeById.Remove(key);
}
if (toDispose.Count > 0)
{
int a = 1;
foreach (var toRemove in toDispose)
{
RemoveFromPool(toRemove);
}
}
// 返回结果(空列表返回 null避免分配
return toDispose.Count > 0 ? toDispose : null;
}
/// <summary>
/// 清空停止池(不释放 AudioPlayer 资源)
/// </summary>
public void Clear()
{
_poolByKey.Clear();
_poolById.Clear();
_stoppedTimeById.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();
_stoppedTimeById.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]--;
// 清除时间戳
_stoppedTimeById.Remove(audio.AudioId);
}
/// <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
}