Unity开发之对象池:优化性能的利器

一、引言

       在游戏开发中,性能优化一直是开发者关注的焦点之一。为了提高游戏运行的流畅性和降低内存使用,我们经常需要采取一些有效的优化手段。对象池(Object Pool)是一种常见且有效的优化技术之一,特别是在处理大量频繁创建和销毁的游戏对象时。本文将介绍Unity中如何使用对象池进行性能优化,提高游戏的运行效率。

二、什么是对象池?

        对象池是一种管理和重复使用游戏对象的机制。在游戏中,经常需要实例化和销毁大量的对象,但这样的操作会导致内存分配和垃圾回收,从而影响游戏的性能。对象池通过提前创建一定数量的对象并将其保存起来,而不是每次需要时都创建新的对象,从而避免了频繁的内存分配和销毁操作。

       其核心思想是,使用完不直接删除,而是将其放回池子里,需要用的时候再取出来。 对象池模式的出现主要优化两点:

1、防止对象被频繁的创建和删除,从而内存抖动、频繁GC(垃圾回收)

2、对象初始化成本较高

三、对象池的应用

对象池的应用简单来说分为五点:

借用:在游戏中,经常会遇到需要动态生成大量的临时对象的情况,比如子弹、爆炸效果等。使用对象池的“借用”策略,可以避免频繁的实例化和销毁操作。当需要新对象时,从对象池中借用一个对象,而不是通过new操作符创建新实例。这减少了内存分配的开销,提高了性能。

归还:使用完对象后,通过“归还”策略将对象放回对象池。这样可以重复使用对象,而不是销毁它们,减少了垃圾回收的频率,降低了内存开销。

预热:在游戏启动或者关键时刻,通过“预热”策略可以提前创建一定数量的对象,减少游戏运行时的对象池扩容和性能波动。这样可以在游戏开始时就确保对象池中有足够的对象,避免在游戏运行时动态创建对象,从而提高游戏的启动速度和稳定性。

缩小:当对象池中的对象过多时,可以通过“缩小”策略来释放一部分对象,以降低内存占用。这通常在游戏运行时的某个合适时机触发,例如切换场景或者进入后台时。

重置:有些对象在被归还到对象池后,可能会带有之前的状态,比如位置、速度等。通过“重置”策略,可以在对象被借用前将其状态重置为初始状态,确保对象在被重新使用时是干净的。

四、Unity中的对象池实现

using System.Collections;
using System.Collections.Generic;
using Mr.Le.Utility.Singleton;
using UnityEngine;

namespace Mr.Le.Utility.Manager
{
    #region 对象池管理器

    public class ObjectPoolManager : MonoSingleton<ObjectPoolManager>
    {
        #region 字段

        //所有对象池的父物体
        private GameObject poolsParent;

        //所有对象池的父物体的名字
        private const string poolsParentName = "ObjectPools";

        //对象池列表
        public List<ObjectPool> ObjectPoolsList = new List<ObjectPool>();

        //对象池对象集合
        public Dictionary<GameObject, ObjectPool> ObjectPoolsDic = new Dictionary<GameObject, ObjectPool>();

        #endregion

        #region 外部方法

        /// <summary>
        /// 预加载指定数量的对象
        /// </summary>
        /// <param name="prefab"></param>
        /// <param name="count"></param>
        public void Preload(GameObject prefab, int count)
        {
            ObjectPool pool = FindObjectPool(prefab);
            
            pool.Preload(count);
        }

        /// <summary>
        /// 从对象池中取出对象
        /// </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;

            ObjectPool objectPool = FindObjectPool(prefab);

            GameObject go = objectPool.Spawn(position, rotation, parent);
            
            ObjectPoolsDic.Add(go,objectPool);
            return go;
        }

        /// <summary>
        /// 回收对象
        /// </summary>
        /// <param name="go"></param>
        /// <param name="delayTime"></param>
        public void Recycle(GameObject go, float delayTime = 0f)
        {
            if (go == null) return;

            StartCoroutine(RecycleCoroutine(go, delayTime));

            IEnumerator RecycleCoroutine(GameObject go, float delayTime = 0f)
            {
                if (delayTime > 0)
                    yield return new WaitForSeconds(delayTime);

                //先从当前正在使用的对象池字典去找指定的对象
                if (ObjectPoolsDic.TryGetValue(go, out ObjectPool pool))
                {
                    ObjectPoolsDic.Remove(go);
                    
                    pool.Recycle(go);
                }
                else //没找到就到对象池列表里面去找
                {
                    pool = FindUsedObjectPool(go);
                    if (pool != null)
                    {
                        pool.Recycle(go);
                    }
                }
            }
        }

        /// <summary>
        /// 把所有对象池中的对象全部回收
        /// </summary>
        public void RecycleAll()
        {
            for (int i = 0; i < ObjectPoolsList.Count; i++)
            {
                ObjectPoolsList[i].RecycleAll();
            }
            
            ObjectPoolsDic.Clear();
        }

        /// <summary>
        /// 返回指定对象池的容量
        /// </summary>
        /// <param name="prefab"></param>
        /// <returns></returns>
        public int GetCapacity(GameObject prefab)
        {
            ObjectPool pool = FindObjectPool(prefab);

            return pool.capacity;
        }

        /// <summary>
        /// 设置指定对象池的容量
        /// </summary>
        /// <param name="prefab"></param>
        /// <param name="capacity"></param>
        public void SetCapacity(GameObject prefab, int capacity = -1)
        {
            ObjectPool pool = FindObjectPool(prefab);

            pool.capacity = capacity;
        }

        /// <summary>
        /// 缩减对象池
        /// </summary>
        /// <param name="prefab"></param>
        /// <param name="count"></param>
        public void Shrink(GameObject prefab, int count)
        {
            if (prefab == null || count <= 0) return;

            ObjectPool pool = FindObjectPool(prefab);

            for (int i = 0; i < ObjectPoolsList.Count; i++)
            {
                if (ObjectPoolsList[i] == pool)
                {
                    for (int j = 0; j < count; j++)
                    {
                        if (pool.unUsedGameObjectList.Count <= 0)
                            break;
                        Destroy(pool.unUsedGameObjectList[0]);
                        pool.unUsedGameObjectList.RemoveAt(0);
                    }
                }
            }
        }

        #endregion

        #region 内部方法

        /// <summary>
        /// 查找当前正在使用的对象池
        /// </summary>
        /// <param name="go"></param>
        /// <returns></returns>
        private ObjectPool FindUsedObjectPool(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.usedGameObjectList.Count; j++)
                {
                    if (pool.usedGameObjectList[i] == go)
                        return pool;
                }
            }

            return null;
        }

        /// <summary>
        /// 查找一个对象池
        /// </summary>
        /// <param name="prefab">对象池对象预制体</param>
        /// <returns></returns>
        private ObjectPool FindObjectPool(GameObject prefab)
        {
            if (prefab == null) return null;
            //创建父对象
            CreatePoolsParent();

            //先遍历大池子 如果有直接拿出来用
            for (int i = 0; i < ObjectPoolsList.Count; i++)
            {
                if (ObjectPoolsList[i].prefab == prefab)
                {
                    return ObjectPoolsList[i];
                }
            }
            
            //如果没有就创建一个
            ObjectPool objectPool = new GameObject($"ObjectPool{prefab.name}").AddComponent<ObjectPool>();
            objectPool.prefab = prefab;
            objectPool.transform.SetParent(poolsParent.transform);
            
            //将这个对象池放入对象池列表
            ObjectPoolsList.Add(objectPool);

            return objectPool;
        }

        /// <summary>
        /// 创建对象池父对象(大池子)
        /// </summary>
        private void CreatePoolsParent()
        {
            if (poolsParent == null)
            {
                ObjectPoolsList.Clear();
                ObjectPoolsDic.Clear();
                poolsParent = new GameObject(poolsParentName);
            }
        }

        #endregion
        
        #region 对象池类

    public class ObjectPool : MonoBehaviour
    {
        #region 字段

        //这个对象池存储的游戏对象预制体
        public GameObject prefab;
        //对象池的容量 默认为-1 表示容量不限制
        public int capacity = -1;
        //对象池中正在使用的对象
        public List<GameObject> usedGameObjectList = new List<GameObject>();
        //对象池中空闲的对象
        public List<GameObject> unUsedGameObjectList = new List<GameObject>();

        #endregion

        #region 属性

        /// <summary>
        /// 对象池中对象的总个数
        /// </summary>
        public int TotalGameObejctCount
        {
            get
            {
                return usedGameObjectList.Count + unUsedGameObjectList.Count;
            }
        }

        #endregion

        #region 方法

        /// <summary>
        /// 对象预加载
        /// </summary>
        /// <param name="count"></param>
        public void Preload(int count = 1)
        {
            if (prefab == null || count <= 0) return;

            for (int i = 0; i < count; i++)
            {
                GameObject go = Instantiate(prefab, Vector3.zero, Quaternion.identity);
                go.SetActive(false);
                go.transform.SetParent(transform,false);
                unUsedGameObjectList.Add(go);
                go.name = prefab.name;
            }
        }

        /// <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 = null;

            //如果当前有空闲的对象 就从对象池中直接拿出来用
            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);
            }
            
            //执行该对象身上脚本的OnSpawn方法 前提是该对象脚本继承了MonoBehaviour
            go.SendMessage("OnSpawn",SendMessageOptions.DontRequireReceiver);

            return go;
        }

        /// <summary>
        /// 回收对象池对象
        /// </summary>
        /// <param name="go"></param>
        public void Recycle(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);
                    //执行该对象身上脚本的OnRecycle方法 前提是该对象脚本继承了MonoBehaviour
                    go.SendMessage("OnRecycle",SendMessageOptions.DontRequireReceiver);
                    go.transform.SetParent(transform,false);
                    go.transform.localPosition = Vector3.zero;
                    go.transform.localRotation = Quaternion.identity;
                    go.SetActive(false);

                }
            }
        }

        /// <summary>
        /// 回收所有对象
        /// </summary>
        public void RecycleAll()
        {
            int count = usedGameObjectList.Count;

            for (int i = 0; i < count; i++)
            {
                Recycle(usedGameObjectList[0]);
            }
            
            usedGameObjectList.Clear();
        }

        #endregion
    }

    #endregion
    }

    #endregion
}

五、应用演示

接下来就借用游戏中释放子弹的一个简单案例来演示对象池的效果。

1.先创建一个Gun脚本为挂载到枪预制体

using System;
using System.Collections;
using System.Collections.Generic;
using Mr.Le.Utility.Manager;
using UnityEngine;

public class Gun : MonoBehaviour
{
    [SerializeField] private float shotForce = 1000f;
    private GameObject bulletPrefab;
    private Transform firePoint;

    private void Awake()
    {
        bulletPrefab = Resources.Load<GameObject>("Prefab/Bullet");
        firePoint = transform.Find("Cube/firePoint");
        ObjectPoolManager.Instance.Preload(bulletPrefab,10);
        
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            GameObject bullet = ObjectPoolManager.Instance.Spawn(bulletPrefab, firePoint.position, firePoint.rotation);
            bullet.GetComponent<Rigidbody>().AddForce(Vector3.forward * shotForce);
            ObjectPoolManager.Instance.Recycle(bullet,1f);
        }
    }
}

2.再创建一个Bullet的脚本挂载到子弹对象上

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

public class Bullet : MonoBehaviour
{
    private void OnSpawn()
    {
        Debug.Log("子弹发射");
    }

    private void OnRecycle()
    {
        Debug.Log("子弹回收");
        GetComponent<Rigidbody>().velocity = Vector3.zero;
    }
}

六、总结

       通过使用对象池,我们可以有效地管理游戏对象,避免频繁的内存分配和销毁操作,从而提高游戏的性能和流畅度。在开发过程中,根据实际情况合理使用对象池,可以在不牺牲可读性的情况下改善游戏的性能。

      希望本文能够帮助你更好地理解和应用Unity中的对象池技术,提升游戏开发的效率和体验。感谢阅读!

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值