子弹、伤害数字、怪物模型和临时 UI 等对象,可能在短时间内被频繁创建和销毁。如果每次都执行 Instantiate 和 Destroy,不仅会增加主线程开销,也容易产生明显的运行时波动。
MyFramework 使用 PrefabPoolManager 统一加载 Prefab、创建实例、回收对象,并在对象池不再使用时释放对应资源。
项目地址:
https://github.com/ZHOURUIH/MyFramework
一、PrefabPoolManager 的整体结构
对象池主要由三部分组成:
PrefabPoolManager
├── PrefabPool
└── GameObjectInfo
它们分别负责:
PrefabPoolManager
管理所有Prefab对象池,并提供对外接口
PrefabPool
管理某一个Prefab的使用中实例和未使用实例
GameObjectInfo
记录单个实例的Prefab路径、Tag、使用状态等信息
每一个 Prefab 路径对应一个独立的 PrefabPool:
Effect/Hit.prefab
↓
PrefabPool A
Character/Monster.prefab
↓
PrefabPool B
业务层不需要自己保存这些对象池,只需要通过 PrefabPoolManager 创建和回收对象。
二、同步创建一个对象
同步创建接口为:
GameObject createObject(
string fileWithPath,
int objectTag,
bool moveToHide,
bool active,
GameObject parent = null);
例如创建一个怪物模型:
GameObject monsterObject =
mPrefabPoolManager.createObject(
"Character/Monster.prefab",
OBJECT_TAG.MONSTER,
true,
true,
mMonsterRoot);
参数含义:
fileWithPath
相对于 Assets/GameResources 的路径,必须带后缀
objectTag
对象分类,用于批量销毁
moveToHide
回收时是否通过移动到远处隐藏
active
创建完成后是否激活
parent
实例创建后的父节点
资源路径应写成:
Character/Monster.prefab
而不是:
Assets/GameResources/Character/Monster.prefab
Character/Monster
Character\Monster.prefab
三、内部如何获得实例
PrefabPoolManager 会先根据路径查找对象池:
protected PrefabPool getPrefabPool(
string fileWithPath)
{
if (!mPrefabPoolList.tryGetValue(
fileWithPath,
out PrefabPool prefabPool))
{
prefabPool =
mPrefabPoolList.addClass(fileWithPath);
prefabPool.setFileName(fileWithPath);
}
return prefabPool;
}
真正获取实例的是 PrefabPool.getOneUnused():
public GameObjectInfo getOneUnused(int tag)
{
mPrefab ??=
mResourceManager
.loadGameResource<GameObject>(
mFileName);
GameObjectInfo objInfo;
if (mUnuseList.Count > 0)
{
objInfo = mUnuseList.popBack();
}
else
{
CLASS(out objInfo)
.createObject(
mPrefab.getResource(),
mFileName);
objInfo.setTag(tag);
}
objInfo.setUsing(true);
return mInuseList.add(objInfo);
}
流程可以概括为:
查找未使用实例
↓
存在:直接复用
↓
不存在:加载Prefab并实例化
↓
放入使用中列表
因此第一次创建通常需要加载和实例化,后续创建则可以直接复用已有对象。
四、回收对象和真正销毁对象
对象使用结束后,必须交还给 PrefabPoolManager:
mPrefabPoolManager.destroyObject(
ref monsterObject,
false);
第二个参数表示是否真正销毁:
false
放回对象池,后续继续复用
true
真正销毁实例,不再保留
通常高频对象应使用:
mPrefabPoolManager.destroyObject(
ref obj,
false);
只在明确不再需要缓存时才使用:
mPrefabPoolManager.destroyObject(
ref obj,
true);
使用 ref 后,回收完成时变量也会被设置为 null,可以减少继续误用旧引用的情况。
不要直接调用:
Object.Destroy(monsterObject);
否则 PrefabPoolManager 中仍然保留该实例的信息,编辑器运行时还会检查并提示:
Object can not be destroy outside of PrefabPoolManager
五、PrefabPool 内部如何回收
每个 PrefabPool 保存两个集合:
protected HashSet<GameObjectInfo> mInuseList = new();
protected List<GameObjectInfo> mUnuseList = new();
回收时先从使用中列表移除:
mInuseList.Remove(obj);
如果需要真正销毁:
UN_CLASS(ref obj);
如果只是放回对象池:
obj.setUsing(false);
mUnuseList.add(obj);
回收对象时,框架还会重置父节点和 Transform。
默认方式是关闭对象:
go.SetActive(false);
如果启用了 moveToHide,并且对象仍在 PrefabPoolManager 节点下,则会移动到远处:
go.transform.localPosition = FAR_POSITION;
这种方式适合某些不希望频繁触发 OnEnable 和 OnDisable 的对象。
但使用 moveToHide 时,需要确保对象自身状态能够在下一次取出时正确恢复。
六、使用安全异步创建
普通异步创建:
mPrefabPoolManager.createObjectAsync(
"Character/Monster.prefab",
OBJECT_TAG.MONSTER,
true,
true,
(go) =>
{
mMonsterObject = go;
});
如果对象加载期间,发起加载的角色、界面或流程已经被销毁,旧回调可能会错误地操作新状态。
这时应使用:
createObjectAsyncSafe
例如角色异步加载模型:
mPrefabPoolManager.createObjectAsyncSafe(
this,
"Character/Monster.prefab",
OBJECT_TAG.MONSTER,
true,
true,
(go) =>
{
mMonsterObject = go;
});
第一个参数必须实现 IRecyclable。
开始加载时,框架会记录关联对象的 AssignID。加载完成后再次检查:
AssignID没有变化
交付创建完成的对象
关联对象已经销毁或被对象池复用
回收刚创建的对象,不执行成功回调
这和 ResourceManager 的安全异步加载使用了相同的生命周期判断方式。
七、区分两种异步失败原因
createObjectAsyncSafe() 还支持失败回调:
mPrefabPoolManager.createObjectAsyncSafe(
this,
"Character/Monster.prefab",
OBJECT_TAG.MONSTER,
true,
true,
onMonsterLoaded,
onMonsterLoadFailed);
失败回调参数的含义为:
protected void onMonsterLoadFailed(
bool resourceLoadFailed)
{
if (resourceLoadFailed)
{
logError("怪物Prefab加载失败");
}
else
{
// 关联对象已经失效,不属于资源错误
}
}
两种情况需要区分:
true
Prefab资源加载失败
false
关联对象已经销毁,本次结果被主动丢弃
第二种情况通常属于正常的生命周期变化,不应该当成资源错误处理。
八、提前预热对象池
第一次进入战斗时突然创建大量怪物或特效,仍然可能产生卡顿。
可以提前创建一批未使用实例:
mPrefabPoolManager.initObjectToPool(
"Character/Monster.prefab",
OBJECT_TAG.MONSTER,
20,
true);
异步加载 Prefab 后预创建:
CustomAsyncOperation operation =
mPrefabPoolManager.initObjectToPoolAsync(
"Character/Monster.prefab",
OBJECT_TAG.MONSTER,
20,
true,
onPreloadFinished);
适合预热:
常用怪物模型
高频战斗特效
子弹Prefab
伤害数字
重复出现的界面列表项
需要注意,initObjectToPoolAsync() 会异步加载 Prefab,但批量实例化仍然需要产生实际创建开销。
因此不要在同一帧预创建数量过大的复杂对象。
九、使用 objectTag 批量销毁
创建对象时可以指定分类:
public static class OBJECT_TAG
{
public const int MONSTER = 1;
public const int EFFECT = 2;
public const int UI_ITEM = 3;
}
切换场景时,可以真正销毁某类对象:
mPrefabPoolManager.destroyAllWithTag(
OBJECT_TAG.MONSTER);
它会找到所有对应 Tag 的实例,并使用:
destroyReally = true
真正销毁。
需要注意,同一个 Prefab 对象池中的实例应使用相同 Tag。
如果同一路径先使用一个 Tag 创建,之后又使用另一个 Tag 获取,框架会输出错误:
不能为同一个物体设置不同的tag
Tag 表示 Prefab 实例所属的固定分类,不适合作为单个实例的临时状态。
十、对象池如何自动释放
PrefabPoolManager 默认每隔三秒扫描一次对象池:
protected float mTimerInterval = 3.0f;
可以修改扫描间隔:
mPrefabPoolManager.setTimerInterval(5.0f);
当某个池中已经没有使用中的对象,也没有正在加载或实例化的任务时,框架会:
销毁未使用实例
释放Prefab的 ResourceRef
删除对应 PrefabPool
也就是说,对象放回池中并不代表永久占用内存。
如果某个 Prefab 希望长期保留,可以注册为不自动卸载:
mPrefabPoolManager.addDontUnloadPrefab(
"Effect/CommonHit.prefab");
适合始终高频使用、重新加载成本较高的资源。
不要把大量低频 Prefab 全部设为常驻,否则对象池会失去自动释放的意义。
十一、对象复用前必须重置状态
PrefabPoolManager 负责恢复:
父节点
位置、旋转和缩放
激活状态
对象池使用状态
但它不知道具体业务组件保存了什么数据。
例如怪物模型中可能存在:
Animator当前状态
粒子播放进度
血条显示
材质参数
碰撞器开关
业务脚本中的目标引用
这些内容必须由业务对象在创建或回收时主动重置。
对象池只能复用实例,不能自动理解每个组件的业务状态。
十二、总结
PrefabPoolManager 的基本使用流程是:
1. 使用 GameResources 相对路径创建对象
2. 优先从对应 PrefabPool 获取未使用实例
3. 没有可用实例时加载并实例化
4. 使用结束后调用 destroyObject
5. false 表示回收到池中
6. true 表示真正销毁
7. 高频对象可以提前预热
8. 异步创建优先使用 createObjectAsyncSafe
常用接口包括:
createObject(...);
createObjectAsyncSafe(...);
initObjectToPool(...);
initObjectToPoolAsync(...);
destroyObject(ref obj, false);
destroyAllWithTag(tag);
它把 Prefab 资源引用、实例创建、对象复用和空闲资源释放统一到同一套生命周期中。
业务层只需要明确什么时候获取对象、什么时候归还对象,以及复用前需要重置哪些业务状态。