215 lines
8.4 KiB
C#
215 lines
8.4 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using UnityEngine.AddressableAssets;
|
||
|
||
namespace BaseGames.Core.Pool
|
||
{
|
||
/// <summary>
|
||
/// 全局对象池(Persistent 场景)。
|
||
/// 先调用 <see cref="WarmupAsync"/> 预热后,再通过 <see cref="Spawn"/> 取出对象。
|
||
/// </summary>
|
||
[DefaultExecutionOrder(-800)]
|
||
public class GlobalObjectPool : MonoBehaviour
|
||
{
|
||
public static GlobalObjectPool Instance { get; private set; }
|
||
|
||
[System.Serializable]
|
||
public struct PoolConfig
|
||
{
|
||
public string AddressKey;
|
||
public int InitialCount;
|
||
/// <summary>0 = 无上限;> 0 强制限制池中 + 活跃对象总数。</summary>
|
||
public int MaxCount;
|
||
}
|
||
|
||
[SerializeField] private PoolConfig[] _warmupConfigs;
|
||
|
||
private readonly Dictionary<string, Queue<PooledObject>> _pools = new();
|
||
private readonly Dictionary<string, List<PooledObject>> _alive = new();
|
||
private readonly Dictionary<string, GameObject> _prefabCache = new();
|
||
private readonly Dictionary<string, int> _maxCounts = new();
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance != null) { Destroy(gameObject); return; }
|
||
Instance = this;
|
||
}
|
||
|
||
// ── 预热 ──────────────────────────────────────────────────────────
|
||
/// <summary>在场景加载完成后(StartCoroutine)调用预热。</summary>
|
||
public IEnumerator WarmupCoroutine()
|
||
{
|
||
foreach (var cfg in _warmupConfigs)
|
||
{
|
||
_maxCounts[cfg.AddressKey] = cfg.MaxCount;
|
||
yield return WarmupSingleCoroutine(cfg.AddressKey, cfg.InitialCount);
|
||
}
|
||
}
|
||
|
||
/// <summary>async Task 版本(可 await,供非 MonoBehaviour 调用)。</summary>
|
||
public async Task WarmupAsync()
|
||
{
|
||
foreach (var cfg in _warmupConfigs)
|
||
{
|
||
_maxCounts[cfg.AddressKey] = cfg.MaxCount;
|
||
await WarmupSingleAsync(cfg.AddressKey, cfg.InitialCount);
|
||
}
|
||
}
|
||
|
||
private IEnumerator WarmupSingleCoroutine(string key, int count)
|
||
{
|
||
var loadOp = Addressables.LoadAssetAsync<GameObject>(key);
|
||
yield return loadOp;
|
||
var prefab = loadOp.Result;
|
||
_prefabCache[key] = prefab;
|
||
EnsureCollections(key, count);
|
||
for (int i = 0; i < count; i++)
|
||
EnqueueNew(key, prefab);
|
||
}
|
||
|
||
private async Task WarmupSingleAsync(string key, int count)
|
||
{
|
||
var prefab = await Addressables.LoadAssetAsync<GameObject>(key).Task;
|
||
_prefabCache[key] = prefab;
|
||
EnsureCollections(key, count);
|
||
for (int i = 0; i < count; i++)
|
||
EnqueueNew(key, prefab);
|
||
}
|
||
|
||
private void EnsureCollections(string key, int capacity)
|
||
{
|
||
if (!_pools.ContainsKey(key)) _pools[key] = new Queue<PooledObject>(capacity);
|
||
if (!_alive.ContainsKey(key)) _alive[key] = new List<PooledObject>();
|
||
}
|
||
|
||
private void EnqueueNew(string key, GameObject prefab)
|
||
{
|
||
var go = Instantiate(prefab);
|
||
var po = go.GetComponent<PooledObject>();
|
||
if (po == null) po = go.AddComponent<PooledObject>();
|
||
po.Setup(key, this);
|
||
go.SetActive(false);
|
||
_pools[key].Enqueue(po);
|
||
}
|
||
|
||
// ── Spawn ─────────────────────────────────────────────────────────
|
||
public T Spawn<T>(string key, Vector3 position, Quaternion rotation) where T : Component
|
||
=> SpawnInternal(key, position, rotation)?.GetComponentCached<T>();
|
||
|
||
public GameObject Spawn(string key, Vector3 position, Quaternion rotation)
|
||
=> SpawnInternal(key, position, rotation)?.gameObject;
|
||
|
||
private PooledObject SpawnInternal(string key, Vector3 pos, Quaternion rot)
|
||
{
|
||
if (!_pools.TryGetValue(key, out var queue))
|
||
{
|
||
Debug.LogError($"[GlobalObjectPool] '{key}' 未预热,请先调用 WarmupAsync/WarmupCoroutine。");
|
||
return null;
|
||
}
|
||
|
||
PooledObject po;
|
||
var aliveList = GetAliveList(key);
|
||
int maxCount = _maxCounts.GetValueOrDefault(key, 0);
|
||
|
||
if (queue.Count > 0)
|
||
{
|
||
po = queue.Dequeue();
|
||
}
|
||
else if (maxCount > 0 && aliveList.Count >= maxCount)
|
||
{
|
||
// 已达上限:LRU 回收最早 Spawn 的活跃对象
|
||
po = aliveList[0];
|
||
aliveList.RemoveAt(0);
|
||
po.ForceReturnToPool();
|
||
Debug.LogWarning($"[GlobalObjectPool] '{key}' 已达 MaxCount={maxCount},LRU 回收中。");
|
||
}
|
||
else
|
||
{
|
||
// 池空且未达上限:同步实例化,并在后台协程补池
|
||
if (!_prefabCache.TryGetValue(key, out var pfx))
|
||
{
|
||
Debug.LogError($"[GlobalObjectPool] '{key}' Prefab 未缓存。");
|
||
return null;
|
||
}
|
||
var go = Instantiate(pfx);
|
||
po = go.GetComponent<PooledObject>();
|
||
if (po == null) po = go.AddComponent<PooledObject>();
|
||
po.Setup(key, this);
|
||
StartCoroutine(BackgroundRefillCoroutine(key, 1));
|
||
}
|
||
|
||
po.transform.SetPositionAndRotation(pos, rot);
|
||
po.gameObject.SetActive(true);
|
||
po.OnSpawn();
|
||
aliveList.Add(po);
|
||
return po;
|
||
}
|
||
|
||
// ── Despawn ───────────────────────────────────────────────────────
|
||
public void Despawn(string key, PooledObject po)
|
||
{
|
||
var aliveList = GetAliveList(key);
|
||
aliveList.Remove(po);
|
||
po.gameObject.SetActive(false);
|
||
po.OnDespawn();
|
||
|
||
int maxCount = _maxCounts.GetValueOrDefault(key, 0);
|
||
int queueSize = _pools.TryGetValue(key, out var queue) ? queue.Count : 0;
|
||
|
||
if (maxCount > 0 && queueSize + aliveList.Count >= maxCount)
|
||
{
|
||
Destroy(po.gameObject);
|
||
return;
|
||
}
|
||
|
||
if (queue == null) { _pools[key] = queue = new Queue<PooledObject>(); }
|
||
queue.Enqueue(po);
|
||
}
|
||
|
||
// ── 后台补池 ──────────────────────────────────────────────────────
|
||
private IEnumerator BackgroundRefillCoroutine(string key, int count)
|
||
{
|
||
if (!_prefabCache.TryGetValue(key, out var pfx)) yield break;
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
yield return null; // 下一帧
|
||
var go = Instantiate(pfx);
|
||
var po = go.GetComponent<PooledObject>();
|
||
if (po == null) po = go.AddComponent<PooledObject>();
|
||
po.Setup(key, this);
|
||
go.SetActive(false);
|
||
if (_pools.TryGetValue(key, out var q)) q.Enqueue(po);
|
||
}
|
||
}
|
||
|
||
// ── 清空 ──────────────────────────────────────────────────────────
|
||
public void ClearPool(string key)
|
||
{
|
||
if (_pools.TryGetValue(key, out var queue))
|
||
{
|
||
while (queue.Count > 0)
|
||
{
|
||
var po = queue.Dequeue();
|
||
if (po != null) Destroy(po.gameObject);
|
||
}
|
||
_pools.Remove(key);
|
||
}
|
||
_alive.Remove(key);
|
||
if (_prefabCache.TryGetValue(key, out var pfx))
|
||
{
|
||
Addressables.Release(pfx);
|
||
_prefabCache.Remove(key);
|
||
}
|
||
}
|
||
|
||
private List<PooledObject> GetAliveList(string key)
|
||
{
|
||
if (!_alive.TryGetValue(key, out var list))
|
||
_alive[key] = list = new List<PooledObject>();
|
||
return list;
|
||
}
|
||
}
|
||
}
|