文章目录
什么是对象池?对象池有什么用?
频繁创建和销毁对象会造成性能的开销。
创建对象的时候,系统会为这个对象开辟一片新的空间。销毁对象的时候,这个对象会变成内存垃圾,当内存垃圾达到一定程度,就会触发垃圾回收机制(及GC),清理内存垃圾,由于此时在清理垃圾,所以程序有可能就会变卡。
为了改善这个问题,我们就可以使用对象池。使用了它之后,程序的性能就能得到提升不那么容易变卡。
对象池的原理
1、当要创建对象的时候,不直接创建,而是先从对象池里面找,如果对象池里有这种类型的对象,就从对象池中取出来用。如果对象池里面没有这种类型的对象,才创建该对象。
2、当要销毁对象的时候,不直接销毁,而是把这个对象放进对象池里面存着,以便下一次创建对象时使用。
对象池的实现
1、从对象池获取对象
从对象池获取一个对象。如果对象池有,从对象池中取出来用。如果对象池没有,则实例化该对象。
csharp
/// <summary>
/// 对象池
/// </summary>
public class ObjectPool : MonoBehaviour
{
// 这个对象池存储的游戏对象的预制体
public GameObject prefab;
// 对象池最多能容纳多少个游戏对象。负数表示可以容纳无数个。
public int capacity = -1;
// 从这个对象池中取出并正在使用的游戏对象。
public List<GameObject> usedGameObjectList = new List<GameObject>();
// 存在这个对象池中没有被使用的游戏对象。
public List<GameObject> unUsedGameObjectList = new List<GameObject>();
// 这个对象池中正在使用和没有被使用的游戏对象的总数
public int TotalGameObjectCount => usedGameObjectList.Count + unUsedGameObjectList.Count;
/// <summary>
/// 从对象池获取一个对象。
/// 如果对象池有,从对象池中取出来用。
/// 如果对象池没有,则实例化该对象。
/// </summary>
/// <param name="position">位置</param>
/// <param name="rotation">旋转</param>
/// <param name="parent">父级</param>
/// <returns>返回这个对象</returns>
public GameObject Spawn(Vector3 position, Quaternion rotation, Transform parent = null)
{
GameObject go;
// 如果对象池中有,则从对象池中取出来用。
if (unUsedGameObjectList.Count > 0)
{
go = unUsedGameObjectList[0];
unUsedGameObjectList.RemoveAt(0);
usedGameObjectList.Add(go);
go.transform.localPosition = position;
go.transform.localRotation = rotation;
go.transform.SetParent(parent, false);
go.SetActive(true);
}
// 如果对象池中没有,则实例化该对象。
else
{
go = Instantiate(prefab, position, rotation, parent);
usedGameObjectList.Add(go);
}
// 如果该游戏对象身上继承MonoBehaviour的脚本中写了名叫OnSpawn的方法,则会执行它们一次。
go.SendMessage("OnSpawn", SendMessageOptions.DontRequireReceiver);
return go;
}
}
2、回收对象
隐藏指定的游戏对象,并把它回收进对象池中。
csharp
/// <summary>
/// 回收对象
/// </summary>
/// <param name="go">要回收的对象</param>
public void Despawn(GameObject go)
{
if (go == null) return;
// 遍历这个对象池中所有正在使用的游戏对象
for (int i = 0; i < usedGameObjectList.Count; i++)
{
if (usedGameObjectList[i] == go)
{
// 如果这个对象池的容量不为负数,且容纳的游戏对象已经满,则把0号的游戏对象剔除掉
if (capacity >= 0 && usedGameObjectList.Count >= capacity)
{
if (unUsedGameObjectList.Count > 0)
{
Destroy(unUsedGameObjectList[0]);
unUsedGameObjectList.RemoveAt(0);
}
}
// 把游戏对象回收到对象池中
unUsedGameObjectList.Add(go);
usedGameObjectList.RemoveAt(i);
go.SetActive(false);
go.transform.SetParent(transform, false);
// 如果该游戏对象身上继承MonoBehaviour的脚本写了名叫OnDespawn的方法,则在回收的时候,会执行一次。
go.SendMessage("OnDespawn", SendMessageOptions.DontRequireReceiver);
return;
}
}
}
3、回收所有对象
把通过这个对象池生成的所有游戏对象全部隐藏并放入对象池中
csharp
/// <summary>
/// 回收所有对象,把通过这个对象池生成的所有游戏对象全部隐藏并放入对象池中
/// </summary>
public void DespawnAll()
{
int count = usedGameObjectList.Count;
for (int i = 0; i < count; i++)
{
Despawn(usedGameObjectList[0]);
}
}
4、预先往这个对象池中加载指定数量的游戏对象
csharp
/// <summary>
/// 预先往这个对象池中加载指定数量的游戏对象
/// </summary>
/// <param name="amount">预先生成游戏对象数量</param>
public void Preload(int amount = 1)
{
if (prefab == null) return;
if (amount <= 0) return;
for (int i = 0; i < amount; i++)
{
GameObject go = Instantiate(prefab, Vector3.zero, Quaternion.identity);
go.SetActive(false);
go.transform.SetParent(transform, false);
unUsedGameObjectList.Add(go);
go.name = prefab.name;
}
}
5、最终代码
csharp
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 对象池
/// </summary>
public class ObjectPool : MonoBehaviour
{
//这个对象池存储的游戏对象的预制体
public GameObject prefab;
//对象池最多能容纳多少个游戏对象。负数表示可以容纳无数个。
public int capacity = -1;
//从这个对象池中取出并正在使用的游戏对象
public List<GameObject> usedGameObjectsList = new List<GameObject>();
//存在这个对象池中并没有被使用的游戏对象
public List<GameObject> unUsedGameObjectsList = new List<GameObject>();
//这个对象池中正在使用和没有被使用的游戏对象的总数
public int TotalGameObjectsCount { get => usedGameObjectsList.Count + unUsedGameObjectsList.Count; }
/// <summary>
/// <para>从对象池获取一个对象</para>
/// <para>如果对象池中有,则从对象池中取出来用。</para>
/// <para>如果对象池中没有,则实例化该对象。</para>
/// </summary>
/// <param name="position">生成游戏对象的位置</param>
/// <param name="rotation">生成游戏对象的旋转</param>
/// <param name="parent">生成的游戏对象的父物体</param>
/// <returns>返回这个对象</returns>
public GameObject Spawn(Vector3 position, Quaternion rotation, Transform parent = null)
{
GameObject go;
if (unUsedGameObjectsList.Count > 0)
{
go = unUsedGameObjectsList[0];
unUsedGameObjectsList.RemoveAt(0);
usedGameObjectsList.Add(go);
go.transform.localPosition = position;
go.transform.localRotation = rotation;
go.transform.SetParent(parent, false);
go.SetActive(true);
}
else
{
go = Instantiate(prefab, position, rotation, parent);
usedGameObjectsList.Add(go);
}
//如果该游戏对象身上继承Monobehavior的脚本中写了名叫OnSpawn的方法,则会执行它们。
go.SendMessage("OnSpawn", SendMessageOptions.DontRequireReceiver);
return go;
}
/// <summary>
/// <para>隐藏指定的游戏对象,并把它回收进对象池中。</para>
/// </summary>
/// <param name="go">要放入对象池的游戏对象</param>
public void Despawn(GameObject go)
{
if (go == null) return;
//遍历这个对象池中所有正在使用的游戏对象
for (int i = 0; i < usedGameObjectsList.Count; i++)
{
if (usedGameObjectsList[i] == go)
{
//如果这个对象池的容量不为负数,且容纳的游戏对象已经满了,则把0号的游戏对象删掉,确保之后新的游戏对象能放入到池子中。
if (capacity >= 0 && unUsedGameObjectsList.Count >= capacity)
{
if (unUsedGameObjectsList.Count > 0)
{
Destroy(unUsedGameObjectsList[0]);
unUsedGameObjectsList.RemoveAt(0);
}
}
//把游戏对象放入到对象池中
unUsedGameObjectsList.Add(go);
usedGameObjectsList.RemoveAt(i);
//如果该游戏对象身上继承Monobehavior的脚本中写了名叫OnDespawn的方法,则会执行它们。
go.SendMessage("OnDespawn", SendMessageOptions.DontRequireReceiver);
go.SetActive(false);
go.transform.SetParent(transform, false);
return;
}
}
}
/// <summary>
/// 把通过这个对象池生成的所有游戏对象,全部隐藏并放入对象池中
/// </summary>
public void DespawnAll()
{
int count = usedGameObjectsList.Count;
for (int i = 1; i <= count; i++)
{
Despawn(usedGameObjectsList[0]);
}
//清空列表
usedGameObjectsList.Clear();
}
/// <summary>
/// <para>在这个对象池中预加载指定数量的游戏对象。</para>
/// </summary>
/// <param name="amount">要预加载的数量</param>
public void Preload(int amount = 1)
{
if (prefab == null) return;
if (amount <= 0) return;
for (int i = 1; i <= amount; i++)
{
GameObject go = Instantiate(prefab, Vector3.zero, Quaternion.identity);
go.SetActive(false);
go.transform.SetParent(transform, false);
unUsedGameObjectsList.Add(go);
go.name = prefab.name;
}
}
}
封装对象池管理器
1、对象池管理器代码
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 对象池管理器
/// </summary>
public class ObjectPoolsManager : Singleton<ObjectPoolsManager>
{
//所有对象池的父物体
GameObject poolsParent;
//所有对象池共同父物体的名字
readonly string poolsParentName = "ObjectPools";
//当前所有对象池的列表
public List<ObjectPool> objectPoolsList = new List<ObjectPool>();
//键:由对象池生成的并且正在使用的游戏对象
//值:这个游戏对象所属的对象池
public Dictionary<GameObject, ObjectPool> objectsDictionary = new Dictionary<GameObject, ObjectPool>();
/// <summary>
/// <para>从对象池获取一个对象</para>
/// <para>如果对象池中有,则从对象池中取出来用。</para>
/// <para>如果对象池中没有,则实例化该对象。</para>
/// </summary>
/// <param name="prefab">要生成的游戏对象的预制体</param>
/// <param name="position">生成游戏对象的位置。如果没有设置父物体,则是世界坐标。如果设置了父物体,则是局部坐标。</param>
/// <param name="rotation">生成游戏对象的旋转</param>
/// <param name="parent">>生成游戏对象的父物体</param>
/// <returns>返回游戏对象</returns>
public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent = null)
{
if (prefab == null) return null;
//生成对象池的父物体
CreatePoolsParentIfNull();
//通过预制体来查找它所属的对象池
ObjectPool objectPool = FindPoolByPrefabOrCreatePool(prefab);
//从对象池获取一个对象
GameObject go = objectPool.Spawn(position, rotation, parent);
//把生成的游戏对象与它所属的对象池记录到字典
objectsDictionary.Add(go, objectPool);
return go;
}
/// <summary>
/// <para>隐藏指定的游戏对象,并把它回收进对象池中。</para>
/// </summary>
/// <param name="go">要放入对象池的游戏对象</param>
/// <param name="delayTime">延迟多少秒执行</param>
public void Despawn(GameObject go, float delayTime = 0)
{
if (go == null) return;
//开启协程,延迟执行回收到对象池的逻辑。
MonoManager.Instance.StartCoroutine(DespawnCoroutine(go, delayTime));
}
IEnumerator DespawnCoroutine(GameObject go, float delayTime = 0)
{
//等待指定秒数
if (delayTime > 0)
yield return CoroutineTool.WaitForSeconds(delayTime);
//先从象池生成的正在使用的游戏对象的字典中找指定的游戏对象
if (objectsDictionary.TryGetValue(go, out ObjectPool pool))
{
objectsDictionary.Remove(go);
//把这个游戏对象放入找到的对象池
pool.Despawn(go);
}
else
{
//获取这个游戏对象所属的对象池
pool = FindPoolByUsedGameObject(go);
if (pool != null)
pool.Despawn(go);
}
}
/// <summary>
/// 把所有通过对象池生成的对象全部隐藏,并回收进对象池中。
/// </summary>
public void DespawnAll()
{
for (int i = 0; i < objectPoolsList.Count; i++)
{
objectPoolsList[i].DespawnAll();
}
objectsDictionary.Clear();
}
/// <summary>
/// <para>在对象池中预加载指定数量的游戏对象。</para>
/// </summary>
/// <param name="prefab">游戏对象的预制体</param>
/// <param name="amount">要预加载的数量</param>
public void Preload(GameObject prefab, int amount = 1)
{
if (prefab == null) return;
if (amount <= 0) return;
//通过预制体来查找它所属的对象池
ObjectPool pool = FindPoolByPrefabOrCreatePool(prefab);
//预加载指定数量的游戏对象
pool.Preload(amount);
}
/// <summary>
/// <para>返回指定的预制体所属的对象池的容量。</para>
/// <para>如果该对象池不存在,则会创建它,然后返回-1。</para>
/// </summary>
/// <param name="prefab">预制体</param>
public int GetCapacity(GameObject prefab)
{
ObjectPool pool = FindPoolByPrefabOrCreatePool(prefab);
return pool.capacity;
}
/// <summary>
/// <para>设置指定的预制体所属的对象池的容量。</para>
/// <para>如果该对象池不存在,则会创建它,然后再设置它的容量。</para>
/// </summary>
/// <param name="prefab">预制体</param>
/// <param name="capacity">要设置的容量。如果设置为负数,则表示这个对象池可以容纳无数个游戏对象。</param>
public void SetCapacity(GameObject prefab, int capacity = -1)
{
ObjectPool pool = FindPoolByPrefabOrCreatePool(prefab);
pool.capacity = capacity;
}
/// <summary>
///生成对象池的父物体
/// </summary>
void CreatePoolsParentIfNull()
{
if (poolsParent == null)
{
//清空列表和字典,避免上一个场景的数据的影响。
objectPoolsList.Clear();
objectsDictionary.Clear();
//生成一个空物体
poolsParent = new GameObject(poolsParentName);
}
}
/// <summary>
/// <para>查找指定的预制体所属的对象池</para>
/// </summary>
/// <param name="prefab">预制体</param>
ObjectPool FindPoolByPrefab(GameObject prefab)
{
if (prefab == null) return null;
for (int i = 0; i < objectPoolsList.Count; i++)
{
if (objectPoolsList[i].prefab == prefab)
return objectPoolsList[i];
}
return null;
}
/// <summary>
/// <para>查找指定的游戏对象所属的对象池</para>
/// </summary>
/// <param name="go">要查找的游戏对象</param>
ObjectPool FindPoolByUsedGameObject(GameObject go)
{
if (go == null) return null;
for (int i = 0; i < objectPoolsList.Count; i++)
{
ObjectPool pool = objectPoolsList[i];
//遍历每一个对象池正在使用的所有游戏对象
for (int j = 0; j < pool.usedGameObjectsList.Count; j++)
{
if (pool.usedGameObjectsList[j] == go)
return pool;
}
}
return null;
}
/// <summary>
/// <para>通过预制体来查找它所属的对象池</para>
/// </summary>
/// <param name="prefab">预制体</param>
ObjectPool FindPoolByPrefabOrCreatePool(GameObject prefab)
{
//生成对象池的父物体
CreatePoolsParentIfNull();
//查找并返回该预制体所属的对象池
ObjectPool objectPool = FindPoolByPrefab(prefab);
//如果该对象池不存在,则创建一个。
if (objectPool == null)
{
//创建一个对象池
objectPool = new GameObject($"ObjectPool-{prefab.name}").AddComponent<ObjectPool>();
//设置这个对象池所管理的预制体
objectPool.prefab = prefab;
//把生成的对象池放到父物体中,方便管理。
objectPool.transform.SetParent(poolsParent.transform);
//记录这个对象池
objectPoolsList.Add(objectPool);
}
return objectPool;
}
}
2、测试调用
csharp
public class PoolTest : MonoBehaviour
{
private void OnGUI()
{
if (GUI.Button(new Rect(0, 0, 150, 70), "生成游戏对象"))
{
GameObject prefab = ResourcesManager.Instance.Load<GameObject>("Prefabs/Cube");
GameObject go = ObjectPoolsManager.Instance.Spawn(prefab, transform.position, Quaternion.identity, transform);
go.GetComponent<Rigidbody>().AddForce(Vector3.forward * 1000f);//加力
//5秒后回收
ObjectPoolsManager.Instance.Despawn(go, 5f);
}
}
}
效果
3、生成和回收游戏对象时自动调用的方法
在预制体添加脚本,书写生成和回收游戏对象时自动调用的方法,可以在上面书写对象初始化方法,重置对象参数
csharp
public class Cube : MonoBehaviour {
//生成游戏对象时自动调用
void OnSpawn(){
Debug.Log("生成游戏对象");
}
//回收游戏对象时自动调用
void OnDespawn(){
Debug.Log("回收游戏对象");
//重置参数
transform.position = Vector3.zero;
GetComponent<Rigidbody>().velocity = Vector3.zero;
}
}
效果
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~