〇它是干什么的
先花 30 秒建立直觉,再动手写。
只学 2 个东西就能干活(真的)
var bullet = RevPool.Get("Battle/Bullet", "Bullet_Normal", parent: bulletRoot); // ① 取一个 // …用一段时间(飞出屏幕 / 打中目标)… RevPool.Return(bullet); // ② 还回去
Get 会自动带上资源引用(池端着,不会被卸载掉),还会把对象复位(位置/旋转/激活状态按池的规则处理)。
想"延迟还"(比如让死亡动画播完):RevPool.Return(go, delayFrames: 30)。
人话
对象池 = 对象不销毁、只"还回去",下次同样的请求直接从池里拿。
它替你把三件事挡掉:频繁 Instantiate/Destroy 的 GC 与卡顿、资源的重复加载与提前卸载、对象数量失控(有上限与瘦身)。
| 它替你解决的问题 | 怎么做的 |
|---|---|
| 子弹/飘字满天飞,创建销毁太贵 | 从池里复用,不做 Instantiate/Destroy |
| 池里的对象导致资源卸不掉 | 池端着资源引用:销毁池时才放(不用你手动 Release) |
| 用完忘记复位,出现"幽灵对象" | 取出来时框架给你一份"干净"的对象(位置/旋转/激活按规则复位) |
| 池越攒越大吃内存 | 空闲上限 + TrimAll(keepCount) 主动瘦身 + ApplyCapacityToAll(capacity) 统一改上限 |
| 切场景后旧池还在 | ClearAll() / ClearGroup(group) / DestroyAll() |
一3 分钟做一把子弹池
准备:预制体放在资源根目录的 Battle/Bullet/Bullet_Normal.prefab。
// ① 发射:取一个并放到枪口 void Fire() { var bullet = RevPool.Get("Battle/Bullet", "Bullet_Normal", parent: bulletRoot); bullet.transform.position = muzzle.position; bullet.transform.rotation = muzzle.rotation; } // ② 回收:撞到 / 飞出屏幕 / 生命周期结束 —— 任何"这个对象不要了"的地方 void OnBulletDead(GameObject bullet) { RevPool.Return(bullet); // 立刻还(会复位) // RevPool.Return(bullet, delayFrames: 30); // 或者:30 帧后再还(留给死亡特效播完) } // ③ 异步取(第一次还没加载完时不卡帧;真机首次加载/WebGL 上必须用它) RevPool.GetAsync("Battle/Bullet", "Bullet_Normal", go => { go.transform.position = muzzle.position; // 回调里再用 }); // 也有"整条路径一把传"的重载(框架源码里的推荐写法): // RevPool.GetAsync("Battle/Bullet/Blue", go => { … }, firePoint, RevResGroup.Battle); // ④ 也支持直接给预制体(预制体已经是字段/引用时) var slot = RevPool.Get(itemSlotPrefab, container);
就这样,没有初始化步骤 + 一个防手滑技巧
池是按"目录 + 名字"自动创建的:第一次
路径别手写字符串:用打包工具生成的
Get 时建、以后复用。你不需要 new 任何池对象。路径别手写字符串:用打包工具生成的
RevResPath 常量拼(改目录时编译期就报错,而不是运行时找不到)。
二"我要做 X" 对照表(全部 API)
入口统一是 RevPool(GameObject 池)。
| 我想… | 这么写 |
|---|---|
| 取一个(按资源路径) | RevPool.Get(rootPath, resName, parent) |
| 取一个(按已有预制体) | RevPool.Get(prefab, parent) |
| 取一个(可能还没加载完) | RevPool.GetAsync(rootPath, resName, go => { }, parent) |
| 还一个 | RevPool.Return(item) / RevPool.Return(component) |
| 延迟还(等动画/特效) | RevPool.Return(item, delayFrames: 30) |
| 清掉某个池的空闲对象 | RevPool.Clear(rootPath, resName)(返回清掉几个) |
| 清掉整组池的空闲对象 | RevPool.ClearGroup(RevResGroup.UI) |
| 清掉全部池的空闲对象 | RevPool.ClearAll()(切场景常用) |
| 彻底销毁某个池(连资源引用一起放) | RevPool.DestroyPool(rootPath, resName) / DestroyPool(prefab) |
| 彻底销毁所有池 | RevPool.DestroyAll()(回登录/大版本切换) |
| 统一改空闲上限 | RevPool.ApplyCapacityToAll(64) |
| 主动瘦身(每个池最多留 N 个) | RevPool.TrimAll(keepCount) |
| 看纯 C# 池的统计 / 池名清单 | RevRefPool.GetGlobalStats() · RevRefPool.GetPoolNames() |
Clear 与 Destroy 的区别(最常问)
Clear 只清"空闲对象"(在用的不受影响),池还留着、资源引用还在;
Destroy 连池一起拆(空闲对象销毁、资源引用放掉)。日常用 Clear,切大场景/回登录用 Destroy。
Destroy 连池一起拆(空闲对象销毁、资源引用放掉)。日常用 Clear,切大场景/回登录用 Destroy。
三"还"与"回收":什么时候做什么
只要记住一条:谁取谁还,别自己 Destroy。
回收的时机对象"逻辑结束"就还:子弹撞了、飘字播完、列表项被移除、怪物死亡
不要手动 Destroy池里的对象 —— 会把池的状态搞乱;要销毁就
DestroyPool还回去别再引用已 Return 的对象随时可能被下一个人取走(想延迟还就用
delayFrames)切场景调
RevPool.ClearAll()(或按组 ClearGroup)收掉空闲对象觉得内存高
TrimAll(32) 主动瘦身;长期偏高就调小 ApplyCapacityToAll 的上限排查
RevRefPool.GetPoolNames() 看有哪些池;配合 GetGlobalStats() 看总量
为什么池里的对象不会被"资源卸载"干掉
池端着资源的引用计数(这正是它存在的意义之一):只要池还在,对象就在;不用你为池里的对象写 Release。
四两种池:GameObject 池 / 纯 C# 池
GameObject 池(
给看得见的东西用:子弹、飘字、列表项、特效、血条。RevPool)特点:与资源加载打通(自动带引用)、自动复位、支持异步取。
纯 C# 对象池(
给纯数据/算法对象用:路径点、寻路节点、战斗帧数据、事件包。RevPoolCore<T>)特点:零 Unity 依赖(可脱机跑断言)、
RevPool.Get<T>() 直接拿。
怎么选
要挂在场景里、要显示 → GameObject 池;只是 new 出来的 C# 对象 → 纯 C# 池。
拿不准就用 GameObject 池(更常用),它的 API 就是本文第二张表。
纯 C# 池的完整 API(容量、清理、统计)见同目录的《对象池架构解析》。
五新手最容易踩的 5 个坑
| 坑 | 正确做法 |
|---|---|
| ① 忘记 Return(对象越堆越多) | 每个 Get 都要有一条"逻辑结束 → Return"的路径;不确定就用 delayFrames 兜底 |
② 用 Destroy(go) 销毁池里的对象 | 用 Return;要销毁池用 DestroyPool |
| ③ Return 之后还继续用这个引用 | Return 后立刻把引用置空;需要"还用一会儿"就延迟还 |
| ④ 对象上的状态没清(列表项残留旧文字) | 在 Get 之后总是显式设置全部需要显示的内容,别依赖"上次的残留" |
| ⑤ 数组/列表字段没清(越积越多) | 对象复用时它自己的集合字段要自己清 —— 框架只复位 Transform/激活状态 |
最贵的一课
"取出来就用、用完就还"是唯一的心智负担。真正难查的不是忘还(那只是内存涨),
而是还了之后还拿着引用继续改(另一个地方的同一个对象被改花)—— 所以 ③ 那条比 ① 更重要。
六相关文档
- 《对象池 · 架构解析》 —— 设计论证:池的分层、容量与淘汰策略、与资源引用计数的配合、纯 C# 池的断言
- 《资源加载系统 · 使用说明》 —— 池里的对象为什么不用你 Release
- 《UI 系统 · 使用说明》 —— 列表项池化的常见用法
- 文档总入口 · 在线文档站