美文网首页
Unity 对象池

Unity 对象池

作者: HJDaryl | 来源:发表于2020-11-29 14:47 被阅读0次

    最近在学习 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));
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:Unity 对象池

          本文链接:https://www.haomeiwen.com/subject/ujdmiqtx.html