Unity 对象池 | Object Pooling详细代码及使用
一:什么是对象池(Object Pooling)
对象池,顾名思义是一个装满对象的池子,当我需要时就取出他,不需要时就放回去。
传统方法:通过 Instantiate 创建对象,通过 Destroy 销毁对象,都会涉及内存的分配和释放,会产生额外开销,并且当生成数量过于庞大,会造成严重的性能下降和垃圾回收(Garbage Collection)导致的游戏卡顿暂停。
对象池:初始化了Object,不用时setActive(false),要用时setActive(true),减少对象的创建和销毁,优化了性能,但是同时延长了对象的生命周期。
只要记住当需要大量重复的对象的时候,就一定使用对象池!比如子弹、弹壳、光效粒子、Rouge中的敌人之类的。
比如:
二:对象池的基本实现
2.1最简单的列表对象池
上代码:使用列表存储对象,使用时遍历查询,效率较低但是对于简单小游戏来说够用。
如何建池子:
using System;using System.Collections;using System.Collections.Generic;using System.Runtime.InteropServices.WindowsRuntime;using UnityEngine;public class BlockPool : MonoBehaviour{ public static BlockPool instance; //单例 public GameObject blockObj; //砖块的预制体 public int blockCountInitial = 20; //初始砖块数量 public int blockCountMax = 50; //砖块上限 public int nowBlockNumber = 0; //砖块数量记录 private List blockPool = new List(); //存储砖块的列表 private void Awake() { instance = this; //实例化 } private void Start() { //创建初始数量的砖块(20) for (int i = 0; i < blockCountInitial; i++) { GameObject obj = Instantiate(blockObj); obj.SetActive(false); //设置Active为false blockPool.Add(obj); } } public GameObject GetBlockObject() { //如果有当前存在的砖块有Active为false的则启用(不需要Instantiate) for (int i = 0; i < blockPool.Count; i++) { if (!blockPool[i].activeInHierarchy) { nowBlockNumber++; return blockPool[i]; } } //如果当前存在砖块数量少于上限,且以存在砖块都使用则生成砖块并启用 if(blockPool.Count < blockCountMax) { GameObject obj = Instantiate(blockObj); blockPool.Add(obj); nowBlockNumber++; return obj; } //砖块数量达到上限 else { Console.WriteLine(\"砖块数量已达上限\"); return null; } } public void DestroyBlockObject(GameObject blocks) { //摧毁方块,并不直接销毁,而是将Active设为false nowBlockNumber--; blocks.SetActive(false); } }
如何使用:
void Update(){ // 检测鼠标左键是否被点击 if (Input.GetMouseButtonDown(0)) { // 获取鼠标位置 Vector3 mousePosition = Input.mousePosition; Vector3 worldPosition = Camera.main.ScreenToWorldPoint(new Vector3(mousePosition.x, mousePosition.y, Camera.main.nearClipPlane)); // 输出鼠标位置 Debug.Log(\"鼠标位置: \" + worldPosition); if (worldPosition.y >= 2.2) { GameObject block = pool.GetBlockObject(); if (block != null) { block.SetActive(true); block.transform.position = worldPosition; } } }}
如何销毁:
void Update(){ if (gameObject.transform.position.y < -5.5) { pool.DestroyBlockObject(gameObject); }}
2.2优化的对象池
但是对于大量生成的,比如子弹之类的对象,这种方法也太吃性能了。所以有了以下优化方案。
2.2.1优化的列表对象池
使用一个index记录当前最后取出对象的位置,并不直接从0开始计数,在开始可以优化一点性能,是简单的贪心优化。
//如果有当前存在的砖块有Active为false的则启用(不需要Instantiate)for (int i = 0; i < blockPool.Count; i++){ int t = (index + i) % blockPool.Count; if (!blockPool[i].activeInHierarchy) { index = (t + 1) % blockPool.Count; nowBlockNumber++; return blockPool[i]; }}
2.2.2最实用的栈(Stack)对象池
与列表对象池不同,列表对象池存储所有的对象预制体,但是栈对象池仅存储未使用的对象预制体,这样,栈对象池不需要判断预制体是否Active,仅需要取出即可,同时栈的Pop和Push操作时间效率为O(1),性能最优化。
using System;using System.Collections;using System.Collections.Generic;using UnityEngine;public class BlockPoolStack : MonoBehaviour{ public static BlockPoolStack instance; // 单例 public GameObject blockObj; // 砖块的预制体 public int blockCountInitial = 20; // 初始砖块数量 public int blockCountMax = 50; // 砖块上限 public int nowBlockNumber = 0; // 砖块数量记录 private Stack blockPool = new Stack(); // 使用 Stack 存储砖块 private void Awake() { instance = this; // 实例化 } private void Start() { // 创建初始数量的砖块(20) for (int i = 0; i 0) { GameObject obj = blockPool.Pop(); obj.SetActive(true); nowBlockNumber++; return obj; } // 如果当前砖块数量少于上限,且栈为空,则生成新的砖块并启用 if (nowBlockNumber < blockCountMax) { GameObject obj = Instantiate(blockObj); //obj.SetActive(true); nowBlockNumber++; return obj; } // 砖块数量达到上限 else { Debug.LogError(\"砖块数量已达上限\"); return null; } } public void DestroyBlockObject(GameObject blocks) { // 将砖块回收到栈中,而不是直接销毁,将 Active 设为 false nowBlockNumber--; blocks.SetActive(false); blockPool.Push(blocks); // 将砖块压入栈 }}
2.2.3队列(Queue)对象池
与栈对象池相同,同样是只存储未使用的对象,而且使用队列还有一个好处:如果需要更复杂的对象池管理(例如优先使用某些对象),可以结合其他数据结构(如 PriorityQueue
或自定义排序逻辑)。
using System;using System.Collections;using System.Collections.Generic;using UnityEngine;public class BlockPool : MonoBehaviour{ public static BlockPool instance; // 单例 public GameObject blockObj; // 砖块的预制体 public int blockCountInitial = 20; // 初始砖块数量 public int blockCountMax = 50; // 砖块上限 public int nowBlockNumber = 0; // 砖块数量记录 private Queue blockPool = new Queue(); // 使用 Queue 存储砖块 private void Awake() { instance = this; // 实例化 } private void Start() { // 创建初始数量的砖块(20) for (int i = 0; i 0) { GameObject obj = blockPool.Dequeue(); obj.SetActive(true); nowBlockNumber++; return obj; } // 如果当前砖块数量少于上限,且队列为空,则生成新的砖块并启用 if (nowBlockNumber < blockCountMax) { GameObject obj = Instantiate(blockObj); obj.SetActive(true); nowBlockNumber++; return obj; } // 砖块数量达到上限 else { Debug.LogError(\"砖块数量已达上限\"); return null; } } public void DestroyBlockObject(GameObject blocks) { // 将砖块回收到队列中,而不是直接销毁,将 Active 设为 false nowBlockNumber--; blocks.SetActive(false); blockPool.Enqueue(blocks); // 将砖块加入队列 }}
2.2.4官方对象池
其实Unity官方已经为我们封装好了对象池,但是不理解其本质直接使用还是不提倡,建议把上面几种方法弄懂,理解了对象池的基本原理再使用喵。
构造函数:public ObjectPool(Func createFunc, Action actionOnGet, Action actionOnRelease, Action actionOnDestroy, bool collectionCheck, int defaultCapacity, int maxSize);
参数:
createFunc:用于在池为空时创建新实例。
actionOnGet:从池中获取实例时调用的回调函数。可以用于激活对象。
actionOnRelease:当实例返回到池时调用的回调函数。可以用于清理或禁用实例。
actionOnDestroy:当由于池达到最大大小而无法将元素返回到池时调用的回调函数。可以用于销毁对象。
collectionCheck:是否在编辑器中进行集合检查。如果实例已经在池中,则会抛出异常。
defaultCapacity:创建池时的默认容量。
maxSize:池的最大大小。当池达到最大大小时,返回到池中的任何其他实例都将被忽略,并可以被垃圾收集。
属性:
CountActive:池已创建但当前正在使用且尚未返回的对象数。
CountAll:活动和非活动对象的总数。
CountInactive:池中当前可用的对象数。
方法:
Clear:删除所有池项。如果池包含 destroy
回调函数,那么它将被池中的每个项目调用。
Dispose:删除所有池项。与 Clear
类似,但通常用于对象池的销毁。
Get:从池中获取实例。如果池为空,则将创建一个新实例。
Release:将实例返回到池中。如果池已满,则调用 actionOnDestroy
。
代码:
using System;using System.Collections;using System.Collections.Generic;using UnityEngine;public class BlockPool : MonoBehaviour{ public static BlockPool instance; //单例 public GameObject blockObj; //砖块的预制体 public int blockCountInitial = 20; //初始砖块数量 public int blockCountMax = 50; //砖块上限 private ObjectPool blockPool; private void Awake() { instance = this; } private void Start() { // 初始化对象池 blockPool = new ObjectPool( createFunc: () => { GameObject obj = Instantiate(blockObj); obj.SetActive(false); //初始状态为非激活 return obj; }, actionOnGet: (obj) => { obj.SetActive(true); //激活对象 }, actionOnRelease: (obj) => { obj.SetActive(false); // 禁用对象 }, actionOnDestroy: (obj) => { Destroy(obj); //销毁对象 }, collectionCheck: true, defaultCapacity: blockCountInitial, //初始容量 maxSize: blockCountMax //最大容量 ); } public GameObject GetBlockObject() { //从对象池中获取对象 return blockPool.Get(); } public void DestroyBlockObject(GameObject block) { // 将对象返回到对象池 blockPool.Release(block); }}
三:泛型对象池
泛型对象池,及可以放任何东西的池子,不局限于GameObject,而是其他的比如BUFF(站在范围内的敌人全部上虚弱)、效果(点击的物体获得持续一段时间的力)。那是如何实现的呢?
3.1建立一个泛型对象池类
这里构造了一个池子,使用栈来存储泛型对象,构造时你可以直接传入初始化的方法和摧毁时执行的方法,也可以留空(后续例子是留空的)。
using System;using System.Collections;using System.Collections.Concurrent;using System.Collections.Generic;using UnityEngine;public class GeneralPool{ private readonly ConcurrentStack _pool; //线程安全的栈,用于存储泛型对象 private readonly Func _objectFactory; //用于创建新对象的委托 private readonly Action _initializer; //用于初始化对象的委托 private readonly Action _destroyer; //用于销毁对象的委托 private readonly int _maxPoolSize; //最大池容量 //构造函数, public GeneralPool(Func objectFactory, Action initializer = null, Action destroyer = null, int maxPoolSize = 100) { _pool = new ConcurrentStack(); _objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory)); _initializer = initializer; _destroyer = destroyer; _maxPoolSize = maxPoolSize; } //取对象 public T GetObject() { if (_pool.TryPop(out T obj)) { //如果对象池中有对象,直接复用 _initializer?.Invoke(obj); return obj; } else { //如果对象池为空,创建新对象 T newObj = _objectFactory(); _initializer?.Invoke(newObj); return newObj; } } //销毁对象 public void ReturnObject(T obj) { if (_pool.Count < _maxPoolSize) { _pool.Push(obj); Debug.Log(\"回到池子,池子大小:\"+_pool.Count); } else { // 如果池已满,销毁对象 _destroyer?.Invoke(obj); Debug.Log(\"池满,销毁\"); } }}
3.2建立效果
泛型对象可以是很多类型,所以在上面的基础上,我们需要建立我们想要的效果,比如这里我想要当右键一个砖块就让他收到一个持续两秒,向上的力(砖块已经有刚体)。
using System.Collections;using System.Collections.Generic;using UnityEngine;public class UpEffect{ public GameObject Target { get; set; } public float Duration { get; set; } public float ForceMagnitude { get; set; } public UpEffect(GameObject target, float duration, float forceMagnitude) { Target = target; Duration = duration; ForceMagnitude = forceMagnitude; } public void Activate() { // 开始施加力 Debug.Log(\"开始施加力\"); Target.GetComponent().AddForce(Vector2.up * ForceMagnitude, ForceMode2D.Force); } public void Deactivate() { // 停止施加力 Debug.Log(\"停止施加力\"); Target.GetComponent().velocity = Vector2.zero; Target.GetComponent().angularVelocity = 0.0f; }}
3.3构造效果池
构造一个对应效果的池子,初始化力的时间和大小,后续生成效果的时候再传入对象。
using System.Collections;using System.Collections.Generic;using UnityEngine;public class UpEffectPool : MonoBehaviour{ private GeneralPool _effectPool; private void Start() { // 初始化对象池 _effectPool = new GeneralPool( objectFactory: () => new UpEffect(null, 2f, 100.0f), initializer: effect => { }, destroyer: effect => { }, maxPoolSize: 10 ); } public UpEffect GetEffect(GameObject target) { // 从对象池中获取一个Effect对象 UpEffect effect = _effectPool.GetObject(); effect.Target = target; return effect; } public void ReturnEffect(UpEffect effect) { // 将Effect对象返回到对象池 _effectPool.ReturnObject(effect); }}
3.4射线检测并添加效果
因为每个方块的效果是独立的,计时也要独立,所以计时放在了每个砖块上。
using System.Collections;using System.Collections.Generic;using UnityEngine;public class BlockForceApplier : MonoBehaviour{ public UpEffectPool effectPool; //引用EffectPool private UpEffect currentEffect = null; //当前使用的Effect对象 private float elapsedTime = 0.0f; //已经过去的时间 private void Update() { // 检查是否点击了物体 if (Input.GetMouseButtonDown(1)) { RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero); if (hit.collider != null && hit.collider.CompareTag(\"block\")) { Debug.Log(\"获取到Block,开始添加效果\"); // 从对象池中获取一个Effect对象 currentEffect = effectPool.GetEffect(hit.collider.gameObject); currentEffect.Activate(); elapsedTime = 0.0f; } } // 如果有Effect对象正在使用 if (currentEffect != null) { elapsedTime += Time.deltaTime; // 如果时间超过持续时间,停止施加力并返回Effect对象 if (elapsedTime >= currentEffect.Duration) { currentEffect.Deactivate(); effectPool.ReturnEffect(currentEffect); currentEffect = null; elapsedTime = 0.0f; } } }}
3.5实现效果
就能实现以下效果: