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

Unity MyFramework 用法说明(二十三):使用 PrefabPoolManager 复用预设实例 子弹、伤害数字、怪物模型和临时 UI 等对象可能在短时间内被频繁创建和销毁。如果每次都执行Instantiate和Destroy不仅会增加主线程开销也容易产生明显的运行时波动。MyFramework 使用PrefabPoolManager统一加载 Prefab、创建实例、回收对象并在对象池不再使用时释放对应资源。项目地址https://github.com/ZHOURUIH/MyFramework一、PrefabPoolManager 的整体结构对象池主要由三部分组成PrefabPoolManager ├── PrefabPool └── GameObjectInfo它们分别负责PrefabPoolManager 管理所有Prefab对象池并提供对外接口 PrefabPool 管理某一个Prefab的使用中实例和未使用实例 GameObjectInfo 记录单个实例的Prefab路径、Tag、使用状态等信息每一个 Prefab 路径对应一个独立的PrefabPoolEffect/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 .loadGameResourceGameObject( 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并实例化 ↓ 放入使用中列表因此第一次创建通常需要加载和实例化后续创建则可以直接复用已有对象。四、回收对象和真正销毁对象对象使用结束后必须交还给PrefabPoolManagermPrefabPoolManager.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 HashSetGameObjectInfo mInuseList new(); protected ListGameObjectInfo 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 获取框架会输出错误不能为同一个物体设置不同的tagTag 表示 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 资源引用、实例创建、对象复用和空闲资源释放统一到同一套生命周期中。业务层只需要明确什么时候获取对象、什么时候归还对象以及复用前需要重置哪些业务状态。