参考:
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多帧,而且经过更多测试,池化始终有二三十帧的优势!!

不使用池化

使用池化