最近在学习 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 实现对象池
首先,考虑到对象池的泛用性,我们要实现一个可以缓存任意类型对象的泛型对象池,该对象池有以下几个重要特点:
- 定义名叫 factory 的代理用于生产缓存的对象
- 定义名叫 reset 的对象用于复用对象时的重置操作
- 定义名叫 available 的 List 用于存储当前可以使用的对象
- 定义名叫 all 的 List 用于存储所有对象池可管理的对象,包括在用的和可用的对象
- 通过 Acquire() 方法从对象池中获取一个对象
- 当对象池中已经没有可以服用的对象时就通过 factory 创建一个新的对象
- 通过 Recycle() 方法回收指定的对象
/// <summary>
/// Maintains a pool of objects
/// </summary>
public class Pool<T>
{
/// <summary>
/// Our factory function
/// </summary>
protected Func<T> factory;
/// <summary>
/// Our resetting function
/// </summary>
protected readonly Action<T> reset;
/// <summary>
/// A list of all available items
/// </summary>
protected readonly List<T> available;
/// <summary>
/// A list of all items managed by the pool
/// </summary>
protected readonly List<T> all;
public int Remaining { get => available.Count; }
public int Total { get => all.Count; }
/// <summary>
/// Create a new pool with a given number of starting elements
/// </summary>
/// <param name="factory">The function that creates pool objects</param>
/// <param name="reset">Function to use to reset items when retrieving from the pool</param>
/// <param name="initialCapacity">The number of elements to seed the pool with</param>
public Pool(Func<T> factory, Action<T> reset, int initialCapacity)
{
available = new List<T>();
all = new List<T>();
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
this.reset = reset;
if (initialCapacity > 0)
{
Grow(initialCapacity);
}
}
/// <summary>
/// Creates a new blank pool
/// </summary>
/// <param name="factory">The function that creates pool objects</param>
public Pool(Func<T> factory) : this(factory, null, 0) { }
/// <summary>
/// Create a new pool with a given number of starting elements
/// </summary>
/// <param name="factory">The function that creates pool objects</param>
/// <param name="initialCapacity">The number of elements to seed the pool with</param>
public Pool(Func<T> factory, int initialCapacity) : this(factory, null, initialCapacity) { }
/// <summary>
/// Gets an item from the pool, growing it if necessary
/// </summary>
/// <returns></returns>
public virtual T Acquire()
{
return Acquire(reset);
}
/// <summary>
/// Gets an item from the pool, growing it if necessary, and with a specified reset function
/// </summary>
/// <param name="reset">A function to use to reset the given object</param>
public virtual T Acquire(Action<T> 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;
}
/// <summary>
/// Gets whether or not this pool contains a specified item
/// </summary>
public virtual bool Contains(T pooledItem)
{
return all.Contains(pooledItem);
}
/// <summary>
/// Return an item to the pool
/// </summary>
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);
}
}
/// <summary>
/// Return all items to the pool
/// </summary>
public virtual void RecycleAll()
{
RecycleAll(null);
}
/// <summary>
/// Returns all items to the pool, and calls a delegate on each one
/// </summary>
public virtual void RecycleAll(Action<T> 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);
}
}
}
/// <summary>
/// Grow the pool by a given number of elements
/// </summary>
public void Grow(int amount)
{
for (int i = 0; i < amount; ++i)
{
AddNewElement();
}
}
/// <summary>
/// Returns an object to the available list. Does not check for consistency
/// </summary>
protected virtual void RecycleInternal(T element)
{
available.Add(element);
}
/// <summary>
/// Adds a new element to the pool
/// </summary>
protected virtual T AddNewElement()
{
T newElement = factory();
all.Add(newElement);
available.Add(newElement);
return newElement;
}
/// <summary>
/// Dummy factory that returns the default T value
/// </summary>
protected static T DummyFactory()
{
return default;
}
}
基于我们已经设计好的泛型对象池 Pool<T>,接下来我们就扩展出一个专门用于缓存 Component 对象的泛型对象池,它的名字叫 UnityComponentPool<T>,该对象池具有以下几个重要特点:
- 只能缓存继承自 Component 的对象,例如 MonoBehaviour
- 当回收一个 Component 对象的时候,对应的 GameObject 对象要被禁用而不是销毁
- 当从该对象池获取一个 Component 对象的时候,对应的 GameObject 对象要被激活
/// <summary>
/// A variant pool that takes Unity components. Automatically enables and disables them as necessary
/// </summary>
public class UnityComponentPool<T> : Pool<T> where T : Component
{
/// <summary>
/// Create a new pool with a given number of starting elements
/// </summary>
/// <param name="factory">The function that creates pool objects</param>
/// <param name="reset">Function to use to reset items when retrieving from the pool</param>
/// <param name="initialCapacity">The number of elements to seed the pool with</param>
public UnityComponentPool(Func<T> factory, Action<T> reset, int initialCapacity) : base(factory, reset, initialCapacity) { }
/// <summary>
/// Creates a new blank pool
/// </summary>
/// <param name="factory">The function that creates pool objects</param>
public UnityComponentPool(Func<T> factory) : base(factory) { }
/// <summary>
/// Create a new pool with a given number of starting elements
/// </summary>
/// <param name="factory">The function that creates pool objects</param>
/// <param name="initialCapacity">The number of elements to seed the pool with</param>
public UnityComponentPool(Func<T> factory, int initialCapacity) : base(factory, initialCapacity) { }
/// <summary>
/// Retrieve an enabled element from the pool
/// </summary>
public override T Acquire(Action<T> reset)
{
T element = base.Acquire(reset);
element.gameObject.SetActive(true);
return element;
}
/// <summary>
/// Automatically disable returned object
/// </summary>
protected override void RecycleInternal(T element)
{
element.gameObject.SetActive(false);
base.RecycleInternal(element);
}
/// <summary>
/// Keep newly created objects disabled
/// </summary>
protected override T AddNewElement()
{
T newElement = base.AddNewElement();
newElement.gameObject.SetActive(false);
return newElement;
}
}
接下来,我们进一步扩展 Component 的对象池,实现一个可以通过指定 Prefab 自动创建 Component 对象的对象池,也就是说客户端在创建对象池的时候不再是指定 factory 代理,而是指定一个 Prefab,该对象池名叫 AutoComponentPrefabPool<T>,它具有以下几个重要特点:
- 在创建对象池的时候需要指定一个 Prefab 对象用于 Instantiate() 拷贝
- 在创建对象池的时候需要指定一个名叫 initialize 的代理用于初始化新拷贝的 Prefab 对象
/// <summary>
/// Variant pool that automatically instantiates objects from a given Unity component prefab
/// </summary>
public class AutoComponentPrefabPool<T> : UnityComponentPool<T> where T : Component
{
/// <summary>
/// Our base prefab
/// </summary>
protected readonly T prefab;
/// <summary>
/// Initialisation method for objects
/// </summary>
protected readonly Action<T> init;
/// <summary>
/// Create a new pool for the given Unity prefab
/// </summary>
/// <param name="prefab">The prefab we're cloning</param>
public AutoComponentPrefabPool(T prefab) : this(prefab, null, null, 0) { }
/// <summary>
/// Create a new pool for the given Unity prefab
/// </summary>
/// <param name="prefab">The prefab we're cloning</param>
/// <param name="initialize">An initialisation function to call after creating prefabs</param>
public AutoComponentPrefabPool(T prefab, Action<T> initialize) : this(prefab, initialize, null, 0) { }
/// <summary>
/// Create a new pool for the given Unity prefab
/// </summary>
/// <param name="prefab">The prefab we're cloning</param>
/// <param name="initialize">An initialisation function to call after creating prefabs</param>
/// <param name="reset">Function to use to reset items when retrieving from the pool</param>
public AutoComponentPrefabPool(T prefab, Action<T> initialize, Action<T> reset) : this(prefab, initialize, reset, 0) { }
/// <summary>
/// Create a new pool for the given Unity prefab with a given number of starting elements
/// </summary>
/// <param name="prefab">The prefab we're cloning</param>
/// <param name="initialCapacity">The number of elements to seed the pool with</param>
public AutoComponentPrefabPool(T prefab, int initialCapacity) : this(prefab, null, null, initialCapacity) { }
/// <summary>
/// Create a new pool for the given Unity prefab
/// </summary>
/// <param name="prefab">The prefab we're cloning</param>
/// <param name="initialize">An initialisation function to call after creating prefabs</param>
/// <param name="reset">Function to use to reset items when retrieving from the pool</param>
/// <param name="initialCapacity">The number of elements to seed the pool with</param>
public AutoComponentPrefabPool(T prefab, Action<T> init, Action<T> 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);
}
}
/// <summary>
/// Create our new prefab item clone
/// </summary>
private T PrefabFactory()
{
T newElement = Object.Instantiate(prefab);
initialize?.Invoke(newElement);
return newElement;
}
}
到这一步为止,我们专门用于缓存 Component 的对象池算是开发完毕。
4 可缓冲组件
上面我们只是实现了可以缓存组件的对象池,还没有实现一开始就提到的可以让 GameObject 拥有被缓存功能的组件,接下来我们就来实现一个叫 Poolable 的组件,它本身的功能很简单:
- 配置初始缓存对象个数
- 提供 Recycle() 方法用于回收该对象
public class Poolable : MonoBehaviour
{
[SerializeField]
private int initialPoolCapacity = 10;
/// <summary>
/// Number of poolables the pool will initialize
/// </summary>
public int InitialPoolCapacity { get => initialPoolCapacity; }
/// <summary>
/// Pool that this poolable belongs to
/// </summary>
public Pool<Poolable> Pool { get; set; }
/// <summary>
/// Repool this instance, and move us under the poolmanager
/// </summary>
public void Recycle()
{
PoolManager.Instance.Recycle(this);
}
}
5 对象池管理器
创建完 Poolable 之后,理论上我们就可以自己创建一个对象池来缓存 Poolable 对象了,但是为了让对象池的使用和管理更方便,我们接下来要创建一个单例对象池管理器,用于统一管理所有的对象池。
该对象池管理器默认的对象池类型是上面提到的 AutoComponentPrefabPool
。对象池管理器的逻辑也很简单,就是当我们从管理器尝试获取一个对象时,如果没有该对象的对象池就新建一个对象池,否则就直接从对象池中获取复用的对象,同时还提供了回收对象的快捷方法。
注意:对象池管理器并不是必须的,你完全可以自己创建对象池单独使用。
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Managers a dictionary of component pools, getting and returning
/// </summary>
public class PoolManager : Singleton<PoolManager>
{
/// <summary>
/// List of poolables that will be used to initialize corresponding pools
/// </summary>
[SerializeField]
private List<Poolable> poolables = new List<Poolable>();
/// <summary>
/// Dictionary of pools, key is the prefab
/// </summary>
private Dictionary<Poolable, AutoComponentPrefabPool<Poolable>> pools;
/// <summary>
/// 从对象池中获取指定的 <see cref="Poolable"/> 对象,如果该对象还没有被池化,则创
/// 建一个新的对象池用于缓存该对象。
/// </summary>
public Poolable Acquire(Poolable prefab)
{
return Acquire(prefab, null);
}
public Poolable Acquire(Poolable prefab, Action<Poolable> reset)
{
if (!pools.ContainsKey(prefab))
{
pools.Add(prefab, new AutoComponentPrefabPool<Poolable>(prefab, PoolableInitialize, null, prefab.InitialPoolCapacity));
}
AutoComponentPrefabPool<Poolable> pool = pools[prefab];
Poolable instance = pool.Acquire(reset);
instance.Pool = pool;
return instance;
}
private void PoolableInitialize(Component poolable)
{
poolable.transform.SetParent(transform, false);
}
/// <summary>
/// 尝试从对象池中获取指定类型的 <see cref="Component"/> 对象,如果对象池中没有该类
/// 型的对象则重新创建一个新的对象。
/// </summary>
public T TryAcquire<T>(GameObject prefab) where T : Component
{
var poolable = prefab.GetComponent<Poolable>();
if (poolable != null && IsInstanceExists)
{
return Acquire(poolable).GetComponent<T>();
}
return Instantiate(prefab).GetComponent<T>();
}
/// <summary>
/// 尝试从对象池中获取指定类型的对象,如果对象池中没有该类型的对象则重新创建一个新的对象。
/// </summary>
public GameObject TryAcquire(GameObject prefab)
{
return TryAcquire(prefab, null);
}
public GameObject TryAcquire(GameObject prefab, Action<Poolable> reset)
{
var poolable = prefab.GetComponent<Poolable>();
if (poolable != null && IsInstanceExists)
{
return Acquire(poolable, reset).gameObject;
}
return Instantiate(prefab);
}
/// <summary>
/// 回收指定的 <see cref="Poolable"/> 对象。
/// </summary>
/// <param name="poolable">Poolable.</param>
public void Recycle(Poolable poolable)
{
poolable.transform.SetParent(transform, false);
poolable.Pool.Recycle(poolable);
}
/// <summary>
/// 尝试回收制定的 <see cref="GameObject"/> 对象,如果该对象无法回收就将其销毁。
/// </summary>
public void TryRecycle(GameObject gameObject)
{
var poolable = gameObject.GetComponent<Poolable>();
if (poolable != null && poolable.Pool != null && IsInstanceExists)
{
poolable.Recycle();
}
else
{
Destroy(gameObject);
}
}
protected override void Awake()
{
base.Awake();
pools = new Dictionary<Poolable, AutoComponentPrefabPool<Poolable>>();
foreach (Poolable poolable in poolables)
{
if (poolable == null)
{
continue;
}
pools.Add(poolable, new AutoComponentPrefabPool<Poolable>(poolable, Init, null, poolable.InitialPoolCapacity));
}
}
}