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();
        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));
        }
    }
}

你可能感兴趣的:(Unity 对象池)