把章节变成可单步观察的过程
分类:工程设计
先说结论
对象池用“回收后再次使用”代替高频创建和销毁。它减少分配和实例化抖动,但不会自动重置旧状态;池中对象只是暂时空闲,不是全新对象。
四次发射只创建三颗子弹
初始:池中 0,已创建 0
发射 A、B、C:池中 0,已创建 3
A 命中并 Release:池中 1,已创建仍为 3
第四次 Get:复用 A,池中 0,已创建仍为 3
第四次取出 A 时必须清除它上次的目标、命中标记和回调。重复 Release A 会让池中出现两个相同引用,之后两个调用者可能同时拿到同一个对象。
对象池解决什么问题
游戏里有很多对象会频繁创建和销毁:
子弹
伤害数字
飘字
命中特效
掉落物
音效播放器
UI 列表项
怪物预制体
普通写法:
GameObject bullet = Instantiate(bulletPrefab);
Destroy(bullet);
偶尔一次没问题,但如果每秒几十次,会带来:
CPU 开销
内存分配
GC 压力
卡顿
资源加载抖动
对象池的思路是:
不用了不要销毁,先放回池子。
下次要用,再从池子里拿出来复用。
跟着代码跑一次:创建、回收、再次取出
先用普通 C# 对象模拟子弹,不依赖 Unity:
using System;
using System.Collections.Generic;
sealed class Bullet
{
private static int nextId;
public int Id { get; } = ++nextId;
public bool Active { get; private set; }
public float Damage { get; private set; }
public void Spawn(float damage)
{
Active = true;
Damage = damage;
}
public void Despawn()
{
Active = false;
Damage = 0f;
}
}
sealed class BulletPool
{
private readonly Stack<Bullet> available = new();
public Bullet Rent(float damage)
{
Bullet bullet = available.Count > 0
? available.Pop()
: new Bullet();
bullet.Spawn(damage);
return bullet;
}
public void Return(Bullet bullet)
{
bullet.Despawn();
available.Push(bullet);
}
}
按下面顺序调用:
BulletPool pool = new();
Bullet first = pool.Rent(10f);
Console.WriteLine($"第一次: Bullet #{first.Id}, Damage={first.Damage}");
pool.Return(first);
Bullet second = pool.Rent(25f);
Console.WriteLine($"第二次: Bullet #{second.Id}, Damage={second.Damage}");
两次都是 Bullet #1,但第二次伤害是 25:
Rent(10) -> 池为空,创建 #1,Spawn(10)
Return -> Despawn 清理 #1,压回池
Rent(25) -> 弹出 #1,Spawn(25)
第二步:复现重复回收
pool.Return(second);
pool.Return(second); // 错:同一个引用被压入两次
Bullet a = pool.Rent(5f);
Bullet b = pool.Rent(8f);
Console.WriteLine(ReferenceEquals(a, b)); // True,严重错误
两个调用者以为各自拿到一颗子弹,实际拿到同一个对象。加入“当前是否在池中”的集合:
private readonly HashSet<Bullet> inPool = new();
public void Return(Bullet bullet)
{
if (!inPool.Add(bullet))
throw new InvalidOperationException("Bullet 被重复回收");
bullet.Despawn();
available.Push(bullet);
}
同时在 Rent 弹出对象后执行 inPool.Remove(bullet)。这次第二个 Return 会立即报出生命周期错误,而不是几帧后表现成两发子弹互相覆盖。后面的 Unity 版本只是把 Spawn/Despawn 换成激活状态、Transform、协程和事件绑定清理,池的状态机没有变化。
核心结构
对象池常用 Stack<T> 保存空闲对象:
Stack<GameObject> pool = new();
原因是对象池通常不关心顺序:
拿哪个空闲对象都行。
Stack.Push 和 Stack.Pop 很适合:
回收时 Push
取出时 Pop
取对象
GameObject Get()
{
if (pool.Count > 0)
{
GameObject obj = pool.Pop();
obj.SetActive(true);
return obj;
}
return Instantiate(prefab);
}
逻辑:
池子里有空闲对象:
拿一个出来,激活,返回
池子里没有:
创建一个新的
回收对象
void Release(GameObject obj)
{
obj.SetActive(false);
pool.Push(obj);
}
逻辑:
不用了:
先隐藏
放回池子
这里不是 Destroy,而是复用。
简化版对象池
public class SimpleGameObjectPool
{
private readonly GameObject prefab;
private readonly Stack<GameObject> pool = new();
public SimpleGameObjectPool(GameObject prefab)
{
this.prefab = prefab;
}
public GameObject Get()
{
if (pool.Count > 0)
{
GameObject obj = pool.Pop();
obj.SetActive(true);
return obj;
}
return UnityEngine.Object.Instantiate(prefab);
}
public void Release(GameObject obj)
{
if (obj == null)
{
return;
}
obj.SetActive(false);
pool.Push(obj);
}
}
使用:
SimpleGameObjectPool bulletPool = new(bulletPrefab);
GameObject bullet = bulletPool.Get();
// 子弹飞完或命中后
bulletPool.Release(bullet);
坑一:取出时要重置状态
对象复用前一定要清理旧状态。
例如子弹上次的数据:
速度 = 20
伤害 = 100
目标 = enemyA
已经命中 = true
如果回收后下次直接拿出来,不重置,就可能:
新子弹一出生就处于“已经命中”
新子弹还保留旧目标
新特效还带着旧颜色、旧位置、旧回调
所以对象池通常会有:
OnSpawn:取出时初始化
OnDespawn:回收时清理
示例:
public interface IPoolable
{
void OnSpawn();
void OnDespawn();
}
坑二:防止重复回收
如果同一个对象被回收两次:
pool.Release(bullet);
pool.Release(bullet);
池子里会有两个相同引用。
之后两次 Get() 可能拿到同一个对象:
A 拿到 bullet
B 也拿到 bullet
这会非常难查。
可以用 HashSet<GameObject> 检测:
HashSet<GameObject> inPool = new();
回收时:
if (!inPool.Add(obj))
{
Debug.LogWarning("重复回收");
return;
}
obj.SetActive(false);
pool.Push(obj);
取出时:
GameObject obj = pool.Pop();
inPool.Remove(obj);
obj.SetActive(true);
这里用到了 HashSet.Add 的返回值:
false 表示已经存在,说明重复回收。
坑三:对象池不是越大越好
对象池减少创建销毁,但也会占内存。
如果池子里长期堆着大量不用的对象:
不销毁也会占内存。
成熟对象池通常会有:
初始容量
最大容量
超出最大容量时 Destroy
长时间不用时裁剪
场景退出时清空
例如:
if (pool.Count >= maxSize)
{
UnityEngine.Object.Destroy(obj);
return;
}
适合对象池的场景
适合:
创建销毁频繁
对象结构差不多
生命周期短
复用成本低
例如:
子弹
特效
伤害数字
UI 列表项
音效播放器
不适合:
很少创建的对象
状态极其复杂且难以重置
长期存在的唯一对象
最重要的收获
对象池用“回收复用”替代“频繁创建销毁”。
最重要的三个坑:
取出时要初始化。
回收时要清理。
防止同一个对象重复回收。
和前面知识点的连接:
Stack:保存可复用对象。
HashSet:检测重复回收。
生命周期管理:谁 Get,谁 Release。
状态重置:避免旧数据污染新对象。