Unity中的通用對象池

本文為博主原創(chuàng)文章谦秧,歡迎轉載鼠次。請保留博主鏈接http://blog.csdn.net/andrewfan


Unity編程標準導引-3.4 Unity中的通用對象池

本節(jié)通過一個簡單的射擊子彈的示例來介紹Transform的用法。子彈射擊本身很容易制作邢笙,只要制作一個子彈Prefab啸如,再做一個發(fā)生器,使用發(fā)生器控制按頻率產生子彈氮惯,即克隆子彈Prefab叮雳,然后為每個子彈寫上運動邏輯就可以了。這本該是很簡單的事情妇汗。不過問題來了帘不,發(fā)射出去后的子彈如何處理?直接Destroy嗎杨箭?這太浪費了寞焙,要知道Unity的Mono內存是不斷增長的。就是說出了Unity內部的那些網格互婿、貼圖等等資源內存(簡單說就是繼承自UnityEngine下的Object的那些類)捣郊,而繼承自System下的Object的那些代碼產生的內存即是Mono內存,它只增不減慈参。同樣呛牲,你不斷Destroy你的Unity對象也是要消耗性能去進行回收,而子彈這種消耗品實在產生的太快了驮配,我們必需加以控制娘扩。
  那么,我們如何控制使得不至于不斷產生新的內存呢壮锻?答案就是自己寫內存池畜侦。自己回收利用之前創(chuàng)建過的對象。所以這個章節(jié)的內容躯保,我們將重點放在寫一個比較好的內存池上旋膳。就我自己來講,在寫一份較為系統(tǒng)的功能代碼之前途事,我考慮的首先不是這個框架是該如何的验懊,而是從使用者的角度去考慮,這個代碼如何寫使用起來才會比較方便尸变,同樣也要考慮容易擴展义图、通用性強、比較安全召烂、減少耦合等等碱工。
本文最后結果顯示如下:



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

首先,我所希望的這個內存池的代碼最后使用應該是這樣的怕篷。

  • Bullet a = Pool.Take<Bullet>(); //從池中立刻獲取一個單元历筝,如果單元不存在,則它需要為我立刻創(chuàng)建出來廊谓。返回一個Bullet腳本以便于后續(xù)控制梳猪。注意這里使用泛型,也就是說它應該可以兼容任意的腳本類型蒸痹。
  • Pool.restore(a);//當使用完成Bullet之后春弥,我可以使用此方法回收這個對象。注意這里實際上我已經把Bullet這個組件的回收等同于某個GameObject(這里是子彈的GameObject)的回收叠荠。
      使用上就差不多是這樣了匿沛,希望可以有極其簡單的方法來進行獲取和回收操作。

3.4.2榛鼎、內存池單元結構

最簡單的內存池形式逃呼,差不多就是兩個List,一個處于工作狀態(tài)借帘,一個處于閑置狀態(tài)。工作完畢的對象被移動到閑置狀態(tài)列表淌铐,以便于后續(xù)的再次獲取和利用肺然,形成一個循環(huán)。我們這里也會設計一個結構來管理這兩個List腿准,用于處理同一類的對象际起。
  接下來是考慮內存池單元的形式,我們考慮到內存池單元要盡可能容易擴展吐葱,就是可以兼容任意數據類型街望,也就是說,假設我們的內存池單元定為Pool_Unit弟跑,那么它不能影響后續(xù)繼承它的類型灾前,那我們最好使用接口,一旦使用類孟辑,那么就已經無法兼容Unity組件哎甲,因為我們自定義的Unity組件全部繼承自MonoBehavior。接下來考慮這個內存單元該具有的功能饲嗽,差不多有兩個基本功能要有:

  • restore();//自己主動回收炭玫,為了方便后續(xù)調用,回收操作最好自己就有貌虾。
  • getState();//獲取狀態(tài)吞加,這里是指獲取當前是處于工作狀態(tài)還是閑置狀態(tài),也是一個標記,用于后續(xù)快速判斷衔憨。因為接口中無法存儲單元叶圃,這里使用變通的方法,就是留給實現(xiàn)去處理巫财,接口中要求具體實現(xiàn)需要提供一個狀態(tài)標記盗似。
      綜合內存池單元和狀態(tài)標記,給出如下代碼:
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平项、單元組結構

接下來考慮單元組赫舒,也就是前面所說的針對某一類的單元進行管理的結構。它內部有兩個列表闽瓢,一個工作接癌,一個閑置,單元在工作和閑置之間轉換循環(huán)扣讼。它應該具有以下功能:

  • 創(chuàng)建新單元缺猛;使用抽象方法,不限制具體創(chuàng)建方法椭符。對于Unity而言荔燎,可能需要從Prefab克隆,那么最好有方法可以從指定的Prefab模板復制創(chuàng)建销钝。
  • 獲取單元有咨;從閑置表中查找,找不到則創(chuàng)建蒸健。
  • 回收單元座享;將其子單元進行回收。
      綜合單元組結構的功能似忧,給出如下代碼:
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>
        /// 獲取一個閑置的單元渣叛,如果不存在則創(chuàng)建一個新的
        /// </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>
        /// 歸還某個單元
        /// </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>
        /// 設置模板
        /// </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、內存池結構

內存池是一些列單元組的集合盯捌,它主要使用多個單元組具體實現(xiàn)內存單元的回收利用淳衙。同時把接口盡可能包裝的簡單,以便于用戶調用饺著,因為用戶只與內存池進行打交道滤祖。另外,我們最好把內存池做成一個組件瓶籽,這樣便于方便進行初始化匠童、更新(目前不需要,或許未來你需要執(zhí)行某種更新操作)等工作的管理塑顺。這樣汤求,我們把內存池結構繼承自上個章節(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>
        /// 緩沖池,按類型存放各自分類列表
        /// </summary>
        private Dictionary<Type, UnitList> m_poolTale = new Dictionary<Type, UnitList>();

        protected override void OnInitFirst()
        {
        }

        protected override void OnInitSecond()
        {

        }

        protected override void OnUpdate()
        {

        }

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

        /// <summary>
        /// 在緩沖池中獲取指定單元類型的列表扬绪,
        /// 如果該單元類型不存在竖独,則立刻創(chuàng)建。
        /// </summary>
        /// <typeparam name="T">單元類型</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挤牛、組件化
  目前為止莹痢,上述的結構都沒有使用到組件,沒有使用到UnityEngine墓赴,也就是說它們不受限使用于Unity組件或者普通的類竞膳。當然使用起來也會比較麻煩。由于我們實際需要的內存池單元常常用于某種具體組件對象诫硕,比如子彈坦辟,那么我們最好針對組件進一步實現(xiàn)。也就是說章办,定制一種適用于組件的內存池單元锉走。同時也定制出相應的單元組,組件化的內存池結構藕届。
  另外挪蹭,由于閑置的單元都需要被隱藏掉,我們在組件化的內存池單元中需要設置兩個GameObject節(jié)點休偶,一個可見節(jié)點梁厉,一個隱藏節(jié)點。當組件單元工作時椅贱,其對應的GameObject被移動到可見節(jié)點下方(當然你也可以手動再根據需要修改它的父節(jié)點)懂算。當組件單元閑置時只冻,其對應的GameObject也會被移動到隱藏節(jié)點下方庇麦。
  綜合以上,給出以下代碼:

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("運行父節(jié)點")]
        protected Transform m_work;
        [SerializeField][Tooltip("閑置父節(jié)點")]
        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)對象
        protected Pool_UnitState m_unitState = new Pool_UnitState();
        //父列表對象
        Pool_UnitList<Pooled_BehaviorUnit> m_parentList;
        /// <summary>
        /// 返回一個單元狀態(tài)喜德,用于控制當前單元的閑置山橄、工作狀態(tài)
        /// </summary>
        /// <returns>單元狀態(tài)</returns>
        public virtual Pool_UnitState state()
        {
            return m_unitState;
        }
        /// <summary>
        /// 接受父列表對象的設置
        /// </summary>
        /// <param name="parentList">父列表對象</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舍悯、內存池單元具體化
接下來航棱,我們將Bullet具體化為一種內存池單元,使得它可以方便從內存池中創(chuàng)建出來萌衬。

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

public class Bullet : Pooled_BehaviorUnit 
{
    [SerializeField][Tooltip("移動速度")]
    private float m_moveVelocity=10;
    [SerializeField][Tooltip("移動時長")]
    private float m_moveTime=3;
    [System.NonSerialized][Tooltip("移動計數")]
    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饮醇、內存池的使用
最后就是寫一把槍來發(fā)射子彈了,這個邏輯也相對簡單秕豫。為了把內存池做成單例模式并存放在單獨的GameObject朴艰,我們還需要另外一個單例單元管理器的輔助观蓄,一并給出。

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

public class Gun_Simple : BaseBehavior 
{

    [SerializeField][Tooltip("模板對象")]
    private GameObject m_bulletTemplate;
    [System.NonSerialized][Tooltip("組件對象池")]
    private Pool_Comp m_compPool;
    [SerializeField][Tooltip("產生間隔")]
    private float m_fireRate=0.5f;
     [System.NonSerialized][Tooltip("產生計數")]
    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)建單例組件祠墅,每個單例組件對應一個GameObject侮穿。
    /// 你可以為單例命名,名字同時也會作為GameObject的名字毁嗦。
    /// 這些產生的單例一般用作管理器亲茅。
    /// </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、總結
最終狗准,我們寫出了所有的代碼克锣,這個內存池是通用的,而且整個游戲工程驶俊,你幾乎只需要這樣的一個內存池娶耍,就可以管理所有的數量眾多且種類繁多的活動單元。而調用處只有以下幾行代碼即可輕松管理饼酿。

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

最終當你正確使用它時榕酒,你的GameObject內存不會再無限制增長,它將出現(xiàn)類似的下圖循環(huán)利用故俐。


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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末药版,一起剝皮案震驚了整個濱河市辑舷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌槽片,老刑警劉巖何缓,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異还栓,居然都是意外死亡碌廓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門剩盒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谷婆,“玉大人,你說我怎么就攤上這事辽聊〖涂妫” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵跟匆,是天一觀的道長异袄。 經常有香客問我,道長玛臂,這世上最難降的妖魔是什么烤蜕? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任埠帕,我火速辦了婚禮,結果婚禮上玖绿,老公的妹妹穿的比我還像新娘敛瓷。我一直安慰自己,他們只是感情好斑匪,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布呐籽。 她就那樣靜靜地躺著,像睡著了一般蚀瘸。 火紅的嫁衣襯著肌膚如雪狡蝶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天贮勃,我揣著相機與錄音贪惹,去河邊找鬼。 笑死寂嘉,一個胖子當著我的面吹牛奏瞬,可吹牛的內容都是我干的。 我是一名探鬼主播泉孩,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼硼端,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寓搬?” 一聲冷哼從身側響起珍昨,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎句喷,沒想到半個月后镣典,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡唾琼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年兄春,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片父叙。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡神郊,死狀恐怖肴裙,靈堂內的尸體忽然破棺而出趾唱,到底是詐尸還是另有隱情,我是刑警寧澤蜻懦,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布甜癞,位于F島的核電站,受9級特大地震影響宛乃,放射性物質發(fā)生泄漏悠咱。R本人自食惡果不足惜蒸辆,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望析既。 院中可真熱鬧躬贡,春花似錦、人聲如沸眼坏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宰译。三九已至檐蚜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沿侈,已是汗流浹背闯第。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留缀拭,地道東北人咳短。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像蛛淋,于是被迫代替她去往敵國和親诲泌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內容