对象池是一种优化技术,用于缓解创建和销毁大量GameObjects时的CPU压力。
对象池模式使用一组初始化好的对象,这些对象保持准备就绪并在停用的"池"中等待。当你需要对象时,你的应用程序不会实例化它。相反,你从池中请求GameObject并启用它。完成使用后,你停用对象并将其返回到池中,而不是销毁它。
对象池可以减少可能由垃圾回收尖峰引起的卡顿。GC尖峰通常伴随创建或销毁大量对象,因为需要分配内存。你可以在适当的时机预实例化你的对象池,例如在加载屏幕上,当用户不会注意到卡顿时。

对象池可以帮助你射击子弹而不会产生游戏卡顿。
示例:简单池系统
考虑一个具有两个定义的MonoBehaviour的简单池系统:
- 一个ObjectPool,它保存要从中提取的GameObject集合
- 一个PooledObject组件,添加到Prefab中。这有助于每个克隆的项目保持对池的引用
在ObjectPool中,你设置描述池大小的字段、你想要存储的PooledObject Prefab,以及将形成池本身的集合(本例中是一个栈)。
public class ObjectPool : MonoBehaviour
{
[SerializeField] private uint initPoolSize;
[SerializeField] private PooledObject objectToPool;
// store the pooled objects in a collection
private Stack<PooledObject> stack;
private void Start()
{
SetupPool();
}
// creates the pool (invoke when the lag is not noticeable)
private void SetupPool()
{
stack = new Stack<PooledObject>();
PooledObject instance = null;
for (int i = 0; i < initPoolSize; i++)
{
instance = Instantiate(objectToPool);
instance.Pool = this;
instance.gameObject.SetActive(false);
stack.Push(instance);
}
}
}
SetupPool方法填充对象池。创建一组新的PooledObject,然后实例化objectToPool的副本以用initPoolSize个元素填充它。在Start中调用SetupPool以确保它在游戏过程中运行一次。
你还需要方法来检索池中的项目(GetPooledObject)和将一个返回到池中(ReturnToPool):
// returns the first active GameObject from the pool
public PooledObject GetPooledObject()
{
// if the pool is not large enough, instantiate new PooledObjects
if (stack.Count == 0)
{
PooledObject newInstance = Instantiate(objectToPool);
newInstance.Pool = this;
return newInstance;
}
// otherwise, just grab the next one from the list
PooledObject nextInstance = stack.Pop();
nextInstance.gameObject.SetActive(true);
return nextInstance;
}
public void ReturnToPool(PooledObject pooledObject)
{
stack.Push(pooledObject);
pooledObject.gameObject.SetActive(false);
}
GetPooledObject仅在池为空时才创建新的PooledObject。否则,它只是返回下一个可用的元素。如果池大小足够,大多数时候你只应该获得对现有GameObject的引用。
调用GetPooledObject的客户端然后需要将池化的对象移动/旋转到位。
每个池化元素将有一个小的PooledObject组件,只是为了引用ObjectPool:
public class PooledObject : MonoBehaviour
{
private ObjectPool pool;
public ObjectPool Pool { get => pool; set => pool = value; }
public void Release()
{
pool.ReturnToPool(this);
}
}
调用Release会禁用GameObject并将其返回到池队列。
随附的项目展示了一个基本用法示例。在这里,一个ExampleGun脚本附加到一个GameObject。它存储对对象池的引用。当用户射击时,武器脚本调用其GetPooledObject方法,而不是调用Object.Instantiate。
在射弹本身是一个ExampleProjectile脚本和一个PooledObject脚本。ExampleProjectile有一个Deactivate方法,在几秒后禁用每个发射的子弹GameObject,将其返回到可用池中。
激活的池化对象
停用的池化对象
禁用和重用池化对象
这样,你可以看起来在屏幕外发射数百个子弹,而实际上你只是禁用了它们并回收它们。只要确保你的池大小足够显示同时活动的对象。
如果你需要超过池大小,池可以实例化额外的对象。然而,大多数时候它从现有的非活动对象中提取。
如果你使用过Unity的ParticleSystem,那么你就直接体验过对象池。ParticleSystem组件包含一个关于最大粒子数的设置。这只是回收可用的粒子,防止效果超过最大数量。对象池的工作方式类似,但适用于你选择的任何GameObject。
改进
上面的示例是一个简单的。当你为实际项目部署对象池时,考虑以下升级:
- 使其成为静态或单例:如果你需要从各种来源生成池化对象,考虑使对象池静态。这使它在应用程序中任何地方都可以访问,但无法使用Inspector。或者,将对象池模式与单例模式结合起来,使其全局可访问以便使用。
- 使用字典管理多个池:如果你有多个想要池化的不同Prefab,将它们存储在单独的池中,并存储键值对,以便你知道查询哪个池(Prefab的InstanceID可以作为唯一键)。
- 创造性地隐藏未使用的GameObjects:有效利用对象池的一部分是隐藏未使用的对象并将它们返回到池中。利用每一个停用池化对象的机会(例如,屏幕外、被爆炸遮挡等)
- 检查错误:避免释放一个已经在池中的对象。否则可能导致运行时错误。
- 添加最大大小/上限:大量池化对象会消耗内存。你可能需要删除超过某个限制的对象,以便池不会使用太多资源。
你使用对象池的方式会因应用程序而异。这种模式常见出现在枪或武器需要发射多个射弹的情况下,就像子弹地狱射击游戏一样。
每次实例化大量对象时,你都可能导致垃圾回收尖峰引起的轻微暂停。对象池可以缓解这个问题,保持你的游戏玩法流畅。
如果你使用的Unity版本是2021及以上,它包含一个内置的对象池系统,因此没有必要像前面的示例那样创建你自己的PooledObject或ObjectPool类。
UnityEngine.Pool
对象池模式非常普遍,以至于Unity 2021现在支持自己的UnityEngine.Pool API。这为你提供了一个基于堆栈的ObjectPool来跟踪你的对象。根据你的需要,你也可以使用CollectionPool(List、HashSet、Dictionary等)。
在示例项目中(参见场景),你不再需要自定义池组件。相反,用顶部的using UnityEngine.Pool;行更新枪脚本。这允许你使用内置的ObjectPool创建射弹池:
using UnityEngine.Pool;
public class RevisedGun : MonoBehaviour
{
...
// stack-based ObjectPool available with Unity 2021 and above
private IObjectPool<RevisedProjectile> objectPool;
// throw an exception if we try to return an existing item, already in the pool
[SerializeField] private bool collectionCheck = true;
// extra options to control the pool capacity and maximum size
[SerializeField] private int defaultCapacity = 20;
[SerializeField] private int maxSize = 100;
private void Awake()
{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,
OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject,
collectionCheck, defaultCapacity, maxSize);
}
// invoked when creating an item to populate the object pool
private RevisedProjectile CreateProjectile()
{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;
return projectileInstance;
}
// invoked when returning an item to the object pool
private void OnReleaseToPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(false);
}
// invoked when retrieving the next item from the object pool
private void OnGetFromPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(true);
}
// invoked when we exceed the maximum number of pooled items (i.e. destroy the pooled object)
private void OnDestroyPooledObject(RevisedProjectile pooledObject)
{
Destroy(pooledObject.gameObject);
}
private void FixedUpdate()
{
...
}
}
该脚本的大部分内容适用于原始的ExampleGun脚本。然而,ObjectPool构造函数现在包含在以下情况下设置一些逻辑的有用能力:
- 首次创建池化项目以填充池时
- 从池中取出项目时
- 将项目返回到池时
- 销毁池化对象时(例如,如果你达到最大限制)
你必须定义一些相应的小方法来传入构造函数。
请注意内置ObjectPool还包括默认池大小和最大池大小的选项。超过最大池大小的项目会触发自我销毁的操作,保持内存使用在控制范围内。
射弹脚本进行了一些小修改,以保持对ObjectPool的引用。这使得将对象释放回池中更方便。
public class RevisedProjectile : MonoBehaviour
{
...
private IObjectPool<RevisedProjectile> objectPool;
// public property to give the projectile a reference to its ObjectPool
public IObjectPool<RevisedProjectile> ObjectPool { set => objectPool = value; }
...
}
UnityEngine.Pool API使设置对象池更快,因为你不必从头重建模式。这是另一个不需要重新发明的轮子。