Unity程序基础框架(二)对象池

2.缓存池模块——对象池

为什么使用对象池?

在我们开发中,往往会遇见需要不断创建和销毁同一物体的情况。(如飞机大战,许多FPSs游戏,三消类游戏等),这时我们系统不断的实例化资源和销毁资源对于内存以及性能的消耗是非常大的。对于这种我们可以使用对象池技术进行优化。效果十分明显。

适用范围:有大量的物体需要被不断的创建和销毁的时候。
关键点:从对象池中放入对象。从对象池中取出对象。
关键思想:相比于lInstantiate和Destroy,不断的实例化和销毁,对象池只是对它们实例化出的对象进行激活和失活的处理。
使用何种数据类型来对这些对象进行储存。

在这个游戏中,点击鼠标可以发射子弹,但是这不只是一个物体,还包括弹壳,子弹的特效,墙上的弹痕特效。一把枪不停的发射子弹,需要创建很多物体,几秒钟之后又被销毁。
当游戏场景有大量物体需要被创建和销毁的时候,就是使用Instance和destory方法,这样的方式是比较消耗内存的。
image

第一颗子弹被创建的时候,就开辟了一个空间,第二个、第三个、依次创建,内存依次开辟空间,几秒钟后,新创建的第一个子弹被销毁了,但是被销毁并不是在内存中被销毁了,而只是链接被销毁了,占用的内存并没有被清理,
image

当内存被占满之后,C#中有GC,GC开始工作,自动清除被占满的空间。它会先检查前面的空间,那一个地方没有被指向,如果没有被指向,那这个区域就是变成垃圾了,可以被销毁了。GC在计算过程中需要算法,耗费时间。如果游戏时产生大量物体,而卡顿,有可能就是因为GC在工作。

问题:不要游戏卡顿,如何优化?

分析解答:开辟一定的内存空间,让物体失活但是并不清除此部分空间,
循环利用。当需要创建新物体时,检查这部分空间是否有失活的物体,
有则激活,没有再去开辟新的空间。

image

把对象池看错大池子,池子里面分门别类很多东西,有子弹,弹痕,火光等等,把这些当做抽屉,子弹抽屉里存放很多子弹Obeject,我们一般用List泛型集合这种数据结构存储,一般不用数组,数组有很大的局限性。 对于整个池子来说,应该用字典类Dictionary.Dictionary<string,List> 键指向抽屉的名字,值应该指向List泛型集合。

image

对象池是单例模式,从池子中拿东西,如果对象池中没有这个东西,则Instance 新开辟一个空间(抽屉),存放这个东西。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 缓存池模块(对象池)
/// 1.Dictionary  List
/// 2.GameObject和Resources类
/// </summary>
public class PoolMgr : MonoBehaviour//缓存池为单例
{//缓存池容器
    public Dictionary<string,List<GameObject>> poolDic=new Dictionary<string,List<GameObject>>();

    //往外拿东西
    public GameObject GetObj(string pathName)
    {
        GameObject obj = null;
        if (poolDic.ContainsKey(pathName) && poolDic[pathName].Count > 0)//有抽屉且抽屉有东西
        {
            obj = poolDic[pathName][0];       //拿到该物体,只是赋值
            poolDic[pathName].RemoveAt(0);  //移除该物体
        }
        else {
            obj = GameObject.Instantiate(Resources.Load<GameObject>(pathName)); //实例化对象
            obj.name = pathName;   //改对象名字  使之与抽屉同名,方便去
        }
        obj.SetActive(true);  //激活物体
            return obj;
    }

    /// <summary>
    /// 暂时不用的东西放入缓存池
    /// </summary>
    /// <param name="pathName"></param>
    /// <param name="obj"></param>
    public void PushObj(string pathName, GameObject obj)
    {
        obj.SetActive(false);  //失活物体
        //里面有抽屉
        if (poolDic.ContainsKey(pathName))
        {
            poolDic[pathName].Add(obj);   //放入该物体 
        }
        else  //没有抽屉
        {
            poolDic.Add(pathName, new List<GameObject>() { obj });  //声明一个抽屉且把该物体放入
        }
    }

}

poolDic[pathName].Add(obj);
poolDic[pathName]是指向的泛型集合

做一个测试

1.设置
创建文件夹和两个预制体. 注意预制体的名称和文件夹的名字要对应。
image

2.exercises.cs把这个脚本挂载到游戏场景中的物体上(比如Camera)
鼠标左键点击会产生Cube,右键会产生Sphere。(新创建的物体可能会互相遮挡,通过面板也可以看到)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class exercises : MonoBehaviour
{
    void Update()
    { if (Input.GetMouseButtonDown(0))
        {
            PoolMgr.Instance.GetObj("Prefab/Cube");//拿物体 
        }
        else if (Input.GetMouseButtonDown(1))
        {
            PoolMgr.Instance.GetObj("Prefab/Sphere");   //  拿物体 
        }
    }
}


image

3.一段时间后物体自动失活,回收到对象池
把脚本挂载在到要会被失活的物体预制体上(Cube和Sphere都要挂载)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/*该脚本挂载在对象池游戏物体身上,
作用:一段时间后该物体自动失活,回收到对象池
 */
public class DelayPush : MonoBehaviour
{
    private void OnEnable()  //当对象激活时,会进入的生命周期函数
    {
        Invoke("Push", 1);   //invok不接受有参数的方法
    }
    void Push()
    {
        PoolMgr.Instance.PushObj(this.gameObject.name, this.gameObject);
    }
}

PoolMgr.Instance.PushObj(this.gameObject.name, this.gameObject);
能够把物体放到对应的抽屉中 是因为PoolMgr中改了对象名,使之与抽屉同名

image

4.把场景中的东西和缓存池中的东西分开
之前的代码运行之后,在Hierarchy窗口出现了很多物体,如果游戏物体特别多,就把窗口占满了。可以给缓存池中的物体设置一个父对象。

先弄一个根节点
private GameObject poolObj; //对象池物体的根节点

 public void PushObj(string pathName, GameObject obj)
    {
        if (poolObj == null) //如果根节点不存在 则创造一个
            poolObj = new GameObject("Pool");
        obj.transform.parent = poolObj.transform; //设置父对象为根节点

        obj.SetActive(false);  //失活物体
....

在向外拿东西的时候,先激活物体,再断开父子关系。

obj.transform.parent = null;    //断开了父子关系

image

完整代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 缓存池模块(对象池)
/// 1.Dictionary  List
/// 2.GameObject和Resources类
/// </summary>
public class PoolMgr : Singleton <PoolMgr>//缓存池为单例
{//缓存池容器
    public Dictionary<string,List<GameObject>> poolDic=new Dictionary<string,List<GameObject>>();
    private GameObject poolObj; //对象池物体的根节点
    //往外拿东西
    public GameObject GetObj(string pathName)
    {
        GameObject obj = null;
        if (poolDic.ContainsKey(pathName) && poolDic[pathName].Count > 0)//有抽屉且抽屉有东西
        {
            obj = poolDic[pathName][0];       //拿到该物体,只是赋值
            poolDic[pathName].RemoveAt(0);  //移除该物体
        }
        else {
            obj = GameObject.Instantiate(Resources.Load<GameObject>(pathName)); //实例化对象
            obj.name = pathName;   //改对象名字  使之与抽屉同名,方便去
        }
        obj.SetActive(true);  //激活物体
        obj.transform.parent = null;    //断开了父子关系
        return obj;
    }

    /// <summary>
    /// 暂时不用的东西放入缓存池
    /// </summary>
    /// <param name="pathName"></param>
    /// <param name="obj"></param>
    public void PushObj(string pathName, GameObject obj)
    {
        if (poolObj == null)
            poolObj = new GameObject("Pool");
        obj.transform.parent = poolObj.transform; //设置父对象为根节点

        obj.SetActive(false);  //失活物体
        //里面有抽屉
        if (poolDic.ContainsKey(pathName))
        {
            poolDic[pathName].Add(obj);   //放入该物体 
        }
        else  //没有抽屉
        {
            poolDic.Add(pathName, new List<GameObject>() { obj });  //声明一个抽屉且把该物体放入
        }
    }

}

5.清空缓存池
在切换场景的是时候,往往会清空缓存池,

/// <summary>
    /// 清空缓存池,用在切换场景
    /// </summary>
    public void Clear()
    {
        poolDic.Clear();
        poolObj = null;
    }

6.想要知道对象池中,某种物体有多少个(即抽屉里面物体个数),所以可以再在池子中进行一次物体分类,比如Prefab/Cube,Prefab/Sphere

image

原代码: //缓存池容器 public Dictionary<string,List<GameObject>> poolDic=new Dictionary<string,List<GameObject>>();
源代码是 Dictionary<键Key,值Value> 键是抽屉名字,值是 泛型集合,可是现在对值有了其他要求:希望这个抽屉有父节点,并且有其他的更多属性的设置 。把值改为类的类型,能增加更多属性,在这个类中包含List泛型集合来存放数据。

把这个类命名为PoolDatel类,只负责管理抽屉,相当于是小池子。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PoolData //不需要继承
{
    public GameObject fatherObj;  //抽屉中对象挂载的父亲点
    public List<GameObject> poolList;  //对象的容器

    public PoolData(GameObject obj, GameObject poolObj)//构造函数
    {
        //给抽屉创建一个父对象,并且把他作为对象池的子物体
        fatherObj = new GameObject(obj.name);
        fatherObj.transform.parent = poolObj.transform;

        poolList = new List<GameObject>();
        PushObj(obj);
    }
    /// <summary>
    /// 往抽屉中放物体
    /// </summary>
    /// <param name="obj"></param>
    public void PushObj(GameObject obj)
    {
        obj.SetActive(false);    //物体失活
        poolList.Add(obj);          //存起来
        obj.transform.parent = fatherObj.transform;   //设置父对象

    }
    /// <summary>
    /// 抽屉中取物体
    /// </summary>
    /// <returns></returns>
    public GameObject GetObj()
    {
        GameObject obj = null;
        obj = poolList[0];   //取出第一个
        poolList.RemoveAt(0);
        obj.SetActive(true);
        obj.transform.parent = null;   //断开父子关系
        return obj;
    }
}

有了抽屉且有东西,拿东西、访问的时候,直接通过键名访问到 值 即PoolData类,类中有GetObj方法。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 缓存池模块(对象池)
/// 1.Dictionary  List
/// 2.GameObject和Resources类
/// </summary>
public class PoolMgr : Singleton <PoolMgr>//缓存池为单例
{//缓存池容器
    public Dictionary<string,PoolData> poolDic=new Dictionary<string,PoolData>();
    private GameObject poolObj; //对象池物体的根节点
    //往外拿东西
    public GameObject GetObj(string pathName)
    {
        GameObject obj = null;
        if (poolDic.ContainsKey(pathName) && poolDic[pathName].poolList.Count > 0)//有抽屉且抽屉有东西
        {
            //obj = poolDic[pathName][0];       //拿到该物体,只是赋值
            //poolDic[pathName].RemoveAt(0);  //移除该物体

            obj = poolDic[pathName].GetObj();
        }
        else {
            obj = GameObject.Instantiate(Resources.Load<GameObject>(pathName)); //实例化对象
            obj.name = pathName;   //改对象名字  使之与抽屉同名,方便去
        }
        obj.SetActive(true);  //激活物体
        obj.transform.parent = null;    //断开了父子关系
        return obj;
    }

    /// <summary>
    /// 暂时不用的东西放入缓存池
    /// </summary>
    /// <param name="pathName"></param>
    /// <param name="obj"></param>
    public void PushObj(string pathName, GameObject obj)
    {
        if (poolObj == null)
            poolObj = new GameObject("Pool");
        obj.transform.parent = poolObj.transform; //设置父对象为根节点

        obj.SetActive(false);  //失活物体
        //里面有抽屉
        if (poolDic.ContainsKey(pathName))
        {
            poolDic[pathName].PushObj(obj);   //放入该物体  通过 ‘键’访问‘类’调用方法
        }
        else  //没有抽屉
        {
            poolDic.Add(pathName, new PoolData(obj, poolObj) );  //声明一个抽屉且把该物体放入
        }
    }
    /// <summary>
    /// 清空缓存池,用在切换场景
    /// </summary>
    public void Clear()
    {
        poolDic.Clear();
        poolObj = null;
    }
}


poolDic[pathName].PushObj(obj); //放入该物体 通过 ‘键’访问‘类’调用方法

最后效果就是 把一个功能相对完善的对象池完成了。从池子中拿东西,一秒钟失活,物体分门别类,放在各自的父物体下面的。

posted @ 2021-09-12 12:29  专心Coding的程侠  阅读(354)  评论(0编辑  收藏  举报