Base 的核心已经看完了剩下的 Base 内容按需补。下一步先看 ObjectPool把池化思想从 ReferencePool 延伸到游戏对象池。IObjectPool这个类是对象池的接口类。其中的属性包括string Name获取对象池名称string FullName获取对象池完整名称。Type ObjectType获取对象池对象类型也就是当前这个对象池对象真正包含的对象是什么类型比如一个GameObject类型的对象池里面的对象ObjectType就是GameObject。int Count获取对象池中对象的数量。int CanReleaseCount获取对象池中能被释放的对象的数量。bool AllowMultiSpawn获取是否允许对象被多次获取就是某个对象是否能被重复获取比如CubeA被获取了一次在使用此时如果是可以多次获取那么其他地方也可以引用到这个对象那么两边的修改都会对同一个CubeA生效。float AutoReleaseInterva(get;set)l获取或设置对象池自动释放可释放对象的间隔秒数。就是几秒才能获取到一个。int Capacity(get;set)对象池的容量后续通过容量来管理对象的留存。float ExpireTime(get;set)对象过期时间当某对象过期时间到就按照优先级回收该对象也就是对象池按照间隔时间检测全部对象当检测的时候怎么判断对象是否过期就是通过这个成员来判断。int Priority(get;set)获取和设置对象池优先级。函数接口包括Register将具体对象obj注册到对象池这个对象池就以这个对象为基准bool是对象是否已被获取也就是当前是否有内容在用。CanSpawn查看对象是否能被弹出池参数是string是查看加到对象池的匹配名字的对象是否能被弹出。Spawn弹出一个对象参数是string的是弹出对象池中对应名字的对象比如指定弹出EnemyA。Unspawn参数是具体对象回收对象。SetLocked是设置对象是否被加锁比如传递某个对象EnemyA加锁true那么这个对象就不会被销毁和回收比如倒计时到回收或者是对象池超出容量销毁。SetPriority设置对象的优先级在定时回收时候会按照优先级回收。ReleaseObject释放单个对象。Release是按数量释放对象。ReleaseAllUnused释放所有对象池中未使用的对象。ReleaseObjectFilterCallback是一个参数为T(T就是当前对象池对应的类型)的委托意思是按照这个回调来筛选对象进行释放。ReleaseObjectFilterCallback一个委托泛型是继承了ObjectBase的对象池对象类型返回List指的就是筛选出来的所有符合释放条件的对象池对象。ObjectBase看上面的代码时候发现IObjectPool中的泛型T约束为ObjectBaseObjectBase是所有对象池中的对象的基类它是一个抽象类继承了IReference是引用类型可以被引用池管理也就说明真正的对象池对象和继承该类的类实例是组合使用的。比如EnemyObject继承于ObjectBase但是内部真正的对象是EnemyObject内的m_Target参数的引用让对象位于EnemyObject内专门管理而EnemyObject又继承于ObjectBase被引用池管理。组合的好处所有可池化对象都继承 ObjectBase于是对象池只需要认识 ObjectBase 就够了对象池不用关心 T 到底是 EnemyObject、UIForm 还是 Entity它只按统一规则管理直接省去对每个类型对象的专门配置。继承的好处因为有ObjectBase很多元方法或者数据都可以交给ObjectBase子类只负责专门的内容。m_Name指的是当前对象名称每一个对象池中包含的都是继承了ObjectBase的类的对象可以通过这个成员给该对象专门的名字而不是像普通的对象池直接改GameObject的名字这样就让对象池对于该对象完全管理了让对象池的对象m_Target可以是任何类型。m_Target存放的真实对象。m_Locked该对象是否被加锁如果加了那么防止对象池回收和清除。m_Priority单个对象的优先级。m_LastUseTime上次使用的时间用来判断对象是否长期不用或者闲置时间过长如果过长那就不让它总是在内存里呆着占地方了。在构造函数里将当前对象默认优先级设为0最后使用时间设置成DateTime的默认值。一些属性。CustomCanReleaseFlag这个是获取自定义释放检查标记默认为true可以被重写如果设置为false那么在对象池清除时假设这个对象优先级是0说明最先被清除但是CustomCanReleaseFlag设置为false那么也不会回收它如果需要回收则通过手动回收不能被对象池自动回收。LastUseTime设置最后使用时间。然后这个Initialize是主要的初始化对象基类的方法意思就是传入一个比如GameObject对象给到m_Target设置名字设置是否有自定义释放标记设置优先级设置最后使用时间(使用UTCNow不使用Now是因为到Now需要转换而我们不需要看这个时间只是用来记录所以用UTCNow更快)。这是其他的初始化对象池对象的方法。这个方法在什么时候使用因为该类继承了引用池对象接口所以被引用池管理从引用池取出来的时候要设置该对象的属性成员什么什么的内容就通过这个Initialize方法反之当该引用对象在Clear时候也就是收回引用池的时候就把对象内部清掉等待下次使用赋值。这个是引用池对象接口要求的回收引用对象的时候执行。然后OnSpawn是获取该对象的时候执行OnUnspawn是将对象回收到对象池的时候执行Release是将对象释放掉的时候执行参数bool是如果关闭对象池时候是否触发该对象的释放如果为false那么当前对象在这个类型对象池关闭后还保留对象实例如果true关闭对象池时候释放这个实例。ObjectPoolBase它是对象池的抽象基类用于辅助对象池子类的一些基础内容的实现和抽象方法的要求构造函数对象池的名字。一些属性其中FullName返回的是TypeNamePair结构体对象拼接的字符串其实意思就是把类型和当前对象池的m_Name拼接成一个字符串。CanReleaseCount对象池能被释放多少个。AllowMultiSpawn能否多次获取对象池中的对象。AutoReleaseInterval对象池自动释放可释放对象的时间。ExpireTime对象池对象过期时间配合AutoReleaseInterval。这些是留给子类的一些方法包括释放对象按照数量释放对象释放所有对象获取对象池信息更新对象池关闭对象池。ObjectInfo这个类像之前引用池里的ReferenceCollection.cs主要用于获取某个模块的信息用于查看或者获得某些需要的数据结构值所以它是struct的然后结构体成员还是那些上面的ObjectPoolBase脚本就使用了它GetAllObjectInfo这个方法返回的内容就是ObjectInfo[]所以说意思就是获取到对象池里每一个对象的数据信息。ObjectPoolManager.Object它是对象池内存储的每一个对象继承了IReference引用池对象并且T要求是继承了ObjectBase的也就是说T要求的那个类型才是真正的对象池管理的对象。而它们的结构是一个对象池中存储多个Object对象而Object对象内存储着真正继承了ObjectBase基类的对象通过Object来管理真正的对象池对象而不是直接在对象池内部存储对象池对象。这样做的好处是什么因为如果要想对对象池对象有一个统一管理的话那么用ObjectBase这个父类就可以了而且ObjectBase里并不知道自己的对象是什么类型只负责回收弹出就可以了而对于Object是池内部的“存储节点”不是对象本身。ObjectBase 回答的是“这个对象是什么它的池化状态如何”Object 回答的是“这个对象在池里怎么存它属于哪个池”m_Object是真正存储的对象。m_SpawnCount是获取的次数。一些属性。对象池对象创建方法因为这个类是在对象池内部并且是引用类型所以想要获得它就在内部通过调用引用池的方式创建一个自己。外部调用的时候将具体对象传递进来然后说明这个对象是否已经被获取了。之后通过引用池创建自己的对象设置obj和如果被获取的话那么获取次数就是1。然后执行“获取时的方法”。最后返回自己这个对象。有点类似事件池里的Event的感觉。清理Clear是当引用池对象回到引用池的时候调用的将自己对象内部清理不被GC掉等着未来用。Peek是查看实际管理的对象。Spawn是获取对象获取的是继承了ObjectBase的那个真正的对象池对象首先获取的次数要增加1然后设置最后获取时间执行获取时方法然后返回真正的对象池对象。Unspawn是回收对象执行具体对象的回收方法设置最后使用时间获取数减1如果获取数量小于0的话抛出错误。回收对象首先会执行真正对象池对象的回收方法然后将自己还到引用池里(要注意之后会执行自己的Clear方法)。isShutdown是是否是关闭对应对象池时候执行对象的Release方法。ObjectPoolManager.ObjectPool它是存在于对象池管理器中的对象池类继承了ObjectPoolBase对象池基类IObjectPool对象池接口。其中泛型要求的是ObjectBase用的地方都在对象池内对象的部分意思是对象池中的每一个对象都必须继承ObjectBase。先来看成员m_Objects是多值字典存储了每一个名字对应的对象这里Object是ObjectPoolManager.Object也就是上面那个让一个字符串对应多个Object比如哥布林 对应Object哥布林。m_ObjectMap键是真正的对象池对象值是对应的Object对象池对象类型用于辅助释放对象。m_DefaultReleaseObjectFilterCallback是一个回调当对象池被释放时候执行这个回调释放对象筛选函数。m_CachedCanReleaseObjects可回收对象缓存列表自动回收的对象们。m_CachedToReleaseObjects符合自定义条件的释放对象列表。m_AllowMultiSpawn是否允许被多次获取。m_AutoReleaseInterval自动回收间隔时间。m_Capacity容量。m_ExpireTime对象池对象过期秒数当对象池执行自动回收时候用来辅助判断对象池对象是否过期。m_Priority对象池的优先级。m_AutoReleaseTime用于辅助计算是否到达设定时间的。构造。后面属性太多了捡重点不重复的了就。获取能被释放的对象的数量调用GetCanReleaseObjects方法得到能被释放的对象放到m_CachedCanReleaseObjects里然后返回它的数量。获取和设置容量注意设置完容量后会进行一次对象池释放。设置对象池对象的过期秒数设置后执行一次对象池释放。创建对象传递实际的对象池对象传递是否该对象已经被获取。通过Object自身来创建Object对象把具体对象池对象给到刚刚创建出的Object对象里然后添加到m_Objects也就是名字对应的对象们里然后添加到m_ObjectMap对象池对象对应Object对象 的集合里。如果当前加到m_ObjectMap里的对象数量(注意m_ObjectMap是用一个对象池对象 对应一个 Object对象)也就是当前对象池中对象池对象的数量大于了对象池容量就执行对象池释放。上面那个不重要有时候可以用来调用这个对象池类检查是否存在下面那个CanSpawn用于传递对象池对象名称来查看该对象是否存在。从m_Objects(也就是一个名字对应多个对象的那个集合)查看是否有对应名字如果有遍历名字对应的对象们是否没有正在被使用或者是否允许多次调用如果允许多次调用或者对象池哪怕一个没有被使用就返回true表示对象可以被Spawn。获取对象和上面那个查看差不多只要对应名字的Object存在并且可以多次使用或者当前有没有正在被使用的那么就执行当前遍历的Object对象的Spawn方法并返回实际的对象池对象。回收对象如果传递的是ObjectBase的子类的话从里面取具体的m_Target来回收如果传递的直接就是真正的对象池对象object那个那么先通过GetObject方法获取到对应的Object对象然后如果能获取到(其实就是查看一下传递的内容是不是对象池对象)那么通过Object来执行回收方法然后回收到对象池后对象池内部容量超了就执行对象池释放。设置对象锁通过调用Object对象里的Locked来设置对象池对象是否加锁(Object对象内部实际改的也是对象池对象的锁)。设置对象优先级还是通过Object对象来设置。释放对象或者说是指定对象释放通过调用Object对象判断它没有正在使用并且没有加锁并且没有自定义释放标志才能释放否则不能释放时候从m_Objects(名字对应对象的那个多值字典)中移除从m_ObjectMap(一个对象池对象对应一个Object的表)中移除。然后执行Object对象的释放方法传递false表示不是对象池关闭时候触发的释放。之后通过引用池将Object对象回收最后返回true表示对象释放成功。这几个release这个时间控制释放很有意思第一个是使用默认释放筛选方法释放数量是所有对象池对象的数量 - 对象池容量。比如说当前对象池对象有100个但是对象池容量是60那么就释放40个对象池对象。第二个是传递数量和使用默认释放筛选方法。第三个是释放超出的数量(和第一个一样)然后传递筛选方法释放。第四个是具体的释放方法当释放的toReleaseCount传递的是小于0的那么就给他改成0然后expireTime是释放时间它的值给了一个DateTime的最小值按照后面的写法来说的话expireTime是用来当做一个时间界定线使用的。先往后看判断m_ExpireTime也就是每一个对象的过期秒数是几秒假设5秒对象就过期如果它小于float.MaxValue也就是说明当需要按照时间对 对象池对象 进行释放的话m_ExpireTime的赋值需要小于float.MaxValue否则不会去计算expireTime也就是时间界定线如果进了if那么把expireTime设置为当前时间下往前算m_ExpireTime秒。什么意思假设当前100m_ExpireTime的值是5秒那么在100分进行Release这个函数里时候就需要查看当前1:00的时候有没有在05955s前使用完成的对象这个05955s就是所谓的expireTime时间界定线。假设一个对象在0:59:53s使用结束那么它对于1:00来说就已经是一个很老的对象了因为设置是5s释放而这个对象已经使用7s了所以这个对象应该被释放反之如果是在0:59:59s的对象因为才使用了一秒并没有大于expireTime所以就不会被释放。再或者if (m_ExpireTime float.MaxValue)这句走了if那么expireTime就是时间上的最小值那么所有对象的时间都会大于它就说明所有对象都不会因为释放时间而被释放掉。m_AutoReleaseTime赋值为0表示当下面要进行释放了用于计算下一次释放时间到了没其实就是个计时器。GetCanReleaseObjects方法传递m_CachedCanReleaseObjects将能被回收的对象放到m_CachedCanReleaseObjects里。然后再执行筛选释放对象的这个回调传递获得的可释放对象传递尝试释放对象的数量传递 时间界定线。如果得到的不为空或者不为0那么就遍历这些对象池对象并且调用ReleaseObject传递对象池对象来释放它们。释放所有未使用对象。首先将计时器m_AutoReleaseTime设置为0表示刚刚释放过然后获取所有可以被释放的对象然后遍历它们调用ReleaseObject来释放这些对象池对象。获取所有对象信息遍历m_Objects里的所有Object对象然后把这个Object对象的信息都给到results这个List里面最后返回出去results这个List。更新对象池首先是m_AutoReleaseTime计时器给自己计时加时间注意加的是真实间隔时间如果计时器时间小于自动释放时间就返回去如果大于了的话就执行对象池的释放实现对象池定时释放。关闭对象池首先循环m_ObjectMap里的所有对象池对象对于值Object执行释放Release传递true表示是对象池关闭时候执行对于键对象池对象 就执行Release释放。最后清理这些集合。获取所有能够被自动释放的对象。先清理results也就是把之前使用的缓存清理干净。然后遍历所有对象池对象取出每一个Object对象如果这个对象没有在使用没有上锁没有自定义释放标志那么就把它添加到可释放列表里不过添加之前先通过Peek获取真正的对象池对象放到results里。默认释放对象过滤回调函数。candidateObjects是传递进来的可以被释放的对象列表toReleaseCount是释放数量expireTime是时间界定线。这个传递进来的可释放列表指的就是“未在使用未上锁没有指定标识要求”的那些对象。返回内容是最终可以被释放的对象列表。先清空m_CachedToReleaseObjects的缓存一会要通过它添加内容。之后判断当前时间界定线 大于 DateTime.MinValue也就是Release函数里expireTime DateTime.UtcNow.AddSeconds(-m_ExpireTime);这句执行过说明设定的m_ExpireTime有效可以执行释放。进入for循环从candidateObjects释放对象列表倒序执行如果取出来的对象最后使用时间是小于等于界定时间的比如0:53:53使用界定线是0:53:55那么这个对象就应该被释放掉了就把他添加到m_CachedToReleaseObjects待释放缓存列表里从candidateObjects里将它移除掉说明不需要管它了。遍历完成后要释放的数量toReleaseCount减去刚刚添加了的数量。最后for循环要求toReleaseCount大于0并且循环的i要小于candidateObjects的数量假设共有30个对象toReleaseCount是20然后第一个循环里加入到m_CachedToReleaseObjects的超时对象有15个那么toReleaseCount就是5个。那么在第二个大循环里最外层循环就是遍历加到candidateObjects里的超时对象池对象并且要求toReleaseCount大于0。再进入内部循环j开始值是i加1然后遍历数量是candidateObjects的超时对象池对象个数并且内部if写的是如果第i个和第j个的优先级进行比较如果i大于j优先级或者i等于j的优先级并且当前遍历对象的最后使用时间大于j的最后使用时间就把i和j对象池对象交换位置也就是说内部是一个选择排序。然后把排序完得到的时间或优先级最小的对象加到m_CachedToReleaseObjects里toReleaseCount递减需要的数量即是减去最开始计算循环时候的时间超时的对象的数量或者candidateObjects的数量如果toReleaseCount比当前candidateObjects还多的话。这表示的就是按照上面说的toReleaseCount还有5个也就是说除了超时对象池对象还有5个对象需要释放那么这5个对象释放前先对这个释放列表排个序然后把这五个也加到释放列表里总共释放20个对象最后返回这个列表得到最终需要释放的对象们。IObjectPoolManager对象池管理器接口首先它肯定是配合GameEntry的“通过接口拿到模块”这种方式使用的其次是一个规范让外部获取的时候获取的是接口而接口规范了具体的模块类外部也获取不到除了接口之外的模块类的实现。主要来看这些大同小异的接口。内部的Count用来获取所有对象池的数量。有很多个重载的HasObjectPool意思是通过名字或者通过继承了对象池对象的类的对象或者通过Type类型通过泛型T或者通过Predicate传递一个函数(函数参数是ObjectPoolBase类型)来查看是否有对应类型的对象池有就返回true。获取对象池接口。获取多个对象池GetObjectPools通过传递函数来筛选第二个是传递List得到结果。GetAllObjectPools是获取所有对象池。还可以选择是否需要排序。…后面还有很多该重载对于参数来说capacity是创建的对象池的初始容量objectType是对象池中对象的类型expireTime是对象池对象的过期秒数autoReleaseInterval是对象池自动释放对象的间隔秒数其他的不重要了都是一些不同组合的重载。然后是创建可以被多次获取的对象池也就是这里面的每一个对象池对象都可以被多次引用多次而上面的那个对象池的每一个对象池对象就只能被引用一次。然后对于函数参数来说是和上面的单次引用的对象池一样的。DestroyObjectPool是按照对象池对象的类型来销毁对象池还可以把具体的对象池传递进来销毁。Release是通过对象池管理器的方式执行对象池释放ReleaseAllUnused是释放对象池中所有未使用的对象。ObjectPoolManagerDefaultCapacity是对象池会给到的默认容量这里写的是int的最大值。DefaultExpireTime是对象池给到的对象超时秒数默认给到MaxValue也就是永不超时意思就是放到对象池的对象不回到对象池。DefaultPriority是对象池默认优先级。m_ObjectPools用来存储所有对象池通过类型名称作为键对象池作为值。m_CachedAllObjectPools缓存对象池列表。m_ObjectPoolComparer是用来比较对象池的委托泛型是对象池在构造函数里赋值了private static int ObjectPoolComparer(ObjectPoolBase a, ObjectPoolBase b)这个函数传递这两个对象池进行比较。层级和获取对象池的数量。更新对象池管理器因为对象池管理器也是一个模块轮询内部的所有对象池并且执行其中的Update。Shutdown是关闭对象池管理器同时关闭所有对象池清空对象池字典清空缓存对象池。这几个都是判断是否有这个对象池。第一个是通过传递泛型将其组成为TypeNamePair的类型调用InternalHasObjectPool方法查看是否有对应TypeNamePair键的对象池。第二个是传递Type来调用InternalHasObjectPool查看。第三个是传递泛型的基础上加一个名字来拼TypeNamePair调用InternalHasObjectPool。第四个是传递Type和名字调用InternalHasObjectPool。第五个是传递一个委托函数函数参数要求是继承了ObjectPoolBase的类型通过执行这个委托函数传递当前遍历的对象池来筛选如果返回ture就说明有所需对象池。IsAssignableFrom是检查后面参数的类型是否能被前面的类型变量所装载比如typeof(Father).IsAssignableFrom(son);返回true。GetObjectPool这几个方法同理不过是获得对应的对象池返回出去。这两个是获取对象池数组。第一个是传递委托函数的方式进来循环所有对象池然后执行这个委托如果委托执行返回true就加入到列表里最后返回这个列表的ToArray。第二个一样只不过列表不是靠内部创建而是靠自己在调用函数的时候传递一个列表如果执行委托返回true就把当前循环的这个委托加入到这个列表里。获取所有对象池。第一个和第二个实际调用的是第三个和第四个。第三个如果需要对象池排序遍历对象池加到results里然后直接用sort排序如果不需要排序直接起一个固定长度的数组循环然后放到数组里然后返回。第四个直接加到传入的列表里如果需要排序就直接调用一下Sort。至于第一个方法就是默认不需要排序的第三个第二个方法就是不需要排序的第二个。然后会有很多的CreateSingleSpawnObjectPool和CreateMultiSpawnObjectPool重载函数但是它们都是分不同情况调用的InternalCreateObjectPool所以直接看这个。通过创建TypeNamePair是对应对象池的类型和传入的名字。如果有当前名字和泛型的对象池就抛出错误如果没有对应对象池就创建一个对象池设置对象池名称是否允许多次调用容量对象过期秒数对象池优先级。然后加到对象池字典中。这个和上面的意思一样但是没有使用泛型。通过类型和名字组成TypeNamePair对象检查和创建对应对象池。然后也会有很多个DestroyObjectPool调用的都是这个内部实现函数。传递TypeNamePair先从所有对象池中获取查看是否有对应键的对象池如果有就执行对应对象池的shutdown然后将这个对应键的对象池从所有对象池字典中移除。如果找不到这个TypeNamePair对应的对象池就返回false。之后是释放相关。Release函数先获取所有对象池放到m_CachedAllObjectPools里面之后遍历m_CachedAllObjectPools执行所有对象池的Release方法。ReleaseAllUnused是释放所有未使用的对象也是获取所有对象池然后放到ReleaseAllUnused里之后遍历并且执行释放。最后是前面使用的两个内部函数一个是查看是否有对应的对象池通过传递的TypeNamePair从所有对象池字典中检查是否有对应键的对象池如果有就返回true。第二个是获取对应对象池传递TypeNamePair如果所有对象池字典中检查有对应TypeNamePair键对应的对象池就返回这个对象池。TypeNamePair最后来看一下使用的TypeNamePair。首先它大部分时间都是用来查看的所有写成了结构体。然后继承IEquatable接口实现了比较方法所以它是可以进行对比的。内部存放了Type和Name。实现了ToString就是将当前检测Type是否能转成字符串如果能就用typeName如果不能就用typeName拼接传递的m_Name。GetHashCode是获取哈希码通过异或让两段哈希码整合成一段哈希码。通过m_Type.GetHashCode() 异或 m_Name.GetHashCode()得到最终的TypeNamePair的哈希码防止加入到字典之后有哈希冲突。但是现在更推荐使用HashCode.Combine(m_Type, m_Name);。(因为加入到字典里的话键会转成哈希码所以要重写这个)。Equals用来判断调用这个方法的TypeNamePair对象是否与参数TypeNamePair相等(类型和名称同时相等)。后面的!和重载都是调用的Equals方法。Ok对象池结束。来稍微使用一下usingSystem.Collections.Generic;usingGameFramework;usingGameFramework.ObjectPool;usingUnityEngine;publicclassTestObjectPool:MonoBehaviour{AmmoManagerammoManager;[SerializeField]GameObjectammoObj;voidStart(){ammoManagernewAmmoManager();ammoManager.Init(ammoObj);}voidOnDisable(){ammoManager?.UnInit();ammoManagernull;}// Update is called once per framevoidUpdate(){if(ammoManager!null)ammoManager.Update();if(Input.GetKeyDown(KeyCode.Space)){ammoManager.CreateAmmo(ammoObj);}}}//-------------------------------------------------------------------------------------------------------------------------------------publicclassAmmoManager{IObjectPoolManagerobjectPoolManager;IObjectPoolTestAmmoammoPool;ListTestAmmoactiveAmmo;publicAmmoManager(){}publicvoidInit(GameObjectammoObj){activeAmmonewListTestAmmo();objectPoolManagerGameFrameworkEntry.GetModuleIObjectPoolManager();objectPoolManager.CreateSingleSpawnObjectPoolTestAmmo(TestAmmoPool,10,100,0);ammoPoolobjectPoolManager.GetObjectPoolTestAmmo(TestAmmoPool);//初始化10个子弹到对象池里for(inti0;i10;i){ammoPool.Register(InternalCreateAmmo(ammoObj),false);}}publicvoidUnInit(){activeAmmo?.Clear();objectPoolManager.DestroyObjectPoolTestAmmo(TestAmmoPool);}publicvoidUpdate(){if(activeAmmonull||activeAmmo.Count0)return;//在正向 for 循环里删元素后面的元素会向前顶i 会跳过刚被顶上来那个。//结果是某个子弹这一帧不会被 Update下一帧才追上极少数情况下会漏掉最后几个。//所以用倒序遍历for(intiactiveAmmo.Count-1;i0;i--){activeAmmo[i].Update();}}publicTestAmmoCreateAmmo(GameObjectammoObj){TestAmmoammoammoPool.Spawn(Ammo);if(ammonull){ammoInternalCreateAmmo(ammoObj);ammoPool.Register(ammo,true);}activeAmmo.Add(ammo);returnammo;}publicvoidRecycleAmmo(TestAmmoammo){ammoPool.Unspawn(ammo);activeAmmo.Remove(ammo);}privateTestAmmoInternalCreateAmmo(GameObjectammoObj){GameObjectgoGameObject.Instantiate(ammoObj);TestAmmoobjReferencePool.AcquireTestAmmo().Create(go,this);returnobj;}}//-------------------------------------------------------------------------------------------------------------------------------------publicclassTestAmmo:ObjectBase{AmmoManagerammoManager;GameObjectammoTarget;floattimer;publicTestAmmoCreate(GameObjectammoObj,AmmoManageram){Initialize(Ammo,ammoObj,false,0);ammoObj.SetActive(false);ammoTargetTargetasGameObject;ammoManageram;returnthis;}publicvoidCheckRecycle(){if(!ammoTarget)return;if(timer5f)ammoManager.RecycleAmmo(this);}publicvoidUpdate(){if(ammoTargetnull)return;timerTime.deltaTime;ammoTarget.transform.Translate(ammoTarget.transform.forward*Time.deltaTime*2f,Space.World);CheckRecycle();}protectedoverridevoidOnSpawn(){base.OnSpawn();timer0;ammoTarget.SetActive(true);ammoTarget.transform.positionnewVector3(0,0,0);}protectedoverridevoidOnUnspawn(){base.OnUnspawn();ammoTarget?.SetActive(false);}protectedoverridevoidRelease(boolisShutdown){if(ammoTarget!null)GameObject.Destroy(ammoTarget);ReferencePool.Release(this);}publicoverridevoidClear(){base.Clear();ammoTargetnull;ammoManagernull;timer0;}}对象池挂载至对象上赋值一个cube之类的游戏对象到ammoObj点击空格测试对象池可用。如有问题还请指出。