chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2a86b9bc162692145809a4f3514ab86e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace BaseGames.Core.Pool
{
/// <summary>
/// 可池化对象组件,挂在每个可池化 Prefab 的根节点上。
/// 实现 <see cref="IPoolable"/> 的子类可覆盖 OnSpawn/OnDespawn。
/// </summary>
public class PooledObject : MonoBehaviour
{
public string AddressKey { get; private set; }
private GlobalObjectPool _pool;
// 组件缓存(避免反复 GetComponent
private readonly Dictionary<Type, Component> _componentCache = new();
public void Setup(string key, GlobalObjectPool pool)
{
AddressKey = key;
_pool = pool;
}
public virtual void OnSpawn() { }
public virtual void OnDespawn(){ }
// ── 归还 API ──────────────────────────────────────────────────────
/// <summary>立即归还到对象池。</summary>
public void ReturnToPool() => _pool?.Despawn(AddressKey, this);
/// <summary>延迟 delay 秒后归还。</summary>
public void ReturnToPoolDelayed(float delay) => StartCoroutine(DelayedReturn(delay));
/// <summary>
/// 由 GlobalObjectPool LRU 回收触发,不应由业务代码直接调用。
/// </summary>
internal void ForceReturnToPool()
{
OnDespawn();
gameObject.SetActive(false);
}
// ── 组件缓存 ──────────────────────────────────────────────────────
/// <summary>从缓存获取 Component首次调用才执行 GetComponent。</summary>
public T GetComponentCached<T>() where T : Component
{
if (!_componentCache.TryGetValue(typeof(T), out var cached))
_componentCache[typeof(T)] = cached = GetComponent<T>();
return (T)cached;
}
private IEnumerator DelayedReturn(float delay)
{
yield return new WaitForSeconds(delay);
ReturnToPool();
}
}
/// <summary>
/// 可选接口:若池化对象需要在 Spawn/Despawn 时执行额外逻辑,
/// 由 PooledObject 子类或同 GameObject 上的其他 MonoBehaviour 实现,
/// 并在 PooledObject.OnSpawn/OnDespawn 中手动驱动。
/// </summary>
public interface IPoolable
{
void OnSpawn();
void OnDespawn();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a1abe6a66724ba4586a0d4f7ae5f6e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: