1.介紹
遊戲開發中我們會頻繁使用到預製體來最佳化記憶體,最佳化效能,增強遊戲表現。
當要使用的預製體次數很多,建立銷燬很頻繁時,為了方便管理、提升效能,我們需要一個物件池。
一般使用單例+一個預製體+一個儲存型別就能做出一個簡單的物件池。
但當我們需要對很多種物體進行物件池管理時、當我們需要對很多型別的物體進行物件池管理時,當我們需要對物件的使用週期進行監聽時、當我們需要一些簡便的管理以及為協作者提供介面時…這時我們就需要仔細思考一下如何升級我們的物件池了。
2.需要實現的功能
- 實現多物體的物件池,提供簡單方便的介面
- 實現多型別的物件池,提供簡單方便的介面
- 為每種每類物件的生命週期提供監聽方法的介面
3.拆解分析與實現
從實現的簡單程度與基礎程度,先實現基礎物件池的生命週期監聽,在此基礎上實現多物體物件池和多型別物件池的管理
3.1 物件生命週期的監聽
物件的生命週期包含物件建立時、從物件池中獲取時、回收進物件池時。正常寫需要將建立、獲取、回收方法封裝一層傳入一個委託方法,Unity有提供ObjectPool<>、LinkedPool<>、DictionaryPool<>、HashPool<>等物件池並提供監聽介面,我們使用ObjectPool<>作為基礎的元池,逐步構建一個更大的物件池。
ObjectPool類:
點選檢視程式碼
//unity的內建類,屬性方法比較簡單,可看unityAPI手冊學習
public class ObjectPool<T> : IDisposable, IObjectPool<T> where T : class
{
public ObjectPool(Func<T> createFunc, Action<T> actionOnGet = null, Action<T> actionOnRelease = null, Action<T> actionOnDestroy = null, bool collectionCheck = true, int defaultCapacity = 10, int maxSize = 10000);
public int CountAll { get; }
public int CountActive { get; }
public int CountInactive { get; }
public void Clear();
public void Dispose();
public T Get();
public PooledObject<T> Get(out T v);
public void Release(T element);
}
3.2 多物體的物件池
當預製體很多時,我們可以使用Dictonary等帶標誌的資料結構儲存、使用一個個預製體的對應物件池,我們只需要將物件與字典聯絡起來,以後就能使用一個字典來管理所有的預製體,並且我們可以規定使用預製體的名字作為鍵,這樣在使用與理解上都十分友好,唯一需要注意的是,必須要有良好的預製體命名規範,不能有相同名字的不同預製體。
點選檢視程式碼
//用於掛載所有預製體
public List<GameObject> poolPrefabs;
//管理預製體
private Dictionary<string, GameObject> sortedPrefabs = new();
void SortedPoolInit()
{
foreach (GameObject prefab in poolPrefabs)
{
if (!sortedPrefabs.ContainsKey(prefab.name))
sortedPrefabs.Add(prefab.name, prefab);
else
Debug.LogError("----------有重複的prefab名稱");
}
}
//物件池Get對應物件的介面
public GameObject Get(string name)
{
}
物件池Release回收物件的介面
public void Release(string name, GameObject obj)
{
}
3.3 多型別的物件池
除開值型別,GameObject一類的,其他型別比如一些類WaitForSeconds、Animal、Bullet啥的,這個其實用到的並不多並且開銷不算大,還有Mono記憶體堆管理,只是都寫到這了補充上吧。和之前GameObject的處理類似,不過需要一個各型別與object的引用型別之間的裝箱與拆箱。
點選檢視程式碼
Dictionary<Type, object> sortedCustomClass = new();
public object GetCustom(Type t)
{
}
public ReleaseCustom(Type t,object o)
{
}
3.4 綜合實現
個人習慣先從介面寫起,定好規範性與約束性。對於大大小小的物件池、元池、擴充池,都需要拿取Get、回收Release介面及他們的監聽OnGet、OnRelease,先照此寫個IPool.
點選檢視程式碼
public interface IPool<T> where T:class
{
/// <summary>
/// 從池中獲取
/// </summary>
/// <returns></returns>
public T Get();
/// <summary>
/// 放回池中並釋放
/// </summary>
public void Release(T obj);
/// <summary>
/// 獲取監聽
/// </summary>
public void OnGet(T obj);
/// <summary>
/// 放回監聽
/// </summary>
public void OnRelease(T obj);
}
因為要實現監聽,而不同池需要不同的監聽,所以需要一個基礎池(元池)父類,要有對應的池的名字(等同其預製體名字)、對應的預製體以及一個ObjectPool:
點選檢視程式碼
public class PoolBase<T> : UnityEngine.Object, IPool<T> where T : class
{
/// <summary>
/// 元池名
/// </summary>
public string poolName;
/// <summary>
/// 元池的預製體
/// </summary>
protected T poolPrefab;
/// <summary>
/// 元池 ObjectPool
/// </summary>
public ObjectPool<T> pool;
/// <summary>
/// 從元池中獲取
/// </summary>
public virtual T Get()
{
throw new NotImplementedException();
}
/// <summary>
/// 元池拿取的監聽事件
/// </summary>
/// <param name="obj">拿取之物</param>
public virtual void OnGet(T obj)
{
}
/// <summary>
/// 元池回收
/// </summary>
/// <param name="obj">回收之物</param>
public virtual void Release(T obj)
{
}
/// <summary>
/// 元池回收的監聽事件
/// </summary>
/// <param name="obj">回收之物</param>
public virtual void OnRelease(T obj)
{
}
}
根據這個泛型元池,我們可以按需要寫一些具體要使用的派生類了:
點選檢視程式碼
/// <summary>
/// GameObject型別的池
/// </summary>
public class ObjPool : PoolBase<GameObject>
{
//區別於ObjectPool的池、按需使用
public HashSetPool<GameObject> hashpool;
public DictionaryPool<string, GameObject> dicpool;
public LinkedPool<GameObject> linkedpool;
//傳入池名及預製體資料
public ObjPool(string name, GameObject prefab)
{
poolName = name;
pool = new ObjectPool<GameObject>(OnCreate, OnGet, OnRelease);
poolPrefab = prefab;
}
//生成監聽
GameObject OnCreate()
{
GameObject GO = Instantiate(poolPrefab);
GO.SetActive(false);
return GO;
}
public override GameObject Get()
{
return pool.Get();
}
public override void Release(GameObject GO)
{
pool.Release(GO);
}
public override void OnGet(GameObject GO)
{
// 自定義監聽事件
GO.SetActive(true);
}
public override void OnRelease(GameObject GO)
{
// 自定義監聽事件
GO.SetActive(false);
}
}
//其他例子,但其實使用很少,也有其他的方法替代
public class MaterialPool : PoolBase<Material>
{
public MaterialPool(string name, Material prefab)
{
poolName = name;
pool = new ObjectPool<Material>(OnCreate, OnGet, OnRelease);
poolPrefab = prefab;
}
Material OnCreate()
{
Material Mt = Instantiate(poolPrefab) ;
return Mt;
}
public override Material Get()
{
return pool.Get();
}
public override void OnGet(Material mt)
{
}
public override void OnRelease(Material mt)
{
}
public override void Release(Material mt)
{
pool.Release(mt);
}
}
點選檢視程式碼
public class CustomPool<V> : IPool<V> where V : class
{
private ObjectPool<V> _CustomPool;
//將生成監聽傳參,其他監聽也可以套用
private Func<V> _Creator;
public CustomPool(Func<V> creator)
{
_Creator = creator ?? throw new ArgumentNullException(nameof(creator));
_CustomPool = new ObjectPool<V>(_Creator, OnGet, OnRelease);
}
public V Get()
{
V v = _CustomPool.Get();
return v;
}
public void Release(V v)
{
_CustomPool.Release(v);
}
public void OnGet(V v)
{
}
public void OnRelease(V v)
{
}
}
搞完自動化了,現在要加上手動擋實現半自動化了。什麼?你問為什麼不做個全自動化?程式碼全自動化了程式設計師不就失業了嗎( o`ω′)ノ!好吧,只是筆者筆力不夠了...
點選檢視程式碼
//Singleton是一個泛型單例基類
public class PoolMgr : Singleton<PoolMgr>
{
#region GO物件池
/// <summary>
/// GO預製體
/// </summary>
public List<GameObject> poolPrefabs;
/// <summary>
/// <summary>
/// GameObject的總物件池
/// </summary>
public Dictionary<string, ObjPool> SortedPool = new();
void Awake()
{
base.Awake();
DontDestroyOnLoad(gameObject);
SortedPoolInit();
}
/// <summary>
/// 分類建立各池
/// </summary>
void SortedPoolInit()
{
foreach (GameObject prefab in poolPrefabs)
{
if (!SortedPool.ContainsKey(prefab.name))
{
SortedPool.Add(prefab.name, new ObjPool(prefab.name, prefab));
Debug.Log("------------新增池 " + prefab.name);
}
else
Debug.LogError("----------有重複的prefab名稱");
}
}
/// <summary>
/// 獲取name池中的一個節點
/// </summary>
public GameObject Get(string name)
{
if (!SortedPool.ContainsKey(name))
{
Debug.LogError("不存在要獲取的子物件池--" + name);
return null;
}
return SortedPool[name].Get();
}
/// <summary>
/// 將obj回收進name池
/// </summary>
public void Release(string name, GameObject obj)
{
if (!SortedPool.ContainsKey(name))
{
Debug.LogWarning("不存在要回收進的物件池--" + name );
Destroy(obj);
return;
}
obj.transform.SetParent(transform);
SortedPool[name].Release(obj);
}
#endregion
#region 其他不可序列化的物件池
Dictionary<Type, CustomPool<object>> CustomPoolDic = new();
public object GetCustom(Type t, Func<object> func = null)
{
//使用預設object構建
if (!CustomPoolDic.ContainsKey(t) && func == null)
{
CustomPoolDic.Add(t, new CustomPool<object>(() => { return new object(); }));
}
//使用提供的function構建
else if (!CustomPoolDic.ContainsKey(t) && func != null)
CustomPoolDic.Add(t, new CustomPool<object>(func));
//替換原構造的custom pool 為新的構造,可能有問題,取決於兩個構建委託的相容性
else if (CustomPoolDic.ContainsKey(t) && func != null)
{
CustomPoolDic[t] = new CustomPool<object>(func);
}
return CustomPoolDic[t].Get();
}
public void ReleaseCustom(Type t, object o)
{
if (!CustomPoolDic.ContainsKey(t))
{
o = null;
}
else
CustomPoolDic[t].Release(o);
}
#endregion
}
前一類池的使用非常簡單,配置好預製體和單例後只需要關注兩個介面:
點選檢視程式碼
GameObject bullet = PoolMgr.Instance.Get("Bullet");
PoolMgr.Instance.Release("Bullet",bullet);
關於後面一類池的使用以及可能出現的問題筆者以WaitForSeconds為例,大家就很好理解了
點選檢視程式碼
//這裡第一次從池中拿,wts1為新構建的WaitForSeconds(0.1f)
WaitForSeconds wts1 = (WaitForSeconds)PoolMgr.Instance.GetCustom(typeof(WaitForSeconds), () => { return new WaitForSeconds(0.1f); });
//這裡放回去一個WaitForSeconds(0.1f)
PoolMgr.Instance.ReleaseCustom(typeof(WaitForSeconds), wts);
//這裡想要拿的是WaitForSeconds(0.2f),但返回的是WaitForSeconds(0.1f)
WaitForSeconds wts2 = (WaitForSeconds)PoolMgr.Instance.GetCustom(typeof(WaitForSeconds), () => { return new WaitForSeconds(0.2f); });
//這裡池中沒有了,所以會使用新的構建拿出一個WaitForSeconds(0.3f)
WaitForSeconds wts3 = (WaitForSeconds)PoolMgr.Instance.GetCustom(typeof(WaitForSeconds), () => { return new WaitForSeconds(0.3f); });
4.總結
這是一個半成品的物件池,還有許多部分與方面可以最佳化,但在大部分情況下夠用了,時間與筆力有限,下次再與大家一起分享探討。