使用说明 · 从零到能写业务

对象池 · 使用说明

这份文档只回答一个问题:我该怎么用它?(一行取、一行还,剩下的回收策略它自己管)

读完你能做到:3 分钟给子弹/飘字/列表项做池 · 知道什么时候"还"、什么时候"销毁池" · 分清 GameObject 池与纯 C# 池

〇它是干什么的

先花 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。

回收的时机对象"逻辑结束"就还:子弹撞了、飘字播完、列表项被移除、怪物死亡
不要手动 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/激活状态
最贵的一课 "取出来就用、用完就还"是唯一的心智负担。真正难查的不是忘还(那只是内存涨), 而是还了之后还拿着引用继续改(另一个地方的同一个对象被改花)—— 所以 ③ 那条比 ① 更重要。

六相关文档