对象池系统2.0
在2.0中,Object Pool(简称OP)的内部实现和GameObject Pool(简称GOP)进行了高度统一,他们的区别和联系主要在于:
-
GameObject对象池需要额外维护Hierarchy上的父子层级关系,是看得见的,所以要做好GameObject的实例化和层物体(需要额外实例化且可重用)的管理。
-
Object对象池是看不见的,其只需要维护好数据的关系,与GameObject的实例化相对的是做好object类型的实例化,同时其不可视化不需要额外维护层物体(但Object和GameObject对象池都要做好层这一级的逻辑关系处理,体现在对象池字典中key和value的映射上,比如说移除某一层(类)对象池,对应remove掉key)。
-
在外界使用的过程中,Init,Push,Get,Clear操作使用逻辑两者基本相同,Object因其传参(type和泛型T)提供更多的重载方法,基本原理都是通过对象池的名字(keyName)找到对应的那一层对象池做操作,只不过名字的获取可以通过多种方式,重载方法的设计提供给了外界更多的选择。
-
实际上Gameobject对象池只负责对GameObject管理,而Object对象池可以对所有类型(object均可,不局限于脚本类)管理,GameObject严格上来说也可以归在Object对象池里(Unity官方就是这么做的只提供一个ObjectPool),但由于游戏对象较为特殊在面板上存在实体可见且常用,所以单独对其进行维护。 2.0对象池系统UML类图如下(划线删除部分为1.0对象池)。
PoolSystem层
PoolSystem作为对象池系统的最上层逻辑,对外提供功能API,相当于对1.0PoolManager进行一层独立的功能封装,PoolSystem不再负责具体的对象池操作逻辑,仅提供方法的调用,因此移除了Check相关的具体内部逻辑(这一逻辑在Moudle层通过TryGetValue简化),并对重载方法进行了优化。这一层相当于全局对象池,持有两种对象池的Moudle层供全局访问。
对象池系统数据及静态构造方法
PoolSystem改用静态类对外提供静态功能方法,不挂载在场景中,其持有GameObject和Object对象池Model层的实例,以及对象池最顶层根节点的游戏物体名称和对应的TransForm。
PoolLayerGameObjectName作为层物体的名称,用于将当前所有的对象池的层物体回收时给一个统一的名字(具体解释见Data层)。
/// <summary>
/// 对象池系统
/// </summary>
public static class PoolSystem
{
/// <summary>
/// 对象池层物体的游戏物体名称,用于将层物体也放进对象池
/// </summary>
public const string PoolLayerGameObjectName = "PoolLayerGameObjectName";
private static GameObjectPoolModule GameObjectPoolModule;
private static ObjectPoolModule CSharObjectPoolModule;
private static Transform poolRootTransform;
static PoolSystem()
{
GameObjectPoolModule = new GameObjectPoolModule();
ObjectPoolModule = new ObjectPoolModule();
poolRootTransform = new GameObject("PoolRoot").transform;
poolRootTransform.position = Vector3.zero;
poolRootTransform.SetParent(JKFrameRoot.RootTransform);
GameObjectPoolModule.Init(poolRootTransform);
}
.......
}
在PoolSystem的静态构造函数中实例化持有了对象池的Module层,并对GameObject对象池根节点对象进行初始化。
GOP相关API
提供初始化、取出、放入、清空GameObject对象池中GameObject的功能API,与1.0相比增加了对象池的容量限制、默认容量初始化的API。
/// <summary>
/// 初始化一个GameObject类型的对象池类型
/// </summary>
/// <param name="assetName">资源名称</param>
/// <param name="maxCapacity">容量限制,超出时会销毁而不是进入对象池,-1代表无限</param>
/// <param name="defaultCapacity">默认容量,填写会向池子中放入对应数量的对象,0代表不预先放入</param>
/// <param name="prefab">填写默认容量时预先放入的对象</param>
public static void InitGameObjectPool(string assetName, int maxCapacity = -1,GameObject prefab = null,int defaultCapacity = 0)
{
GameObjectPoolModule.InitObjectPool(assetName, maxCapacity,prefab,defaultCapacity);
}
/// <summary>
/// 获取GameObject,没有则返回Null
/// </summary>
public static GameObject GetGameObject(string assetName, Transform parent = null)
{
GameObject go = GameObjectPoolModule.GetObject(assetName, parent);
return go;
}
/// <summary>
/// 获取GameObject,没有则返回Null
/// T:组件
/// </summary>
public static T GetGameObject<T>(string assetName, Transform parent = null) where T : Component
{
GameObject go = GetGameObject(assetName, parent);
if (go != null) return go.GetComponent<T>();
else return null;
}
/// <summary>
/// 游戏物体放置对象池中
/// </summary>
/// <param name="assetName">对象池中的key</param>
/// <param name="obj">放入的物体</param>
public static void PushGameObject(string assetName, GameObject obj)
{
if (obj != null)
{
GameObjectPoolModule.PushObject(assetName, obj);
}
}
/// <summary>
/// 游戏物体放置对象池中
/// </summary>
/// <param name="obj">放入的物体,并且基于它的name来确定它是什么物体</param>
public static void PushGameObject(GameObject obj)
{
PushGameObject(obj.name, obj);
}
/// <summary>
/// 清除某个游戏物体在对象池中的所有数据
/// </summary>
public static void ClearGameObject(string assetName)
{
GameObjectPoolModule.Clear(assetName);
}
取出、放入、清空都是通过assetName作为索引完成,放入提供两种重载,不指定对象池名字默认使用物体的名字做索引。这里需要注意的是,PushGameObject重载方法通过直接调用节省了代码量(正常应该对应调用Moudle层的重载方法),进行了合并,但在Moudle层该有的重载方法还是存在的,因为Moudle会被单独持有,我们希望保留多样的功能接口。
OP相关API
提供初始化、取出、放入、清空Object对象池中C#类的功能API,与1.0相比增加了对象池的容量限制、默认容量初始化的API。
/// <summary>
/// 初始化对象池并设置容量
/// </summary>
/// <param name="keyName">资源名称</param>
/// <param name="maxCapacity">容量限制,超出时会销毁而不是进入对象池,-1代表无限</param>
/// <param name="defaultCapacity">默认容量,填写会向池子中放入对应数量的对象,0代表不预先放入</param>
public static void InitObjectPool<T>(string keyName, int maxCapacity = -1, int defaultCapacity = 0) where T : new()
{
ObjectPoolModule.InitObjectPool<T>(keyName, maxCapacity, defaultCapacity);
}
/// <summary>
/// 初始化一个普通C#对象池类型
/// </summary>
/// <param name="keyName">keyName</param>
/// <param name="maxCapacity">容量,超出时会丢弃而不是进入对象池,-1代表无限</param>
public static void InitObjectPool(string keyName, int maxCapacity = -1)
{
ObjectPoolModule.InitObjectPool(keyName, maxCapacity);
}
/// <summary>
/// 初始化对象池
/// </summary>
/// <param name="type">资源类型</param>
/// <param name="maxCapacity">容量限制,超出时会销毁而不是进入对象池,-1代表无限</param>
public static void InitObjectPool(System.Type type, int maxCapacity = -1)
{
ObjectPoolModule.InitObjectPool(type,maxCapacity);
}
根据keyname初始化一个空的对象池,通过maxCapacity指定容量限制(-1为无限制),可选择通过T传入默认对象类型和数量进行预先初始化,同时keyname可以直接传也可以通过type和T间接拿到,提供多种重载。
/// <summary>
/// 获取普通对象(非GameObject)
/// </summary>
public static T GetObject<T>() where T : class
{
return ObjectPoolModule.GetObject<T>();
}
/// <summary>
/// 获取普通对象(非GameObject)
/// </summary>
public static object GetObject(System.Type type)
{
return ObjectPoolModule.GetObject(type);
}
/// <summary>
/// 普通对象(非GameObject)放置对象池中
/// </summary>
public static void PushObject(object obj)
{
ObjectPoolModule.PushObject(obj);
}
/// <summary>
/// 清理某个C#类型数据
/// </summary>
public static void ClearObject<T>()
{
CSharObjectPoolModule.ClearObject<T>();
}
/// <summary>
/// 清理某个C#类型数据
/// </summary>
public static void ClearObject(System.Type type)
{
ObjectPoolModule.ClearObject(type);
}
GOP和OP对象池同时启用的API
清空两个对象池。
public static void ClearAll(bool clearGameObject = true, bool clearCSharpObject = true)
{
if (clearGameObject)
{
GameObjectPoolModule.ClearAll();
}
if (clearCSharpObject)
{
CSharObjectPoolModule.ClearAll();
}
}
PoolModule层
PoolMoudle负责对一类对象池中的所有池子进行操作,使用dic来存储某一种对象池的名字和其数据的映射关系,作用与1.0的PoolManager类似,增加了对象池容量的相关管理逻辑。
GOPModule
GOPModule持有的数据及初始化方法
持有GameObject对象池根节点对象及数据字典,通过Init方法由System层传过来根节点对象池的初始值。
// 根节点
private Transform poolRootTransform;
/// <summary>
/// GameObject对象容器
/// </summary>
private Dictionary<string, GameObjectPoolData> poolDic = new Dictionary<string, GameObjectPoolData>();
public void Init(Transform poolRootTransform)
{
this.poolRootTransform = poolRootTransform;
}
初始化对象池容量,并建立Data层的数据。
需要说明的是,初始化这块逻辑在1.0里是放在PushGameObject里的,为了在放入对象池时如果没有对应的池子会自动新建。因为需要对容量进行设置,2.0把这块逻辑单独封了一个InitObjectPool方法让外界可以指定池子的最大容量和默认容量。
另外可以发现,用于存储数据的poolData类本身用object对象池管理,实现资源的重复利用,因为尽管在dic中不同类型对象池对应的GameObjectPoolData的Queue中装的GameObject不同,但数据本身都是GameObjectPoolData,也是可以重复使用的(销毁时把类留下来,再用的时候重新初始化),只不过在1.0中清空一类对象池时,GameObjectPoolData会直接从GameObjectPoolDic中Remove掉没有考虑重复使用。
/// <summary>
/// 初始化对象池并设置容量
/// </summary>
public void InitObjectPool(string keyName, int capacity = -1)
{
if (poolDic.TryGetValue(keyName, out GameObjectPoolData poolData))
{
poolData.Capacity = capacity;
}
else
{
poolData = CreateGameObjectPoolData(keyName, capacity);
}
}
/// <summary>
/// 创建一条新的对象池数据
/// </summary>
private GameObjectPoolData CreateGameObjectPoolData(string layerName, int capacity = -1)
{
//交由Object对象池拿到poolData的类
GameObjectPoolData poolData = PoolSystem.GetObject<GameObjectPoolData>();
//Object对象池中没有再new
if (poolData == null) poolData = new GameObjectPoolData(capacity);
//对拿到的poolData副本进行初始化(覆盖之前的数据)
poolData.Init(layerName, poolRootTransform);
poolDic.Add(layerName, poolData);
return poolData;
}
GOP相关功能
取出、放入、清空GameObject对象池中具体一类池子的功能,在放入取出是要先检查有没有这一层,进而调用Data层的Push,Get逻辑,功能实现与1.0相同。
public GameObject GetObject(string keyName, Transform parent = null)
{
GameObject obj = null;
// 检查有没有这一层
if (poolDic.TryGetValue(keyName, out GameObjectPoolData poolData) && poolData.PoolQueue.Count > 0)
{
obj = poolData.GetObj(parent);
}
return obj;
}
public void PushObject(GameObject go)
{
PushObject(go.name, go);
}
public void PushObject(string keyName, GameObject obj)
{
// 现在有没有这一层
if (poolDic.TryGetValue(keyName, out GameObjectPoolData poolData))
{
poolData.PushObj(obj);
}
else
{
poolData = GetAndInitGameObjectPoolData(keyName);
poolData.PushObj(obj);
}
}
public void Clear(string keyName)
{
if (poolDic.TryGetValue(keyName, out GameObjectPoolData gameObjectPoolData))
{
gameObjectPoolData.Desotry(true);
poolDic.Remove(keyName);
}
}
public void ClearAll()
{
var enumerator = poolDic.GetEnumerator();
while (enumerator.MoveNext())
{
enumerator.Current.Value.Desotry(false);
}
poolDic.Clear();
}
OPModule
OPModule持有的数据及初始化方法
这块逻辑和GameObjectPoolModule一致,只不过存储的PoolData数据类型不一样。
/// <summary>
/// 普通类 对象容器
/// </summary>
private Dictionary<string, ObjectPoolData> poolDic = new Dictionary<string, ObjectPoolData>();
/// <summary>
/// 初始化对象池并设置容量
/// </summary>
public void InitObjectPool(System.Type type, int capacity = -1, int defaultCount = 0)
{
if (poolDic.TryGetValue(type.FullName, out ObjectPoolData poolData))
{
poolData.Capacity = capacity;
}
else
{
poolData = CreateObjectPoolData(type.FullName, capacity);
}
}
/// <summary>
/// 创建一条新的对象池数据
/// </summary>
private ObjectPoolData CreateObjectPoolData(string layerName, int capacity = -1)
{
// 交由Object对象池拿到poolData的类
ObjectPoolData poolData = this.GetObject<ObjectPoolData>();
//Object对象池中没有再new
if (poolData == null)
{
poolData = new ObjectPoolData(capacity);
}
//对拿到的poolData副本进行初始化(覆盖之前的数据)
poolData.maxCapacity = capacity;
poolDic.Add(layerName, poolData);
return poolData;
}
OP相关功能
这块逻辑和GameObjectPoolModule一致,只不过不需要传parent进行层级设置。
public object GetObject(System.Type type)
{
object obj = null;
if (poolDic.TryGetValue(type.FullName, out ObjectPoolData objectPoolData) && objectPoolData.PoolQueue.Count > 0)
{
obj = poolDic[type.FullName].GetObj();
}
return obj;
}
public T GetObject<T>()
{
return (T)GetObject(typeof(T));
}
public void PushObject(object obj)
{
if (poolDic.TryGetValue(obj.GetType().FullName, out ObjectPoolData poolData) == false)
{
poolData = CreateObjectPoolData(obj.GetType().FullName);
}
poolData.PushObj(obj);
}
public void PushObject(object obj,string keyName)
{
if (poolDic.TryGetValue(keyName, out ObjectPoolData poolData) == false)
{
poolData = CreateObjectPoolData(keyName);
}
poolData.PushObj(obj);
}
public void ClearAll()
{
var enumerator = poolDic.GetEnumerator();
while (enumerator.MoveNext())
{
enumerator.Current.Value.Desotry(false);
}
poolDic.Clear();
}
public void ClearObject<T>()
{
ClearObject(typeof(T).FullName);
}
public void ClearObject(System.Type type)
{
ClearObject(type.FullName);
}
public void ClearObject(string keyName)
{
if (poolDic.TryGetValue(keyName, out ObjectPoolData objectPoolData))
{
objectPoolData.Desotry(true);
poolDic.Remove(keyName);
}
}
PoolData层
PoolData层负责对一类对象池下一个特定种类的对象池中所有同类数据进行管理,使用Queue存储数据,作用与1.0中的PoolData一致,对对象池数据的清除进行了完善,正如GameObjectPool相关功能一节所说的,用于存储数据的poolData类本身可以用object对象池管理,因此在数据清除的时候可以选择移除dic中的记录,但数据类本身留存等待下一次使用。
GOPData
GOPData持有的数据及初始化方法
根据容量对Queue进行设置,在初始化过程中首先创建父节点并设置到对象池根节点下方,这里与1.0不同的是:
- 父节点(层物体)也是可以重复利用的,和poolData使用Object对象池重复利用类似,但层物体并没有像GameObjectPoolData,ObjectPoolData这种通用的载体,为此使用PoolLayerGameObjectName把不同种类的GameObject对象池层物体统一为一类方便管理(实际上层物体就是个空物体,只不过不同种名字不一样)。
- 此外需要注意一下GetGameObject的用法是把层物体从对象池中取出再放到PoolRoot下边。
// 这一层物体的 父节点
public Transform RootTransform;
// 对象容器
public Queue<GameObject> PoolQueue;
// 容量限制 -1代表无限
public int Capacity = -1;
public GameObjectPoolData(int capacity = -1)
{
if (capacity == -1)
{
PoolQueue = new Queue<GameObject>();
}
else
{
PoolQueue = new Queue<GameObject>(capacity);
}
}
public void Init(string assetPath, Transform poolRootObj)
{
// 创建父节点 并设置到对象池根节点下方
GameObject go = PoolSystem.GetGameObject(PoolSystem.PoolLayerGameObjectName, poolRootObj);
if (go.IsNull())
{
go = new GameObject(PoolSystem.PoolLayerGameObjectName);
go.transform.SetParent(poolRootObj);
}
RootTransform = go.transform;
RootTransform.name = assetPath;
}
GOP数据相关操作
这里有两点需要注意,其余逻辑与1.0无区别。
- 放入对象池时,由于有容量限制,所以要进行检查,超过容量不再放入对象池内,直接销毁。
- 在销毁GameObject对象时,如上面所说的,其父节点层物体和poolData类是可以重用的,只有GameObject对象自己是真正不能重复利用的,在1.0中,销毁的逻辑实际是有PoolManger相当于2.0的PoolMoudle完成的,直接根据对象池种类名,Destroy根节点并在dic中remove对应的poolData,为此2.0内部提供两种选择,在移除GameObject对象池下的所有种类对象池时,层物体不做保留,如果只移除某一种GameObject对象池,则将层物体给一个统一的名字PoolLayerGameObjectName放回对象池等待下次调用,对于poolData,一律将数据清空放回对象池等待下次调用。
/// <summary>
/// 将对象放进对象池
/// </summary>
public void PushObj(GameObject obj)
{
// 检测是不是超过容量
if (Capacity != -1 && PoolQueue.Count >= Capacity)
{
GameObject.Destroy(obj);
return;
}
// 对象进容器
PoolQueue.Enqueue(obj);
// 设置父物体
obj.transform.SetParent(RootTransform);
// 设置隐藏
obj.SetActive(false);
}
/// <summary>
/// 从对象池中获取对象
/// </summary>
/// <returns></returns>
public GameObject GetObj(Transform parent = null)
{
GameObject obj = PoolQueue.Dequeue();
// 显示对象
obj.SetActive(true);
// 设置父物体
obj.transform.SetParent(parent);
if (parent == null)
{
// 回归默认场景
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(obj, UnityEngine.SceneManagement.SceneManager.GetActiveScene());
}
return obj;
}
/// <summary>
/// 销毁层数据
/// </summary>
/// <param name="pushThisToPool">将对象池层级挂接点也推送进对象池</param>
public void Desotry(bool pushThisToPool = false)
{
Capacity = -1;
if (!pushThisToPool)
{
// 真实销毁 这里由于删除层级根物体 会导致下方所有对象都被删除,所以不需要单独删除PoolQueue
GameObject.Destroy(RootTransform.gameObject);
}
else
{
// 销毁队列中的全部游戏物体
foreach (GameObject item in PoolQueue)
{
GameObject.Destroy(item);
}
// 扔进对象池
RootTransform.gameObject.name = PoolSystem.PoolLayerGameObjectName;
PoolSystem.PushGameObject(RootTransform.gameObject);
PoolSystem.PushObject(this);
}
// 队列清理
PoolQueue.Clear();
RootTransform = null;
}
OPData
OPData持有的数据及初始化方法
逻辑同上。
// 对象容器
public Queue<object> PoolQueue;
// 容量限制 -1代表无限
public int Capacity = -1;
public ObjectPoolData(int capacity = -1)
{
Capacity = capacity;
if (Capacity == -1) PoolQueue = new Queue<object>();
else PoolQueue = new Queue<object>(capacity);
}
OP数据相关操作
原理同上,只不过不需要处理层物体。
/// <summary>
/// 将对象放进对象池
/// </summary>
public void PushObj(object obj)
{
// 检测是不是超过容量
if (Capacity != -1 && PoolQueue.Count >= Capacity)
{
return;
}
PoolQueue.Enqueue(obj);
}
/// <summary>
/// 从对象池中获取对象
/// </summary>
/// <returns></returns>
public object GetObj()
{
return PoolQueue.Dequeue();
}
public void Desotry(bool pushThisToPool = false)
{
PoolQueue.Clear();
Capacity = -1;
if (pushThisToPool)
{
PoolSystem.PushObject(this);
}
}