Unity中的通用對(duì)象池

本文為博主原創(chuàng)文章,歡迎轉(zhuǎn)載。請(qǐng)保留博主鏈接http://blog.csdn.net/andrewfan


Unity編程標(biāo)準(zhǔn)導(dǎo)引-3.4 Unity中的通用對(duì)象池

本節(jié)通過(guò)一個(gè)簡(jiǎn)單的射擊子彈的示例來(lái)介紹Transform的用法。子彈射擊本身很容易制作,只要制作一個(gè)子彈Prefab,再做一個(gè)發(fā)生器,使用發(fā)生器控制按頻率產(chǎn)生子彈,即克隆子彈Prefab,然后為每個(gè)子彈寫(xiě)上運(yùn)動(dòng)邏輯就可以了。這本該是很簡(jiǎn)單的事情。不過(guò)問(wèn)題來(lái)了,發(fā)射出去后的子彈如何處理?直接Destroy嗎?這太浪費(fèi)了,要知道Unity的Mono內(nèi)存是不斷增長(zhǎng)的。就是說(shuō)出了Unity內(nèi)部的那些網(wǎng)格、貼圖等等資源內(nèi)存(簡(jiǎn)單說(shuō)就是繼承自UnityEngine下的Object的那些類(lèi)),而繼承自System下的Object的那些代碼產(chǎn)生的內(nèi)存即是Mono內(nèi)存,它只增不減。同樣,你不斷Destroy你的Unity對(duì)象也是要消耗性能去進(jìn)行回收,而子彈這種消耗品實(shí)在產(chǎn)生的太快了,我們必需加以控制。
  那么,我們?nèi)绾慰刂剖沟貌恢劣诓粩喈a(chǎn)生新的內(nèi)存呢?答案就是自己寫(xiě)內(nèi)存池。自己回收利用之前創(chuàng)建過(guò)的對(duì)象。所以這個(gè)章節(jié)的內(nèi)容,我們將重點(diǎn)放在寫(xiě)一個(gè)比較好的內(nèi)存池上。就我自己來(lái)講,在寫(xiě)一份較為系統(tǒng)的功能代碼之前,我考慮的首先不是這個(gè)框架是該如何的,而是從使用者的角度去考慮,這個(gè)代碼如何寫(xiě)使用起來(lái)才會(huì)比較方便,同樣也要考慮容易擴(kuò)展、通用性強(qiáng)、比較安全、減少耦合等等。
本文最后結(jié)果顯示如下:



3.4.1、從使用者視角給出需求

首先,我所希望的這個(gè)內(nèi)存池的代碼最后使用應(yīng)該是這樣的。

  • Bullet a = Pool.Take<Bullet>(); //從池中立刻獲取一個(gè)單元,如果單元不存在,則它需要為我立刻創(chuàng)建出來(lái)。返回一個(gè)Bullet腳本以便于后續(xù)控制。注意這里使用泛型,也就是說(shuō)它應(yīng)該可以兼容任意的腳本類(lèi)型。
  • Pool.restore(a);//當(dāng)使用完成Bullet之后,我可以使用此方法回收這個(gè)對(duì)象。注意這里實(shí)際上我已經(jīng)把Bullet這個(gè)組件的回收等同于某個(gè)GameObject(這里是子彈的GameObject)的回收。
      使用上就差不多是這樣了,希望可以有極其簡(jiǎn)單的方法來(lái)進(jìn)行獲取和回收操作。

3.4.2、內(nèi)存池單元結(jié)構(gòu)

最簡(jiǎn)單的內(nèi)存池形式,差不多就是兩個(gè)List,一個(gè)處于工作狀態(tài),一個(gè)處于閑置狀態(tài)。工作完畢的對(duì)象被移動(dòng)到閑置狀態(tài)列表,以便于后續(xù)的再次獲取和利用,形成一個(gè)循環(huán)。我們這里也會(huì)設(shè)計(jì)一個(gè)結(jié)構(gòu)來(lái)管理這兩個(gè)List,用于處理同一類(lèi)的對(duì)象。
  接下來(lái)是考慮內(nèi)存池單元的形式,我們考慮到內(nèi)存池單元要盡可能容易擴(kuò)展,就是可以兼容任意數(shù)據(jù)類(lèi)型,也就是說(shuō),假設(shè)我們的內(nèi)存池單元定為Pool_Unit,那么它不能影響后續(xù)繼承它的類(lèi)型,那我們最好使用接口,一旦使用類(lèi),那么就已經(jīng)無(wú)法兼容Unity組件,因?yàn)槲覀冏远x的Unity組件全部繼承自MonoBehavior。接下來(lái)考慮這個(gè)內(nèi)存單元該具有的功能,差不多有兩個(gè)基本功能要有:

  • restore();//自己主動(dòng)回收,為了方便后續(xù)調(diào)用,回收操作最好自己就有。
  • getState();//獲取狀態(tài),這里是指獲取當(dāng)前是處于工作狀態(tài)還是閑置狀態(tài),也是一個(gè)標(biāo)記,用于后續(xù)快速判斷。因?yàn)榻涌谥袩o(wú)法存儲(chǔ)單元,這里使用變通的方法,就是留給實(shí)現(xiàn)去處理,接口中要求具體實(shí)現(xiàn)需要提供一個(gè)狀態(tài)標(biāo)記。
      綜合內(nèi)存池單元和狀態(tài)標(biāo)記,給出如下代碼:
namespace AndrewBox.Pool
{
    public interface Pool_Unit
    {
        Pool_UnitState state();
        void setParentList(object parentList);
        void restore();
    }
    public enum Pool_Type
    {
        Idle,
        Work
    }
    public class Pool_UnitState
    {
        public Pool_Type InPool
        {
            get;
            set;
        }
    }
}

3.4.3、單元組結(jié)構(gòu)

接下來(lái)考慮單元組,也就是前面所說(shuō)的針對(duì)某一類(lèi)的單元進(jìn)行管理的結(jié)構(gòu)。它內(nèi)部有兩個(gè)列表,一個(gè)工作,一個(gè)閑置,單元在工作和閑置之間轉(zhuǎn)換循環(huán)。它應(yīng)該具有以下功能:

  • 創(chuàng)建新單元;使用抽象方法,不限制具體創(chuàng)建方法。對(duì)于Unity而言,可能需要從Prefab克隆,那么最好有方法可以從指定的Prefab模板復(fù)制創(chuàng)建。
  • 獲取單元;從閑置表中查找,找不到則創(chuàng)建。
  • 回收單元;將其子單元進(jìn)行回收。
      綜合單元組結(jié)構(gòu)的功能,給出如下代碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AndrewBox.Pool
{
    public abstract class Pool_UnitList<T> where T:class,Pool_Unit
    {
        protected object m_template;
        protected List<T> m_idleList;
        protected List<T> m_workList;
        protected int m_createdNum = 0;
        public Pool_UnitList()
        {
            m_idleList = new List<T>();
            m_workList = new List<T>();
        }



        /// <summary>
        /// 獲取一個(gè)閑置的單元,如果不存在則創(chuàng)建一個(gè)新的
        /// </summary>
        /// <returns>閑置單元</returns>
        public virtual T takeUnit<UT>() where UT:T
        {
            T unit;
            if (m_idleList.Count > 0)
            {
                unit = m_idleList[0];
                m_idleList.RemoveAt(0);
            }
            else
            {
                unit = createNewUnit<UT>();
                unit.setParentList(this);
                m_createdNum++;
            }
            m_workList.Add(unit);
            unit.state().InPool = Pool_Type.Work;
            OnUnitChangePool(unit);
            return unit;
        }
        /// <summary>
        /// 歸還某個(gè)單元
        /// </summary>
        /// <param name="unit">單元</param>
        public virtual void restoreUnit(T unit)
        {
            if (unit!=null && unit.state().InPool == Pool_Type.Work)
            {
                m_workList.Remove(unit);
                m_idleList.Add(unit);
                unit.state().InPool = Pool_Type.Idle;
                OnUnitChangePool(unit);
            }
        }
        /// <summary>
        /// 設(shè)置模板
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="template"></param>
        public void setTemplate(object template)
        {
            m_template = template;
        }
        protected abstract void OnUnitChangePool(T unit);
        protected abstract T createNewUnit<UT>() where UT : T;
    }
}

3.4.4、內(nèi)存池結(jié)構(gòu)

內(nèi)存池是一些列單元組的集合,它主要使用多個(gè)單元組具體實(shí)現(xiàn)內(nèi)存單元的回收利用。同時(shí)把接口盡可能包裝的簡(jiǎn)單,以便于用戶(hù)調(diào)用,因?yàn)橛脩?hù)只與內(nèi)存池進(jìn)行打交道。另外,我們最好把內(nèi)存池做成一個(gè)組件,這樣便于方便進(jìn)行初始化、更新(目前不需要,或許未來(lái)你需要執(zhí)行某種更新操作)等工作的管理。這樣,我們把內(nèi)存池結(jié)構(gòu)繼承自上個(gè)章節(jié)的BaseBehavior。獲得如下代碼:

using AndrewBox.Comp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AndrewBox.Pool
{
    public abstract class Pool_Base<UnitType, UnitList> : BaseBehavior
        where UnitType : class,Pool_Unit
        where UnitList : Pool_UnitList<UnitType>, new()
    {
        /// <summary>
        /// 緩沖池,按類(lèi)型存放各自分類(lèi)列表
        /// </summary>
        private Dictionary<Type, UnitList> m_poolTale = new Dictionary<Type, UnitList>();

        protected override void OnInitFirst()
        {
        }

        protected override void OnInitSecond()
        {

        }

        protected override void OnUpdate()
        {

        }

        /// <summary>
        /// 獲取一個(gè)空閑的單元
        /// </summary>
        public T takeUnit<T>() where T : class,UnitType
        {
            UnitList list = getList<T>();
            return list.takeUnit<T>() as T;
        }

        /// <summary>
        /// 在緩沖池中獲取指定單元類(lèi)型的列表,
        /// 如果該單元類(lèi)型不存在,則立刻創(chuàng)建。
        /// </summary>
        /// <typeparam name="T">單元類(lèi)型</typeparam>
        /// <returns>單元列表</returns>
        public UnitList getList<T>() where T : UnitType
        {
            var t = typeof(T);
            UnitList list = null;
            m_poolTale.TryGetValue(t, out list);
            if (list == null)
            {
                list = createNewUnitList<T>();
                m_poolTale.Add(t, list);
            }
            return list;
        }
        protected abstract UnitList createNewUnitList<UT>() where UT : UnitType;
    }
}

3.4.5、組件化
  目前為止,上述的結(jié)構(gòu)都沒(méi)有使用到組件,沒(méi)有使用到UnityEngine,也就是說(shuō)它們不受限使用于Unity組件或者普通的類(lèi)。當(dāng)然使用起來(lái)也會(huì)比較麻煩。由于我們實(shí)際需要的內(nèi)存池單元常常用于某種具體組件對(duì)象,比如子彈,那么我們最好針對(duì)組件進(jìn)一步實(shí)現(xiàn)。也就是說(shuō),定制一種適用于組件的內(nèi)存池單元。同時(shí)也定制出相應(yīng)的單元組,組件化的內(nèi)存池結(jié)構(gòu)。
  另外,由于閑置的單元都需要被隱藏掉,我們?cè)诮M件化的內(nèi)存池單元中需要設(shè)置兩個(gè)GameObject節(jié)點(diǎn),一個(gè)可見(jiàn)節(jié)點(diǎn),一個(gè)隱藏節(jié)點(diǎn)。當(dāng)組件單元工作時(shí),其對(duì)應(yīng)的GameObject被移動(dòng)到可見(jiàn)節(jié)點(diǎn)下方(當(dāng)然你也可以手動(dòng)再根據(jù)需要修改它的父節(jié)點(diǎn))。當(dāng)組件單元閑置時(shí),其對(duì)應(yīng)的GameObject也會(huì)被移動(dòng)到隱藏節(jié)點(diǎn)下方。
  綜合以上,給出以下代碼:

using AndrewBox.Comp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace AndrewBox.Pool
{

    public class Pool_Comp:Pool_Base<Pooled_BehaviorUnit,Pool_UnitList_Comp>
    {
        [SerializeField][Tooltip("運(yùn)行父節(jié)點(diǎn)")]
        protected Transform m_work;
        [SerializeField][Tooltip("閑置父節(jié)點(diǎn)")]
        protected Transform m_idle;

        protected override void OnInitFirst()
        {
            if (m_work == null)
            {
                m_work = CompUtil.Create(m_transform, "work");
            }
            if (m_idle == null)
            {
                m_idle = CompUtil.Create(m_transform, "idle");
                m_idle.gameObject.SetActive(false);
            }
        }

        public void OnUnitChangePool(Pooled_BehaviorUnit unit)
        {
            if (unit != null)
            {
                var inPool=unit.state().InPool;
                if (inPool == Pool_Type.Idle)
                {
                    unit.m_transform.SetParent(m_idle);
                }
                else if (inPool == Pool_Type.Work)
                {
                    unit.m_transform.SetParent(m_work);
                }
            }
        }
        protected override Pool_UnitList_Comp createNewUnitList<UT>()
        {
            Pool_UnitList_Comp list = new Pool_UnitList_Comp();
            list.setPool(this);
            return list;
        }


    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace AndrewBox.Pool
{
    public class Pool_UnitList_Comp : Pool_UnitList<Pooled_BehaviorUnit>
    {
        protected Pool_Comp m_pool;
        public void setPool(Pool_Comp pool)
        {
            m_pool = pool;
        }
        protected override Pooled_BehaviorUnit createNewUnit<UT>() 
        {
            GameObject result_go = null;
            if (m_template != null && m_template is GameObject)
            {
                result_go = GameObject.Instantiate((GameObject)m_template);
            }
            else
            {
                result_go = new GameObject();
                result_go.name = typeof(UT).Name;
            }
            result_go.name =result_go.name + "_"+m_createdNum;
            UT comp = result_go.GetComponent<UT>();
            if (comp == null)
            {
                comp = result_go.AddComponent<UT>();
            }
            comp.DoInit();
            return comp;
        }

        protected override void OnUnitChangePool(Pooled_BehaviorUnit unit)
        {
            if (m_pool != null)
            {
                m_pool.OnUnitChangePool(unit);
            }
        }
    }
}

using AndrewBox.Comp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AndrewBox.Pool
{
    public abstract class Pooled_BehaviorUnit : BaseBehavior, Pool_Unit
    {
        //單元狀態(tài)對(duì)象
        protected Pool_UnitState m_unitState = new Pool_UnitState();
        //父列表對(duì)象
        Pool_UnitList<Pooled_BehaviorUnit> m_parentList;
        /// <summary>
        /// 返回一個(gè)單元狀態(tài),用于控制當(dāng)前單元的閑置、工作狀態(tài)
        /// </summary>
        /// <returns>單元狀態(tài)</returns>
        public virtual Pool_UnitState state()
        {
            return m_unitState;
        }
        /// <summary>
        /// 接受父列表對(duì)象的設(shè)置
        /// </summary>
        /// <param name="parentList">父列表對(duì)象</param>
        public virtual void setParentList(object parentList)
        {
            m_parentList = parentList as Pool_UnitList<Pooled_BehaviorUnit>;
        }
        /// <summary>
        /// 歸還自己,即將自己回收以便再利用
        /// </summary>
        public virtual void restore()
        {
            if (m_parentList != null)
            {
                m_parentList.restoreUnit(this);
            }
        }

    }
}

3.4.6、內(nèi)存池單元具體化
接下來(lái),我們將Bullet具體化為一種內(nèi)存池單元,使得它可以方便從內(nèi)存池中創(chuàng)建出來(lái)。

using UnityEngine;
using System.Collections;
using AndrewBox.Comp;
using AndrewBox.Pool;

public class Bullet : Pooled_BehaviorUnit 
{
    [SerializeField][Tooltip("移動(dòng)速度")]
    private float m_moveVelocity=10;
    [SerializeField][Tooltip("移動(dòng)時(shí)長(zhǎng)")]
    private float m_moveTime=3;
    [System.NonSerialized][Tooltip("移動(dòng)計(jì)數(shù)")]
    private float m_moveTimeTick;
    protected override void OnInitFirst()
    {
    }

    protected override void OnInitSecond()
    {
    }

    protected override void OnUpdate()
    {
        float deltaTime = Time.deltaTime;
        m_moveTimeTick += deltaTime;
        if (m_moveTimeTick >= m_moveTime)
        {
            m_moveTimeTick = 0;
            this.restore();
        }
        else
        {
            var pos = m_transform.localPosition;
            pos.z += m_moveVelocity * deltaTime;
            m_transform.localPosition = pos;
        }
    }
}

3.4.7、內(nèi)存池的使用
最后就是寫(xiě)一把槍來(lái)發(fā)射子彈了,這個(gè)邏輯也相對(duì)簡(jiǎn)單。為了把內(nèi)存池做成單例模式并存放在單獨(dú)的GameObject,我們還需要另外一個(gè)單例單元管理器的輔助,一并給出。

using UnityEngine;
using System.Collections;
using AndrewBox.Comp;
using AndrewBox.Pool;

public class Gun_Simple : BaseBehavior 
{

    [SerializeField][Tooltip("模板對(duì)象")]
    private GameObject m_bulletTemplate;
    [System.NonSerialized][Tooltip("組件對(duì)象池")]
    private Pool_Comp m_compPool;
    [SerializeField][Tooltip("產(chǎn)生間隔")]
    private float m_fireRate=0.5f;
     [System.NonSerialized][Tooltip("產(chǎn)生計(jì)數(shù)")]
    private float m_fireTick;
    protected override void OnInitFirst()
    {
        m_compPool = Singletons.Get<Pool_Comp>("pool_comps");
        m_compPool.getList<Bullet>().setTemplate(m_bulletTemplate);
    }

    protected override void OnInitSecond()
    {

    }

    protected override void OnUpdate()
    {
        m_fireTick -= Time.deltaTime;
        if (m_fireTick < 0)
        {
            m_fireTick += m_fireRate;
            fire();
        }
    }
    protected void fire()
    {
        Bullet bullet =  m_compPool.takeUnit<Bullet>();
        bullet.m_transform.position = m_transform.position;
        bullet.m_transform.rotation = m_transform.rotation;
    }
}
using AndrewBox.Comp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace AndrewBox.Comp
{
    /// <summary>
    /// 單例單元管理器
    /// 你可以創(chuàng)建單例組件,每個(gè)單例組件對(duì)應(yīng)一個(gè)GameObject。
    /// 你可以為單例命名,名字同時(shí)也會(huì)作為GameObject的名字。
    /// 這些產(chǎn)生的單例一般用作管理器。
    /// </summary>
    public static class Singletons
    {
        private static Dictionary<string, BaseBehavior> m_singletons = new Dictionary<string, BaseBehavior>();
        public static T Get<T>(string name) where T:BaseBehavior
        {
            
            BaseBehavior singleton = null;
            m_singletons.TryGetValue(name, out singleton);
            if (singleton == null)
            {
                GameObject newGo = new GameObject(name);
                singleton = newGo.AddComponent<T>();
                m_singletons.Add(name, singleton);
            }
            return singleton as T;
        }
        public static void Destroy(string name)
        {
            BaseBehavior singleton = null;
            m_singletons.TryGetValue(name, out singleton);
            if (singleton != null)
            {
                m_singletons.Remove(name);
                GameObject.DestroyImmediate(singleton.gameObject);
            }
        }
        public static void Clear()
        {
            List<string> keys = new List<string>();
            foreach (var key in m_singletons.Keys)
            {
                keys.Add(key);
            }
            foreach (var key in keys)
            {
                Destroy(key);
            }
        }

    }
}

3.4.8、總結(jié)
最終,我們寫(xiě)出了所有的代碼,這個(gè)內(nèi)存池是通用的,而且整個(gè)游戲工程,你幾乎只需要這樣的一個(gè)內(nèi)存池,就可以管理所有的數(shù)量眾多且種類(lèi)繁多的活動(dòng)單元。而調(diào)用處只有以下幾行代碼即可輕松管理。

        m_compPool = Singletons.Get<Pool_Comp>("pool_comps");//創(chuàng)建內(nèi)存池
        m_compPool.getList<Bullet>().setTemplate(m_bulletTemplate);//設(shè)置模板
        Bullet bullet =  m_compPool.takeUnit<Bullet>();//索取單元
        bullet.restore(); //回收單元

最終當(dāng)你正確使用它時(shí),你的GameObject內(nèi)存不會(huì)再無(wú)限制增長(zhǎng),它將出現(xiàn)類(lèi)似的下圖循環(huán)利用。


本例完整項(xiàng)目資源請(qǐng)參見(jiàn)我的CSDN博客:http://blog.csdn.net/andrewfan
本文為博主原創(chuàng)文章,歡迎轉(zhuǎn)載。請(qǐng)保留博主鏈接http://blog.csdn.net/andrewfan

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容