Unity 对象池

版权声明:辛辛苦苦写的,转载请注明出处噢 https://blog.csdn.net/wangjiangrong/article/details/80196676

概念

我们先讲讲对象池是什么,有什么用。在游戏的制作过程中,我们可能会碰到这样的情况,就像现在最火的吃鸡游戏一样,我们有一把枪,开枪的时候射出子弹。每个子弹即一个对象,正常情况,我们的处理方式可能会是,每开一枪,就GameObject.Instantiate()一个新的子弹,当子弹到达极限距离的时候再GameObject.Destroy()销毁它。假设有射出1000发子弹,我们就会执行1000次这样的操作,然而在Unity中Instantiate和Destroy操作,不仅影响性能还容易产生内存碎片,总之就是要尽量少做这种操作。这个时候就有对象池这个概念。

所谓对象池,就是针对需要经常生成消失的对象。我们在对象需要消失的时候不Destroy而是SetActive(false),然后放入池子中(Queue),当需要再次显示一个新的对象的时候,先去池子中看有没有隐藏的对象,有就取出SetActive(true),若池子里没有可用的则再Instantiate。

还是上面的例子,假设我们玩家能看见的子弹只有10发,那么在连续开枪1000次的时候,我们生成第一发子弹的时候,此时对象池为空,我们Instantiate一个子弹,第二到第十发子弹的时候同理,再Instantiate九个子弹,第十一发的时候,前十发子弹还是处于显示状态,池子中依旧为空,我们继续Instantiate一个子弹,当这个子弹射出的时候,我们的第一发子弹就应该无法看见了,我们即可SetActive(false),然后放入对象池中。当第12发子弹的时候,我们这个时候对象池里就有一个可用对象,即第一发子弹,我们取出并显示它,然后将它的起始坐标更新到枪口。第13发子弹之后就如此循环。这样,我们在1000发子弹的时候只需要执行11次Instantiate和Destroy操作,起到优化作用。

实现

注:实现的方式有很多,下面只是个人的一种方式,大家可以针对自己的需求任意修改。

了解了上面的概念之后,实现起来就很方便了。首先我们先生成一个对象池的class,里面的内容就很简单,首先有一个Queue用来存放池子中的对象,然后实现两个方法,一个取对象,一个销毁对象,取对象的时候,若池子中有可用对象则取出一个,若没有则Instantiate一个。销毁对象即将对象SetActive(false)并且放入池子中。代码如下

public class BaseGameObjectPool {

    /// <summary>
    /// 队列,存放对象池中没有用到的对象,即可分配对象
    /// </summary>
    protected Queue m_queue;
    /// <summary>
    /// 对象池中存放最大数量
    /// </summary>
    protected int m_maxCount;
    /// <summary>
    /// 对象预设
    /// </summary>
    protected GameObject m_prefab;
    /// <summary>
    /// 该对象池的transform
    /// </summary>
    protected Transform m_trans;
    /// <summary>
    /// 每个对象池的名称,当唯一id
    /// </summary>
    protected string m_poolName;
    /// <summary>
    /// 默认最大容量
    /// </summary>
    protected const int m_defaultMaxCount = 10;

    public BaseGameObjectPool() {
        m_maxCount = m_defaultMaxCount;
        m_queue = new Queue();
    }

    public virtual void Init(string poolName, Transform trans) {
        m_poolName = poolName;
        m_trans = trans;
    }

    public GameObject prefab {
        set {
            m_prefab = value;
        }
    }

    public int maxCount {
        set {
            m_maxCount = value;
        }
    }

    /// <summary>
    /// 生成一个对象
    /// </summary>
    /// <param name="position">起始坐标</param>
    /// <param name="lifetime">对象存在的时间</param>
    /// <returns>生成的对象</returns>
    public virtual GameObject Get(Vector3 position, float lifetime) {
        if(lifetime < 0) {
            //lifetime<0时,返回null  
            return null;
        }
        GameObject returnObj;
        if(m_queue.Count > 0) {
            //池中有待分配对象
            returnObj = (GameObject)m_queue.Dequeue();
        } else {
            //池中没有可分配对象了,新生成一个
            returnObj = GameObject.Instantiate(m_prefab) as GameObject;
            returnObj.transform.SetParent(m_trans);
            returnObj.SetActive(false);
        }
        //使用PrefabInfo脚本保存returnObj的一些信息
        GameObjectPoolInfo info = returnObj.GetComponent<GameObjectPoolInfo>();
        if(info == null) {
            info = returnObj.AddComponent<GameObjectPoolInfo>();
        }
        info.poolName = m_poolName;
        if(lifetime > 0) {
            info.lifetime = lifetime;
        }
        returnObj.transform.position = position;
        returnObj.SetActive(true);
        return returnObj;
    }

    /// <summary>
    /// “删除对象”放入对象池
    /// </summary>
    /// <param name="obj">对象</param>
    public virtual void Remove(GameObject obj) {
        //待分配对象已经在对象池中  
        if(m_queue.Contains(obj)) {
            return;
        }
        if(m_queue.Count > m_maxCount) {
            //当前池中object数量已满,直接销毁
            GameObject.Destroy(obj);
        } else {
            //放入对象池,入队
            m_queue.Enqueue(obj);
            obj.SetActive(false);
        }
    }

    /// <summary>
    /// 销毁该对象池
    /// </summary>
    public virtual void Destroy() {
        m_queue.Clear();
    }
}

同时,这也是个基类,针对一些特殊的对象,可能会有些独特的操作,我们可以单独生成一个类继承于它,进行修改。举个例子,下面这个对象池的预设取自AssetBundle:

public class CubePool : BaseGameObjectPool {

    PrefabAssetBundleItem m_cubeAsset;

    public CubePool() : base() {
    }

    public override void Init(string poolName, Transform trans) {
        base.Init(poolName, trans);
        m_cubeAsset = new PrefabAssetBundleItem("", "Cube");
        m_cubeAsset.Load();
        m_prefab = m_cubeAsset.prefab;
    }

    public override GameObject Get(Vector3 position, float lifetime) {
        lifetime = 3;
        return base.Get(position, lifetime);
    }

    public override void Destroy() {
        base.Destroy();
        m_cubeAsset.Destroy();
    }
}

上面的代码大家会发现,在生成一个新对象的时候,我们给对象添加了一个GameObjectPoolInfo的自定义组件。前面也提到了,对象池主要针对显示之后一段时间就会消失的对象。所以这个自定义组件的作用就是设置了一个显示时间,当时间到了之后将该对象加入对象池。

public class GameObjectPoolInfo : MonoBehaviour {
    /// <summary>
    /// 对象显示的持续时间,若=0,则不隐藏
    /// </summary>
    [HideInInspector] public float lifetime = 0;
    /// <summary>
    /// 所属对象池的唯一id
    /// </summary>
    [HideInInspector] public string poolName;

    WaitForSeconds m_waitTime;

    void Awake() {
        if(lifetime > 0) {
            m_waitTime = new WaitForSeconds(lifetime);
        }
    }

    void OnEnable() {
        if(lifetime > 0) {
            StartCoroutine(CountDown(lifetime));
        }
    }

    IEnumerator CountDown(float lifetime) {
        yield return m_waitTime;
        //将对象加入对象池
        GameObjectPoolManager.instance.RemoveGameObject(poolName, gameObject);
    }
}

同时我们还需要生成一个对象池的管理类,因为我们可能会有很多的对象池,比如AK的子弹是一类对象池,M4的子弹是另一类。我们需要将这些对象池都存在一个字典当中,方便后续处理

//SingleClass就是单例,大家可以自己来实现
public class GameObjectPoolManager : SingleClass<GameObjectPoolManager> {

    /// <summary>
    /// 存放所有的对象池
    /// </summary>
    Dictionary<string, BaseGameObjectPool> m_poolDic;
    /// <summary>
    /// 对象池在场景中的父控件
    /// 本例中将对象池的对象都放在了一个单独的gameobject下,大家可以按照自己的需求来乱放
    /// </summary>
    Transform m_parentTrans;

    public GameObjectPoolManager() {
        m_poolDic = new Dictionary<string, BaseGameObjectPool>();
        //生成一个新的GameObject存放所有的对象池对象
        GameObject go = new GameObject("GameObjectPoolManager");
        m_parentTrans = go.transform;
    }

    /// <summary>
    /// 创建一个新的对象池
    /// </summary>
    /// <typeparam name="T">对象池类型</typeparam>
    /// <param name="poolName">对象池名称,唯一id</param>
    /// <returns>对象池对象</returns>
    public T CreatGameObjectPool<T>(string poolName) where T : BaseGameObjectPool, new() {
        if(m_poolDic.ContainsKey(poolName)) {
            return (T)m_poolDic[poolName];
        }
        GameObject obj = new GameObject(poolName);
        obj.transform.SetParent(m_parentTrans);
        T pool = new T();
        pool.Init(poolName, obj.transform);
        m_poolDic.Add(poolName, pool);
        return pool;
    }

    /// <summary>
    /// 从对象池中取出新的对象
    /// </summary>
    /// <param name="poolName">对象池名称</param>
    /// <param name="position">对象新坐标</param>
    /// <param name="lifeTime">对象显示时间</param>
    /// <returns>新对象</returns>
    public GameObject GetGameObject(string poolName, Vector3 position, float lifeTime) {
        if(m_poolDic.ContainsKey(poolName)) {
            return m_poolDic[poolName].Get(position, lifeTime);
        }
        return null;
    }

    /// <summary>
    /// 将对象存入对象池中
    /// </summary>
    /// <param name="poolName">对象池名称</param>
    /// <param name="go">对象</param>
    public void RemoveGameObject(string poolName, GameObject go) {
        if(m_poolDic.ContainsKey(poolName)) {
            m_poolDic[poolName].Remove(go);
        }
    }

    /// <summary>
    /// 销毁所有对象池操作
    /// </summary>
    public void Destroy() {
        m_poolDic.Clear();
        GameObject.Destroy(m_parentTrans);
    }
}

接着我们就可以使用它了,例子里我点击按钮随机生成对象

public class GameObjectPoolDemo : MonoBehaviour {

    [SerializeField] Button m_addSphereBtn;
    [SerializeField] Button m_addCubeBtn;
    [SerializeField] GameObject m_spherePrefab;

    BaseGameObjectPool m_spherePool;
    CubePool m_cubePool;

    void Start () {
        m_spherePool = GameObjectPoolManager.instance.CreatGameObjectPool<BaseGameObjectPool>("SpherePool");
        m_spherePool.prefab = m_spherePrefab;

        m_cubePool = GameObjectPoolManager.instance.CreatGameObjectPool<CubePool>("CubePool");

        m_addSphereBtn.onClick.AddListener(() => {
            float x = Random.Range(-15, 15);
            float y = Random.Range(-10, 10);
            m_spherePool.Get(new Vector3(x, y, 0), 1);
            //GameObjectPoolManager.instance.GetGameObject("SpherePool", new Vector3(x, y, 0), 1);
        });

        m_addCubeBtn.onClick.AddListener(() => {
            float x = Random.Range(-15, 15);
            float y = Random.Range(-10, 10);
            GameObjectPoolManager.instance.GetGameObject("CubePool", new Vector3(x, y, 0), 1);
        });
    }
}

效果如下,可以看见虽然屏幕中看着像一直生成新的小球方块,但是真正生成的对象其实就几个。


猜你喜欢

转载自blog.csdn.net/wangjiangrong/article/details/80196676
今日推荐