文章目录
- 前言
- 不使用对象池
- 使用官方内置对象池
- 应用
- 自制对象池
- 总结
- 源码
- 参考
- 完结
前言
发明对象池的人绝对是个天才,游戏中我们常常会遇到,频繁创建和销毁大量相同对象的场景,例如敌人子弹
如果我们不做任何处理,只是单纯的创建和销毁,可能会导致内存泄露,性能下降和卡顿等问题
Instantiate(gameobject)
Destroy(gameobject)
对象池的出现,减少了频繁,创建和销毁对象带来的成本,实现对象的循环和复用
在对象池设计理念中,我们不再单纯的创建和销毁,创建对象时,我们会将对象存入对象池中,需要使用对象时,我们从池子中获取对象,当不需要对象时,我们再将对象存入对象池中,以实现对象的循环复用,减少频繁创建销毁象带来的成本
幸运的是,从2021年3月
版本后,unity官方为我门内置了对象池
,接下来的教程我将以一个简单的例子,带大家熟悉了解使用官方的对象池
不使用对象池
制作一个金币预制体,挂载刚体、碰撞和Coin脚本
新增一个空对象,用于生成金币,挂载CoinPool脚本
Coin脚本代码,金币接触地面销毁
private void OnCollisionEnter2D(Collision2D collision) // 碰撞检测
{
Debug.Log(collision.gameObject.layer);
if (collision.gameObject.layer == LayerMask.NameToLayer("Ground"))
{
//销毁
Destroy(gameObject);
}
}
CoinPool脚本代码
public GameObject coin; //金币预制体
public float time; //金币生成间隔时间
void Start()
{
StartCoroutine(go());
}
IEnumerator go()
{
while (true)
{
//协程每time秒执行一次
CreateCoin();
yield return new WaitForSeconds(time);
}
}
//生成金币
private void CreateCoin()
{
GameObject gb = Instantiate(coin, transform);//在当前对象处生成一个金币
gb.transform.position = new Vector3(Random.Range(-80f, -67f), Random.Range(3f, 9f));//随机生成位置
}
效果,可以看到我们只是单纯的创建和销毁金币
使用官方内置对象池
一、命名空间
using UnityEngine.Pool;
二、构造方法
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
);
参数列表解释
每个参数等号右边代表默认值,即第一个参数为必填项。
1.Func createFunc
需填入一个带T类型返回值的方法,即自定义新建对象时的操作
2.Action actionOnGet, Action actionOnRelease, Action actionOnDestroy
分别为出池、进池、销毁的响应事件。填入自定义方法名即可,用于拓展相应操作,比如在actionOnDestroy中通过Destroy()方法将因为池满而不能入池的对象直接删除
3.bool collectionCheck
是否在进池前检查对象是否已经存在池中
4.int defaultCapacity, int maxSize
初始容量,最大容量
三、属性
1.int CountAll
所有的对象数,三个属性都为只读初始值为0,经测试,已知每调用一次createFunc委托该值就会+1
2.int CountActive
被启用的对象数,已知每调用一次Release()方法就会-1,Get()方法+1
3.int CountInactive
未被启用的对象数,已知每调用一次Release()方法就会+1,Get()方法-1,最大值为池的最大容量
四、常用方法
1.T Get()
出池,返回一个池中对象。如果池为空,则先调用createFunc,再出池
2.void Relese(T element)
进池,将对象element加入池。如果池已达到最大容量,则不入池并触发actionOnDestroy事件
3.void Clear()
清空池
应用
使用官方内置对象池修改前面的例子
Coin脚本
using UnityEngine;
using UnityEngine.Pool;
public class Coin : MonoBehaviour
{
public ObjectPool<GameObject> pool;
private void OnCollisionEnter2D(Collision2D collision) // 碰撞检测
{
Debug.Log(collision.gameObject.layer);
if (collision.gameObject.layer == LayerMask.NameToLayer("Ground"))
{
//销毁
// Destroy(gameObject);
pool.Release(gameObject);
}
}
}
CoinPool脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
public class CoinPool : MonoBehaviour
{
public GameObject coin; //金币预制体
public float time; //金币生成间隔时间
public ObjectPool<GameObject> pool;
public int poolMaxSize;//对象池最大容量
void Start()
{
//是否在进池前检查对象是否已经存在池中,初始容量,最大容量
pool = new ObjectPool<GameObject>(createFunc, actionOnGet, actionOnRelease, actionOnDestroy, true, 10, poolMaxSize);
StartCoroutine(go());
}
IEnumerator go()
{
while (true)
{
//协程每time秒执行一次
CreateCoin();
Debug.Log("总对象数:"+pool.CountAll);
Debug.Log("启用的对象数:"+pool.CountActive);
Debug.Log("未启用的对象数:"+pool.CountInactive);
yield return new WaitForSeconds(time);
}
}
//生成金币
private void CreateCoin()
{
// GameObject gb = Instantiate(coin, transform);//在当前对象处生成一个金币
GameObject gb = pool.Get();
gb.transform.position = new Vector3(Random.Range(-80f, -67f), Random.Range(3f, 9f));//随机生成位置
}
// 需填入一个带T类型返回值的方法,即自定义新建对象时的操作
public GameObject createFunc()
{
GameObject obj = Instantiate(coin, transform);
obj.GetComponent<Coin>().pool = pool;//将pool和Coin的pool赋值为同一个
return obj;
}
void actionOnGet(GameObject gameObject)
{
gameObject.SetActive(true);//显示敌人
Debug.Log(gameObject.name + "出池");
}
void actionOnRelease(GameObject gameObject)
{
gameObject.SetActive(false);//隐藏敌人
Debug.Log(gameObject.name + "进池");
}
void actionOnDestroy(GameObject gameObject)
{
Debug.Log("池已满," + gameObject.name + "被销毁");
Destroy(gameObject);
}
}
效果,可以看到我们不再是单纯的创建和销毁金币,而是开启和关闭复用前面生成的金币
自制对象池
新增对象池脚本ObjectPool
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool
{
private static ObjectPool instance; // 单例模式
// /**
// * 我们希望不同的物体可以被分开存储,在这种情况下使用字典是最合适的
// * 所以声明一个字典objectPool作为对象池主体,以字符串类型的物体的名字作为key
// * 使用队列存储物体来作为value,这里使用队列只是因为入队和出队的操作较为方便,也可以换成其他集合方式
// * 然后实例化这个字典以备后续使用
// * /
private Dictionary<string, Queue<GameObject>> objectPool = new Dictionary<string, Queue<GameObject>>(); // 对象池字典
private GameObject pool; // 为了不让窗口杂乱,声明一个对象池父物体,作为所有生成物体的父物体
public static ObjectPool Instance // 单例模式
{
get
{
if (instance == null)
{
instance = new ObjectPool();
}
return instance;
}
}
public GameObject GetObject(GameObject prefab) // 从对象池中获取对象
{
GameObject _object;
if (!objectPool.ContainsKey(prefab.name) || objectPool[prefab.name].Count == 0) // 如果对象池中没有该对象,则实例化一个新的对象
{
_object = GameObject.Instantiate(prefab);
PushObject(_object); // 将新的对象加入对象池
if (pool == null)
pool = new GameObject("ObjectPool"); // 如果对象池父物体不存在,则创建一个新的对象池父物体
GameObject childPool = GameObject.Find(prefab.name + "Pool"); // 查找该对象的子对象池
if (!childPool)
{
childPool = new GameObject(prefab.name + "Pool"); // 如果该对象的子对象池不存在,则创建一个新的子对象池
childPool.transform.SetParent(pool.transform); // 将该子对象池加入对象池父物体中
}
_object.transform.SetParent(childPool.transform); // 将新的对象加入该对象的子对象池中
}
_object = objectPool[prefab.name].Dequeue(); // 从对象池中取出一个对象
_object.SetActive(true); // 激活该对象
return _object; // 返回该对象
}
public void PushObject(GameObject prefab) // 将对象加入对象池中
{
//获取对象的名称,因为实例化的物体名都会加上"(Clone)"的后缀,需要先去掉这个后缀才能使用名称查找
string _name = prefab.name.Replace("(Clone)", string.Empty);
if (!objectPool.ContainsKey(_name))
objectPool.Add(_name, new Queue<GameObject>()); // 如果对象池中没有该对象,则创建一个新的对象池
objectPool[_name].Enqueue(prefab); // 将对象加入对象池中
prefab.SetActive(false); // 将对象禁用
}
}
Coin脚本
using UnityEngine;
using UnityEngine.Pool;
public class Coin : MonoBehaviour
{
private void OnCollisionEnter2D(Collision2D collision) // 碰撞检测
{
// Debug.Log(collision.gameObject.layer);
if (collision.gameObject.layer == LayerMask.NameToLayer("Ground"))
{
//销毁
// Destroy(gameObject);
// pool.Release(gameObject);
ObjectPool.Instance.PushObject(gameObject);
}
}
}
CoinPool脚本
using System.Collections;
using UnityEngine;
public class CoinPool : MonoBehaviour
{
public GameObject coin; //金币预制体
public float time; //金币生成间隔时间
void Start()
{
StartCoroutine(go());
}
IEnumerator go()
{
while (true)
{
//协程每time秒执行一次
CreateCoin();
yield return new WaitForSeconds(time);
}
}
//生成金币
private void CreateCoin()
{
// GameObject gb = Instantiate(coin, transform);//在当前对象处生成一个金币
// GameObject gb = pool.Get();
GameObject gb = ObjectPool.Instance.GetObject(coin);
gb.transform.position = new Vector3(Random.Range(-80f, -67f), Random.Range(3f, 9f));//随机生成位置
}
}
效果
总结
Unity官方内置对象池和自己写对象池都有各自的优缺点,具体取决于你的需求和项目的规模。
如果你的游戏或应用程序很简单,并且对象池的需求较小,那么使用Unity官方内置的对象池是一个方便和快速的选择。Unity的对象池方法已经经过优化,并且与引擎的其他功能集成得很好,使用起来也非常简单。你可以直接使用ObjectPool
类来创建和管理对象池,而无需自己编写额外的代码。
然而,当你的游戏或应用程序更复杂,并且需要更高级的对象池功能时,可能会需要自己编写对象池。自己编写对象池可以根据项目的具体需求进行定制化,以满足特定的性能要求。你可以实现自己的池管理系统、缓存策略和对象回收机制,以及其他高级功能,如对象优先级、预加载等。
总的来说,如果你的项目规模较小并且简单,使用Unity官方内置对象池是一个方便的选择。如果你的项目更加复杂或有特定的需求,编写自己的对象池可能更适合。最佳选择取决于你的项目需求、时间和资源限制,以及你对对象池功能的具体要求。
如果是我的话,我还是会选择自己写对象池脚本,因为这样可以保证一定的可控性、复用性和可扩展性。
好多钱,快去捡
源码
稍后整理好了,我会放上来
参考
【视频】https://www.bilibili.com/video/BV1Su411E7b2
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~