对象池
复用 GameObject 实例来取代 Instantiate 与 Destroy,保持帧时间稳定。
用
Get/Release取代Instantiate/Destroy· 启动时预热,永久复用 · 空操作替代:NullObjectPool
CGS 通过 IObjectPoolService 消除频繁生成和销毁对象的开销。你不再反复调用 Instantiate(prefab) 和 Destroy(go),而是调用 Get(prefab) 获取一个就绪的实例,用完后调用 Release(instance) 归还。昂贵的工作——内存分配、Awake 调用、GameObject 注册——从热路径移到了一次性的**预热(prewarm)**步骤(在启动时预先创建实例)。结果是:一次生成 30 个敌人、引爆几十个特效、连射弹幕都不再造成帧卡顿。
功能概览
- 按预制体划分的池。 用调优选项注册一次预制体;服务会为它维护一个非激活实例的栈。
- 用获取和归还取代创建和销毁。
Get弹出一个非激活实例(或创建一个新的),激活后交给你。Release将其停用并放回池中。 - 生命周期钩子。 实现了
IPoolable的组件会收到OnSpawned()和OnDespawned()回调——它们是池化场景下Awake/OnDestroy的等价物,用于在两次使用之间重置状态。 - 预热。 在加载画面期间预创建 N 个实例,让第一个繁忙的战斗帧永远不必支付分配成本。
- 诊断。 随时查询每个预制体的激活数、非激活数和总数。
获取服务
解析一次并缓存——不要在 Update 中调用 Resolve:
using CommonGameSystem.Core;
var pool = ServiceLocator.Resolve<IObjectPoolService>();
如果你在 MonoBehaviour 的 Awake 或 Start 中解析,请给类加上 [DefaultExecutionOrder(100)]。对象池是自动引导器在任何场景 Awake 运行之前注册的 23 个服务之一,所以当你解析它时它已经就绪。
API 参考
注册
void RegisterPrefab(GameObject prefab, PoolOptions options) // Create a pool for the prefab
bool IsRegistered(GameObject prefab) // Does this prefab have a pool?
void UnregisterPrefab(GameObject prefab) // Destroy the pool and all its instances
对同一预制体注册两次会抛出 ArgumentException——在启动时注册一次即可。
Get / Release
GameObject Get(GameObject prefab)
GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation)
bool Release(GameObject instance)
Get 弹出一个非激活实例(或按耗尽策略创建一个新的),将其激活,并在实例实现了 IPoolable 时调用 OnSpawned()。三参数重载会在激活前设置 transform。Release 停用实例,如有 OnDespawned() 则调用之,然后放回池中——如果该实例不属于任何池,则返回 false。
预热
void Prewarm(GameObject prefab, int count) // Pre-create instances to kill first-use hiccups
诊断与清理
int CountActive(GameObject prefab) // Instances currently in use
int CountInactive(GameObject prefab) // Instances resting in the pool
int CountTotal(GameObject prefab) // Active + inactive
void Clear(GameObject prefab) // Destroy all inactive instances (registration stays)
void ClearAll() // Clear every pool
调优(PoolOptions)
| 选项 | 默认值 | 作用 |
|---|---|---|
InitialCapacity | 0 | 注册时预创建的实例数。 |
MaxSize | int.MaxValue | 非激活实例数量的上限。超出上限被归还的实例会被销毁而不是保留。 |
OnExhausted | GrowAndDestroy | 当没有可用的非激活实例时 Get 的行为(见下文)。 |
CallIPoolable | true | 是否调用 OnSpawned() / OnDespawned() 钩子。 |
耗尽策略(PoolExhaustionPolicy)
| 策略 | 池为空时的行为 |
|---|---|
GrowAndDestroy | Get 总是创建新实例(永不返回 null)。归还时,超出 MaxSize 的实例会被销毁。安全的默认值。 |
GrowAndReuse | Get 总是创建,归还也总是保留——池会无限增长。这是对内存增长的显式选择。 |
ReturnNull | 激活数达到 MaxSize 后,Get 返回 null,由调用点自行决定如何处理(例如跳过该特效)。 |
生命周期钩子(IPoolable)
在池化预制体的任意组件上实现:
void OnSpawned() // Called right after Get activates the instance — reset state here
void OnDespawned() // Called right before Release deactivates it — clean up here
池化实例在第一次使用之后不会再执行 Awake,因此每次使用前的初始化应放在 OnSpawned 里。
完整示例
一个子弹系统:发射器负责注册和开火;子弹自己移动、倒计生存时间并自行归还。
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class BulletSpawner : MonoBehaviour
{
[SerializeField] private GameObject _bulletPrefab;
private IObjectPoolService _pool;
private void Awake()
{
_pool = ServiceLocator.Resolve<IObjectPoolService>();
_pool.RegisterPrefab(_bulletPrefab, new PoolOptions
{
InitialCapacity = 50, // Pre-create 50 bullets at startup
MaxSize = 100, // Keep up to 100 inactive
OnExhausted = PoolExhaustionPolicy.GrowAndDestroy
});
}
public void Fire(Vector3 position, Quaternion rotation)
{
// With GrowAndDestroy this never returns null.
_pool.Get(_bulletPrefab, position, rotation);
}
}
// On the bullet prefab:
[DefaultExecutionOrder(100)]
public class Bullet : MonoBehaviour, IPoolable
{
[SerializeField] private float _speed = 20f;
[SerializeField] private float _maxLifetime = 3f;
private IObjectPoolService _pool;
private float _lifetime;
private void Awake()
{
_pool = ServiceLocator.Resolve<IObjectPoolService>();
}
public void OnSpawned()
{
// Reset per-use state — pooled instances do not re-run Awake.
_lifetime = _maxLifetime;
}
public void OnDespawned()
{
// Optional cleanup before going back to the pool.
}
private void Update()
{
transform.Translate(Vector3.forward * _speed * Time.deltaTime);
_lifetime -= Time.deltaTime;
if (_lifetime <= 0f)
{
_pool.Release(gameObject); // Back to the pool — never Destroy()
}
}
}
关闭该服务
要禁用池化(用于调试,或在内存充裕的平台上),换上空实现:
ServiceLocator.Replace<IObjectPoolService>(new NullObjectPool());
NullObjectPool 直接通过 Instantiate / Destroy 提供实例。你的游戏代码察觉不到任何差别——只是失去了性能收益。调用点不需要判空,也不需要任何改动。
常见陷阱
-
仅限主线程。 所有池方法都必须从 Unity 主线程调用。在 Debug 构建中,来自工作线程的调用会抛出
InvalidOperationException。 -
通过池归还,绝不要
Destroy。 对池化实例调用UnityEngine.Object.Destroy(instance)会破坏池的激活计数。始终使用Release(instance)。 -
显式注册预制体。 如果对未注册的预制体调用
Get(prefab),池会用默认选项自动注册它并记录一次性警告。虽然能用,但你失去了调优InitialCapacity、MaxSize和OnExhausted的机会——请提前注册。 -
单参数
Get保留旧的 transform。Get(prefab)会让实例停留在它被归还时所在的位置。当生成位置重要时,请使用Get(prefab, position, rotation)。 -
钩子异常会被隔离。 如果你的
OnSpawned()或OnDespawned()抛出异常,池会捕获它、记录一条错误并继续运行——实例仍会正常激活或停用。你的 bug 不会弄坏池。 -
IL2CPP 裁剪。 如果你用自己的配置或事件类型扩展了池,请添加
link.xml条目让它们在 IL2CPP 构建中存活。核心类型(IObjectPoolService、GameObjectPool、NullObjectPool)已由框架保留。