Unity MyFramework 用法说明(二十三):使用 PrefabPoolManager 复用预设实例

子弹、伤害数字、怪物模型和临时 UI 等对象,可能在短时间内被频繁创建和销毁。如果每次都执行 InstantiateDestroy,不仅会增加主线程开销,也容易产生明显的运行时波动。

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;

这种方式适合某些不希望频繁触发 OnEnableOnDisable 的对象。

但使用 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 资源引用、实例创建、对象复用和空闲资源释放统一到同一套生命周期中。

业务层只需要明确什么时候获取对象、什么时候归还对象,以及复用前需要重置哪些业务状态。

相关推荐
冰凌糕3 小时前
Unity3D Shader 法线与基础光照
unity
新手unity自用笔记7 小时前
unity网络基础_1
网络·unity·游戏引擎
SmalBox16 小时前
【节点】[IsInfinite节点]原理解析与实际应用
unity3d·游戏开发·图形学
谢斯16 小时前
[vscode] 使用unity打开vscode的取消.csproj的显示
ide·vscode·unity
魔术师Dix16 小时前
StartGame:Unity TDD 外部工程指南
学习·游戏·unity·c#·测试驱动开发
hsw8157739432 天前
第10章 软件架构的演化和维护 — 系统架构设计师
unity·系统架构·游戏引擎
SmalBox2 天前
【节点】[IsFrontFace节点]原理解析与实际应用
unity3d·游戏开发·图形学