前四章解决了“看得见、可配置、可解耦、可调度”,这一章解决“跑得稳”:
- 高频创建子弹、特效、掉落物会触发大量
Instantiate/Destroy。 - 每次销毁都会给 GC 和 CPU 带来额外负担。
- 规模一上来,帧率抖动会非常明显。
对象池的价值不是“写一个容器”,而是把对象生命周期变成可控协议。
这章的目标
交付一个可以直接放进 Unity 项目的对象池方案,具备:
- 统一接口约定:对象拿出与回收时有明确回调。
- 预热能力:开局就把高频对象准备好。
- 回收安全:防止重复回收、跨池回收。
- 监控指标:知道池命中率和运行时扩容次数。
接口先行:定义生命周期协议
先定义两个接口,让“池化对象”可预测。
public interface IPoolable
{
void OnSpawned();
void OnDespawned();
}
public interface IPoolIdentity
{
int PoolId { get; set; }
}
为什么要 PoolId:
- 防止 A 池的对象被回收到 B 池。
- 防止重复回收导致状态错乱。
最小对象池实现(泛型)
这部分逻辑已沉淀到 Foundation 小库:
foundation/Runtime/Pooling/ObjectPool.cs
核心 API:
Get():从池里拿对象,没有则创建。Release(T):回收到池,执行重置。CountInactive:当前可用对象数量。
配合回调约定写法:
var pool = new ObjectPool<Bullet>(
factory: CreateBullet,
onGet: OnBulletGet,
onRelease: OnBulletRelease,
initialCapacity: 64);
在 Unity 场景里接入
先写一个 BulletPoolService,把池逻辑与业务解耦。
using UnityEngine;
using U3DC.Foundation.Pooling;
using U3DC.Foundation.Logging;
public sealed class BulletPoolService : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
[SerializeField] private Transform poolRoot;
[SerializeField] private int warmupCount = 64;
private ObjectPool<Bullet> _pool;
private int _created;
private int _spawned;
private void Awake()
{
_pool = new ObjectPool<Bullet>(Create, OnGet, OnRelease, warmupCount);
FoundationLog.Info("pool", "bullet pool ready inactive=" + _pool.CountInactive);
}
public Bullet Spawn(Vector3 position, Quaternion rotation)
{
var bullet = _pool.Get();
bullet.transform.SetPositionAndRotation(position, rotation);
_spawned++;
return bullet;
}
public void Despawn(Bullet bullet)
{
_pool.Release(bullet);
}
private Bullet Create()
{
var instance = Instantiate(bulletPrefab, poolRoot);
instance.gameObject.SetActive(false);
_created++;
return instance;
}
private static void OnGet(Bullet bullet)
{
bullet.gameObject.SetActive(true);
var poolable = bullet as IPoolable;
if (poolable != null) poolable.OnSpawned();
}
private static void OnRelease(Bullet bullet)
{
var poolable = bullet as IPoolable;
if (poolable != null) poolable.OnDespawned();
bullet.gameObject.SetActive(false);
}
}
这里的重点不是语法,而是边界:
Create只做创建。OnGet/OnRelease只做状态切换。- 业务逻辑(比如伤害结算)不写进池内部。
预热策略怎么选
给你一个可直接落地的经验值:
- 子弹类:按“峰值并发 * 1.2”预热。
- 特效类:按“单位时间触发上限 * 特效持续时间”估算。
- UI 列表项:按“最大可见数量 + 缓冲”预热。
不要盲目拉大预热,预热太高会把启动时间拖慢。
回收安全(必须做)
对象池最常见线上问题:重复回收。
建议在调试期加入状态位:
public sealed class Bullet : MonoBehaviour, IPoolable
{
private bool _activeInScene;
public void OnSpawned()
{
_activeInScene = true;
}
public void OnDespawned()
{
_activeInScene = false;
// 重置速度、生命周期、碰撞状态
}
public bool IsActiveInScene { get { return _activeInScene; } }
}
回收前做断言:
FoundationAssert.IsTrue(bullet.IsActiveInScene, "pool", "duplicate release detected");
性能观测(最小指标)
对象池不做指标,后续优化基本靠猜。
建议每 10 秒输出一次:
created:实际创建数量inactive:池中空闲对象spawned:累计取出次数hitRate:命中率(1 - created/spawned)
这能快速判断“池太小”还是“预热过度”。
本章验收
- 高并发场景下,
Instantiate/Destroy次数显著下降。 - 对象重用后状态正确(位置、速度、碰撞、特效开关都已重置)。
- 池命中率可观测,扩容行为可追踪。
与 Foundation 的衔接
本章与前四章代码约定已经串起来:
- 日志:
foundation/Runtime/Logging/FoundationLog.cs - 断言:
foundation/Runtime/Diagnostics/FoundationAssert.cs - 事件:
foundation/Runtime/Events/EventBus.cs - 调度:
foundation/Runtime/Timing/Scheduler.cs - 对象池:
foundation/Runtime/Pooling/ObjectPool.cs
后续 Unity 章节会直接复用这些模块,不再重复造基础设施。
下一章预告
下一章做“存档系统”:
- 版本升级
- 数据校验
- 损坏恢复
目标是把“本地存档可演进”做成标准能力。