Unity|Unity 对象池

Unity|Unity 对象池
文章图片
最近在学习 Unity 官方的 《Tower Defense Template》 游戏源码,其中对象池的设计个人觉得很有借鉴意义,所以就写了这篇文章作为总结,希望对大家有所帮助。
1 为什么使用对象池 使用 Unity 开发游戏的时候经常会创建很多游戏对象,有些对象的存活时间还非常的短暂,例如射击游戏中的子弹,频繁的对象创建和销毁会触发平凡的 GC 操作,这可能会在资源有限的平台上造成卡顿,所以我们会使用对象池来复用已有的对象。
对象池的基本原理就是将已经创建好的,或者事先创建好的的对象缓存在内存当中,需要使用的时候就从对象池中申请一个对象,不需要使用的时候就将对象回收到对象池中。
2 实现对象池的思路 我在网上查阅了一些关于对象池的文章,基本都是使用集合缓存 GameObject 对象,这样的做法从需求角度来说是没问题,但是个人觉得它违背了 Unity 的一个最基本的设计原则,就是所有的功能扩展最好都是组件化的,例如我们希望一个 GameObject 可以有碰撞功能,就给它添加一个 Collider 组件,而当我们不需要该功能的时候随时可以移除 Collider 组件。同理,如果我们希望一个 GameObject 可以被复用,最好的实现方式就是开发一个组件(Component),任何添加了该组件的 GameObject 就扩展出可以被对象池缓存的功能,这就是本文要介绍的对象池实现思路:
通过添加组件的方式让一个 GameObject 可以被对象池缓存。
为了实现组件化的对象缓存功能,我们需要了解一个最基本的知识点:
当 Instantiate() 复制一个 Component 对象的时候,同时也会复制其依附的 GameObject 对象。
基于 Instantiate() 复制对象的原理,我们在设计对象池的时候可以不再是面向 GameObject,而是面向 Component,也就是对象池中缓存的不再是 GameObject 对象,而是 Component 对象,接下来我们就通过代码实现复用 Component 的对象池。
3 实现对象池 首先,考虑到对象池的泛用性,我们要实现一个可以缓存任意类型对象的泛型对象池,该对象池有以下几个重要特点:

  1. 定义名叫 factory 的代理用于生产缓存的对象
  2. 定义名叫 reset 的对象用于复用对象时的重置操作
  3. 定义名叫 available 的 List 用于存储当前可以使用的对象
  4. 定义名叫 all 的 List 用于存储所有对象池可管理的对象,包括在用的和可用的对象
  5. 通过 Acquire() 方法从对象池中获取一个对象
  6. 当对象池中已经没有可以服用的对象时就通过 factory 创建一个新的对象
  7. 通过 Recycle() 方法回收指定的对象
/// /// Maintains a pool of objects /// public class Pool { /// /// Our factory function /// protected Func factory; /// /// Our resetting function /// protected readonly Action reset; /// /// A list of all available items /// protected readonly List available; /// /// A list of all items managed by the pool /// protected readonly List all; public int Remaining { get => available.Count; } public int Total { get => all.Count; }/// /// Create a new pool with a given number of starting elements /// /// The function that creates pool objects /// Function to use to reset items when retrieving from the pool /// The number of elements to seed the pool with public Pool(Func factory, Action reset, int initialCapacity) { available = new List(); all = new List(); this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); this.reset = reset; if (initialCapacity > 0) { Grow(initialCapacity); } }/// /// Creates a new blank pool /// /// The function that creates pool objects public Pool(Func factory) : this(factory, null, 0) { }/// /// Create a new pool with a given number of starting elements /// /// The function that creates pool objects /// The number of elements to seed the pool with public Pool(Func factory, int initialCapacity) : this(factory, null, initialCapacity) { }/// /// Gets an item from the pool, growing it if necessary /// /// public virtual T Acquire() { return Acquire(reset); }/// /// Gets an item from the pool, growing it if necessary, and with a specified reset function /// /// A function to use to reset the given object public virtual T Acquire(Action reset) { if (available.Count == 0) { Grow(1); } if (available.Count == 0) { throw new InvalidOperationException("Failed to grow pool"); }int itemIndex = available.Count - 1; T item = available[itemIndex]; available.RemoveAt(itemIndex); reset?.Invoke(item); return item; }/// /// Gets whether or not this pool contains a specified item /// public virtual bool Contains(T pooledItem) { return all.Contains(pooledItem); }/// /// Return an item to the pool /// public virtual void Recycle(T pooledItem) { if (all.Contains(pooledItem) && !available.Contains(pooledItem)) { RecycleInternal(pooledItem); } else { throw new InvalidOperationException("Trying to recycle an item to a pool that does not contain it: " + pooledItem + ", " + this); } }/// /// Return all items to the pool /// public virtual void RecycleAll() { RecycleAll(null); }/// /// Returns all items to the pool, and calls a delegate on each one /// public virtual void RecycleAll(Action preRecycle) { for (int i = 0; i < all.Count; ++i) { T item = all[i]; if (!available.Contains(item)) { // This item is current in use, so invoke preRecycle() before recycle it. preRecycle?.Invoke(item); RecycleInternal(item); } } }/// /// Grow the pool by a given number of elements /// public void Grow(int amount) { for (int i = 0; i < amount; ++i) { AddNewElement(); } }/// /// Returns an object to the available list. Does not check for consistency /// protected virtual void RecycleInternal(T element) { available.Add(element); }/// /// Adds a new element to the pool /// protected virtual T AddNewElement() { T newElement = factory(); all.Add(newElement); available.Add(newElement); return newElement; }/// /// Dummy factory that returns the default T value /// protected static T DummyFactory() { return default; } }

基于我们已经设计好的泛型对象池 Pool,接下来我们就扩展出一个专门用于缓存 Component 对象的泛型对象池,它的名字叫 UnityComponentPool,该对象池具有以下几个重要特点:
  1. 只能缓存继承自 Component 的对象,例如 MonoBehaviour
  2. 当回收一个 Component 对象的时候,对应的 GameObject 对象要被禁用而不是销毁
  3. 当从该对象池获取一个 Component 对象的时候,对应的 GameObject 对象要被激活
/// /// A variant pool that takes Unity components. Automatically enables and disables them as necessary /// public class UnityComponentPool : Pool where T : Component { /// /// Create a new pool with a given number of starting elements /// /// The function that creates pool objects /// Function to use to reset items when retrieving from the pool /// The number of elements to seed the pool with public UnityComponentPool(Func factory, Action reset, int initialCapacity) : base(factory, reset, initialCapacity) { }/// /// Creates a new blank pool /// /// The function that creates pool objects public UnityComponentPool(Func factory) : base(factory) { }/// /// Create a new pool with a given number of starting elements /// /// The function that creates pool objects /// The number of elements to seed the pool with public UnityComponentPool(Func factory, int initialCapacity) : base(factory, initialCapacity) { }/// /// Retrieve an enabled element from the pool /// public override T Acquire(Action reset) { T element = base.Acquire(reset); element.gameObject.SetActive(true); return element; }/// /// Automatically disable returned object /// protected override void RecycleInternal(T element) { element.gameObject.SetActive(false); base.RecycleInternal(element); }/// /// Keep newly created objects disabled /// protected override T AddNewElement() { T newElement = base.AddNewElement(); newElement.gameObject.SetActive(false); return newElement; } }

接下来,我们进一步扩展 Component 的对象池,实现一个可以通过指定 Prefab 自动创建 Component 对象的对象池,也就是说客户端在创建对象池的时候不再是指定 factory 代理,而是指定一个 Prefab,该对象池名叫 AutoComponentPrefabPool,它具有以下几个重要特点:
  1. 在创建对象池的时候需要指定一个 Prefab 对象用于 Instantiate() 拷贝
  2. 在创建对象池的时候需要指定一个名叫 initialize 的代理用于初始化新拷贝的 Prefab 对象
/// /// Variant pool that automatically instantiates objects from a given Unity component prefab /// public class AutoComponentPrefabPool : UnityComponentPool where T : Component { /// /// Our base prefab /// protected readonly T prefab; /// /// Initialisation method for objects /// protected readonly Action init; /// /// Create a new pool for the given Unity prefab /// /// The prefab we're cloning public AutoComponentPrefabPool(T prefab) : this(prefab, null, null, 0) { }/// /// Create a new pool for the given Unity prefab /// /// The prefab we're cloning /// An initialisation function to call after creating prefabs public AutoComponentPrefabPool(T prefab, Action initialize) : this(prefab, initialize, null, 0) { }/// /// Create a new pool for the given Unity prefab /// /// The prefab we're cloning /// An initialisation function to call after creating prefabs /// Function to use to reset items when retrieving from the pool public AutoComponentPrefabPool(T prefab, Action initialize, Action reset) : this(prefab, initialize, reset, 0) { }/// /// Create a new pool for the given Unity prefab with a given number of starting elements /// /// The prefab we're cloning /// The number of elements to seed the pool with public AutoComponentPrefabPool(T prefab, int initialCapacity) : this(prefab, null, null, initialCapacity) { }/// /// Create a new pool for the given Unity prefab /// /// The prefab we're cloning /// An initialisation function to call after creating prefabs /// Function to use to reset items when retrieving from the pool /// The number of elements to seed the pool with public AutoComponentPrefabPool(T prefab, Action init, Action reset, int initialCapacity) : base(DummyFactory, reset, 0) { // Pass 0 to initial capacity because we need to set ourselves up first // We then call Grow again ourselves this.init = init; this.prefab = prefab; factory = PrefabFactory; if (initialCapacity > 0) { Grow(initialCapacity); } }/// /// Create our new prefab item clone /// private T PrefabFactory() { T newElement = Object.Instantiate(prefab); initialize?.Invoke(newElement); return newElement; } }

到这一步为止,我们专门用于缓存 Component 的对象池算是开发完毕。
4 可缓冲组件 上面我们只是实现了可以缓存组件的对象池,还没有实现一开始就提到的可以让 GameObject 拥有被缓存功能的组件,接下来我们就来实现一个叫 Poolable 的组件,它本身的功能很简单:
  1. 配置初始缓存对象个数
  2. 提供 Recycle() 方法用于回收该对象
public class Poolable : MonoBehaviour { [SerializeField] private int initialPoolCapacity = 10; /// /// Number of poolables the pool will initialize /// public int InitialPoolCapacity { get => initialPoolCapacity; }/// /// Pool that this poolable belongs to /// public Pool Pool { get; set; }/// /// Repool this instance, and move us under the poolmanager /// public void Recycle() { PoolManager.Instance.Recycle(this); } }

5 对象池管理器 创建完 Poolable 之后,理论上我们就可以自己创建一个对象池来缓存 Poolable 对象了,但是为了让对象池的使用和管理更方便,我们接下来要创建一个单例对象池管理器,用于统一管理所有的对象池。
该对象池管理器默认的对象池类型是上面提到的 AutoComponentPrefabPool。对象池管理器的逻辑也很简单,就是当我们从管理器尝试获取一个对象时,如果没有该对象的对象池就新建一个对象池,否则就直接从对象池中获取复用的对象,同时还提供了回收对象的快捷方法。
注意:对象池管理器并不是必须的,你完全可以自己创建对象池单独使用。
using System; using System.Collections.Generic; using UnityEngine; /// /// Managers a dictionary of component pools, getting and returning /// public class PoolManager : Singleton{ /// /// List of poolables that will be used to initialize corresponding pools /// [SerializeField] private List poolables = new List(); /// /// Dictionary of pools, key is the prefab /// private Dictionary> pools; /// /// 从对象池中获取指定的对象,如果该对象还没有被池化,则创 /// 建一个新的对象池用于缓存该对象。 /// public Poolable Acquire(Poolable prefab) { return Acquire(prefab, null); }public Poolable Acquire(Poolable prefab, Action reset) { if (!pools.ContainsKey(prefab)) { pools.Add(prefab, new AutoComponentPrefabPool(prefab, PoolableInitialize, null, prefab.InitialPoolCapacity)); } AutoComponentPrefabPool pool = pools[prefab]; Poolable instance = pool.Acquire(reset); instance.Pool = pool; return instance; }private void PoolableInitialize(Component poolable) { poolable.transform.SetParent(transform, false); }/// /// 尝试从对象池中获取指定类型的对象,如果对象池中没有该类 /// 型的对象则重新创建一个新的对象。 /// public T TryAcquire(GameObject prefab) where T : Component { var poolable = prefab.GetComponent(); if (poolable != null && IsInstanceExists) { return Acquire(poolable).GetComponent(); } return Instantiate(prefab).GetComponent(); }/// /// 尝试从对象池中获取指定类型的对象,如果对象池中没有该类型的对象则重新创建一个新的对象。 /// public GameObject TryAcquire(GameObject prefab) { return TryAcquire(prefab, null); }public GameObject TryAcquire(GameObject prefab, Action reset) { var poolable = prefab.GetComponent(); if (poolable != null && IsInstanceExists) { return Acquire(poolable, reset).gameObject; } return Instantiate(prefab); }/// /// 回收指定的对象。 /// /// Poolable. public void Recycle(Poolable poolable) { poolable.transform.SetParent(transform, false); poolable.Pool.Recycle(poolable); }/// /// 尝试回收制定的对象,如果该对象无法回收就将其销毁。 /// public void TryRecycle(GameObject gameObject) { var poolable = gameObject.GetComponent【Unity|Unity 对象池】(); if (poolable != null && poolable.Pool != null && IsInstanceExists) { poolable.Recycle(); } else { Destroy(gameObject); } }protected override void Awake() { base.Awake(); pools = new Dictionary>(); foreach (Poolable poolable in poolables) { if (poolable == null) { continue; } pools.Add(poolable, new AutoComponentPrefabPool(poolable, Init, null, poolable.InitialPoolCapacity)); } } }

    推荐阅读