对象池

复用 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 的 AwakeStart 中解析,请给类加上 [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

选项默认值作用
InitialCapacity0注册时预创建的实例数。
MaxSizeint.MaxValue非激活实例数量的上限。超出上限被归还的实例会被销毁而不是保留。
OnExhaustedGrowAndDestroy当没有可用的非激活实例时 Get 的行为(见下文)。
CallIPoolabletrue是否调用 OnSpawned() / OnDespawned() 钩子。

耗尽策略(PoolExhaustionPolicy

策略池为空时的行为
GrowAndDestroyGet 总是创建新实例(永不返回 null)。归还时,超出 MaxSize 的实例会被销毁。安全的默认值。
GrowAndReuseGet 总是创建,归还也总是保留——池会无限增长。这是对内存增长的显式选择。
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),池会用默认选项自动注册它并记录一次性警告。虽然能用,但你失去了调优 InitialCapacityMaxSizeOnExhausted 的机会——请提前注册。

  • 单参数 Get 保留旧的 transform。 Get(prefab) 会让实例停留在它被归还时所在的位置。当生成位置重要时,请使用 Get(prefab, position, rotation)

  • 钩子异常会被隔离。 如果你的 OnSpawned()OnDespawned() 抛出异常,池会捕获它、记录一条错误并继续运行——实例仍会正常激活或停用。你的 bug 不会弄坏池。

  • IL2CPP 裁剪。 如果你用自己的配置或事件类型扩展了池,请添加 link.xml 条目让它们在 IL2CPP 构建中存活。核心类型(IObjectPoolServiceGameObjectPoolNullObjectPool)已由框架保留。

相关页面