Unity 新版对象池(Object Pooling)应用

 参考:

Introduction to Object Pooling - Unity Learnhttps://learn.unity.com/tutorial/introduction-to-object-pooling#5ff8d015edbc2a002063971cUnity - Scripting API: ObjectPool (unity3d.com)https://docs.unity3d.com/ScriptReference/Pool.ObjectPool_1.html

本文仍在试探和尝试中,不代表最优解

什么是池化

对象池是一种编程模式,用于优化对象的创建和销毁过程,以提高应用程序的性能。对象池将对象预先创建并存储在内存中,以便稍后重复使用。也就是说,如果游戏场景中一把枪限定只能同时存在50颗发射出去的子弹,那么这把枪就会重复利用已经生成的这这50颗子弹,不会再有生成和销毁的步骤,这可以减少在运行时创建和销毁对象的次数,从而减少系统开销。

此外,对象池通常用于需要频繁创建和销毁对象的场景,如线程池、数据库连接池、网络连接池等。在这些场景中,使用对象池可以减少系统的开销,提高应用程序的响应速度和吞吐量。

通常,对象池会维护一个对象队列,对象的创建和销毁都由对象池控制。当需要使用对象时,应用程序从对象池中获取对象,使用完毕后将其返回给对象池,以便下次重复使用。如果对象池中没有可用对象,则会创建新的对象,并将其添加到对象池中。

Unity池化实现

新版Unity已经正式封装了自己的池化方法,可以直接调用使用了!

我们使用子弹发生来研究池化的优化效果,我们需要两个游戏物体,一个是枪械(用来挂在子弹发射脚本BulletPooling),一个是Assets中的子弹Prefab(用来挂在子弹脚本PoolBullet),我们将子弹的刚体设置为如下,这样既可以保证子弹不会穿透物体,又能增加计算机负担来进行压力测试:

以下是子弹的脚本PoolBullet:

using System.Collections;
using UnityEngine;
namespace ObjectPoolSpace
{
    public class PoolBullet : MonoBehaviour
    {
        public int destroyTime = 3;
        public BulletPooling bulletPoolManager;// can also just use the 'bulletPool' in the BulletPooling.cs
        private void OnCollisionEnter(Collision other)
        {
            // if (other.gameObject.CompareTag("Player")) //do somthing 
            StartCoroutine(KillBullet(gameObject, new WaitForSeconds(destroyTime)));
        }
        public IEnumerator KillBullet(GameObject bullet, WaitForSeconds hide_DestroyTime)
        {
            yield return hide_DestroyTime;

            if (bulletPoolManager.isPooling)
            { //check if the bullet is still active
                if (bullet.activeSelf) bulletPoolManager.bulletPool.Release(bullet);
            }
            else Destroy(bullet);
        }
    }
}

以及挂在枪械上的脚本BulletPooling:

using System.Collections;
using UnityEngine;
using UnityEngine.Pool;//base on Stack
/// <summary>
///https://docs.unity3d.com/ScriptReference/Pool.ObjectPool_1.html
/// </summary>
namespace ObjectPoolSpace
{
    public class BulletPooling : MonoBehaviour
    {
        public bool isPooling = true;
        [SerializeField] Transform firePoint;
        [SerializeField] int bulletSpeed = 10;
        [SerializeField] GameObject bulletPrefab;
        [Tooltip("The default number of objects to have in the pool, when the space is not enough, the pool will auto expand(stack).")]
        [SerializeField] int defaultPoolSize = 100;
        [Tooltip("The maximum number of objects to have in the pool. 0 = no maximum.")]
        [SerializeField] int maxPoolSize = 1000;
        public ObjectPool<GameObject> bulletPool;
        [Header("Below For Debug Use:")]
        [SerializeField] private int activeCount, inactiveCount, totalCount;

        // Start is called before the first frame update
        void Start()
        {
            bulletPool = new ObjectPool<GameObject>(OnCreatPoolItem, OnGetItemFromPool, OnReleaseItemFromPool, OnDestroyItemFromPool, true, defaultPoolSize, maxPoolSize);
        }
        private void Update()
        {
            activeCount = this.bulletPool.CountActive;
            inactiveCount = this.bulletPool.CountInactive;
            totalCount = this.bulletPool.CountAll;

            Shoot();
        }

        GameObject tempbullet;
        void Shoot()
        {
            //if use pooling, then use the Get() method to get a bullet from the pool, otherwise create a new one by Instantiate()
            tempbullet = isPooling ? bulletPool.Get() : //the Get() method will return an object from the pool, or create a new one if the pool is empty.
             Instantiate(bulletPrefab, firePoint.position, Quaternion.identity, this.transform);
            if (!tempbullet) return;
            tempbullet.GetComponent<Rigidbody>().velocity = firePoint.forward * bulletSpeed + new Vector3(UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f));
            tempbullet.GetComponent<PoolBullet>().bulletPoolManager = this;
            StartCoroutine(KillBullet(tempbullet, new WaitForSeconds(8)));//kill the bullet whatever after 5 seconds
        }
        /// <summary>
        /// This method will be called when the bullet is hit something, or the time is up.
        /// </summary>
        public IEnumerator KillBullet(GameObject bullet, WaitForSeconds hide_DestroyTime)
        {
            yield return hide_DestroyTime;

            if (isPooling)
            { //check if the bullet is still active
                if (bullet && bullet.activeSelf) bulletPool.Release(bullet);
            }
            else Destroy(bullet);
        }

        /***Below are the callback methods for the ObjectPool***/

        /// <summary> 
        /// 在对象池中创建对象时调用; This method will be called when the the Get() method is first time called and there still have space in the pool.
        /// </summary>
        private GameObject OnCreatPoolItem()
        {
            var bullet = Instantiate(bulletPrefab, firePoint.position, Quaternion.identity, this.transform);
            bullet.SetActive(true);
            //Debug.Log("OnCreatPoolItem");
            return bullet;
        }
        /// <summary>
        /// 当对象池超过容量时,或者对象被销毁时调用,一般不会发生,除非调用了Clean 或者 Dispose 方法
        ///This method will be called when the pool is full, ot Clean() or Dispose() method is called.
        /// </summary>
        private void OnDestroyItemFromPool(GameObject obj)
        {
            Destroy(obj);
            //Debug.Log("OnDestroyItemFromPool");
        }
        /// <summary>
        /// 从对象池中获取对象时调用; This method will be called when the the Get() method is called.
        /// </summary>
        private void OnGetItemFromPool(GameObject bullet)
        {
            if (bullet) bullet.SetActive(true);
            //Debug.Log("OnGetItemFromPool");
        }
        /// <summary>
        /// 当对象放回对象池时调用; This method will be called when the the Release() method is called.
        /// </summary>
        private void OnReleaseItemFromPool(GameObject bullet)
        {
            bullet.SetActive(false);
            //Reset the bullet's position
            bullet.transform.position = firePoint.position;
            bullet.transform.rotation = Quaternion.identity;
            // bullet.GetComponent<Rigidbody>().velocity = Vector3.zero;
            //Debug.Log("OnReleaseItemFromPool");
        }
    }
}

整个方案的核心是:

 bulletPool = new ObjectPool<GameObject>(OnCreatPoolItem,
 OnGetItemFromPool,
 OnReleaseItemFromPool,
 OnDestroyItemFromPool,
 true,
 defaultPoolSize,
 maxPoolSize);

以及正确设置何时将子弹回收(使用Release()方法),以上脚本将可以实现子弹的发射,子弹检测到碰撞一定时间后自动销毁/回收,以及子弹长时间没有碰撞后自动销毁/回收。实验场景将有8个炮台暴力输出炮弹,每个炮台在子弹数量稳定后都会在同一时刻有约300个炮弹在飞翔和撞击,总共就是2400多个物体在运动渲染和碰撞。


性能分析

Unity的销毁Destroy()和实例化Instantiate()是需要消耗不少资源和算力的,所以再很多物体不停生成和删除时会大幅增加计算机负担,降低游戏帧数。

最后我们通过对比可以看到,池化真的可以显著提高性能,虽然我还是不满意(代码可能哪里有问题),但是确实是优化了不少!

池化子弹在隐藏和激活之间交替,不涉及增加和摧毁

如下性能测试中可以看出,左图不使用池化,使用传统 实例化-再删除 的帧数约45帧。右图使用池化-资源重复利用 帧率来到了65多帧,而且经过更多测试,池化始终有二三十帧的优势!!

不使用池化
使用池化

猜你喜欢

转载自blog.csdn.net/weixin_46146935/article/details/130512691
今日推荐