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() 方法回收指定的对象
/// <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>,该对象池具有以下几个重要特点:

  1. 只能缓存继承自 Component 的对象,例如 MonoBehaviour
  2. 当回收一个 Component 对象的时候,对应的 GameObject 对象要被禁用而不是销毁
  3. 当从该对象池获取一个 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>,它具有以下几个重要特点:

  1. 在创建对象池的时候需要指定一个 Prefab 对象用于 Instantiate() 拷贝
  2. 在创建对象池的时候需要指定一个名叫 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 的组件,它本身的功能很简单:

  1. 配置初始缓存对象个数
  2. 提供 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));
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容