1. 程式人生 > >【Unity程式設計】Unity3D-使用物件池高效管理記憶體

【Unity程式設計】Unity3D-使用物件池高效管理記憶體

Unity程式設計標準導引-3.4 Unity中的物件池

  本節通過一個簡單的射擊子彈的示例來介紹Transform的用法。子彈射擊本身很容易製作,只要製作一個子彈Prefab,再做一個發生器,使用發生器控制按頻率產生子彈,即克隆子彈Prefab,然後為每個子彈寫上運動邏輯就可以了。這本該是很簡單的事情。不過問題來了,發射出去後的子彈如何處理?直接Destroy嗎?這太浪費了,要知道Unity的Mono記憶體是不斷增長的。就是說出了Unity內部的那些網格、貼圖等等資源記憶體(簡單說就是繼承自UnityEngine下的Object的那些類),而我們自己寫的C#程式碼繼承自System下的Object,這些程式碼產生的記憶體即是Mono記憶體,它只增不減。同樣,你不斷Destroy你的Unity物件也是要消耗效能去進行回收,而子彈這種消耗品實在產生的太快了,我們必需加以控制。   那麼,我們如何控制使得不至於不斷產生新的記憶體呢?答案就是自己寫記憶體池。自己回收利用之前建立過的物件。所以這個章節的內容,我們將重點放在寫一個比較好的記憶體池上。就我自己來講,在寫一份較為系統的功能程式碼之前,我考慮的首先不是這個框架是該如何的,而是從使用者的角度去考慮,這個程式碼如何寫使用起來才會比較方便,同樣也要考慮容易擴充套件、通用性強、比較安全、減少耦合等等。

本文最後結果顯示如下:

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

  首先,我所希望的這個記憶體池的程式碼最後使用應該是這樣的。

  • Bullet a = Pool.Take<Bullet>(); //從池中立刻獲取一個單元,如果單元不存在,則它需要為我立刻創建出來。返回一個Bullet指令碼以便於後續控制。注意這裡使用泛型,也就是說它應該可以相容任意的指令碼型別。
  • Pool.restore(a);//當使用完成Bullet之後,我可以使用此方法回收這個物件。注意這裡實際上我已經把Bullet這個元件的回收等同於某個GameObject(這裡是子彈的GameObject)的回收。   使用上就差不多是這樣了,希望可以有極其簡單的方法來進行獲取和回收操作。

3.4.2、記憶體池單元結構

  最簡單的記憶體池形式,差不多就是兩個List,一個處於工作狀態,一個處於閒置狀態。工作完畢的物件被移動到閒置狀態列表,以便於後續的再次獲取和利用,形成一個迴圈。我們這裡也會設計一個結構來管理這兩個List,用於處理同一類的物件。   接下來是考慮記憶體池單元的形式,我們考慮到記憶體池單元要儘可能容易擴充套件,就是可以相容任意資料型別,也就是說,假設我們的記憶體池單元定為Pool_Unit,那麼它不能影響後續繼承它的型別,那我們最好使用介面,一旦使用類,那麼就已經無法相容Unity元件,因為我們自定義的Unity元件全部繼承自MonoBehavior。接下來考慮這個記憶體單元該具有的功能,差不多有兩個基本功能要有:

  • restore();//自己主動回收,為了方便後續呼叫,回收操作最好自己就有。
  • getState();//獲取狀態,這裡是指獲取當前是處於工作狀態還是閒置狀態,也是一個標記,用於後續快速判斷。因為介面中無法儲存單元,這裡使用變通的方法,就是留給實現去處理,介面中要求具體實現需要提供一個狀態標記。   綜合記憶體池單元和狀態標記,給出如下程式碼:

    複製程式碼

    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、單元組結構

      接下來考慮單元組,也就是前面所說的針對某一類的單元進行管理的結構。它內部有兩個列表,一個工作,一個閒置,單元在工作和閒置之間轉換迴圈。它應該具有以下功能:
  • 建立新單元;使用抽象方法,不限制具體建立方法。對於Unity而言,可能需要從Prefab克隆,那麼最好有方法可以從指定的Prefab模板複製建立。
  • 獲取單元;從閒置表中查詢,找不到則建立。
  • 回收單元;將其子單元進行回收。   綜合單元組結構的功能,給出如下程式碼:

複製程式碼

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>
        /// 獲取一個閒置的單元,如果不存在則建立一個新的
        /// </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、記憶體池結構

  記憶體池是一些列單元組的集合,它主要使用多個單元組具體實現記憶體單元的回收利用。同時把介面儘可能包裝的簡單,以便於使用者呼叫,因為使用者只與記憶體池進行打交道。另外,我們最好把記憶體池做成一個元件,這樣便於方便進行初始化、更新(目前不需要,或許未來你需要執行某種更新操作)等工作的管理。這樣,我們把記憶體池結構繼承自上個章節的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>
        /// 在緩衝池中獲取指定單元型別的列表,
        /// 如果該單元型別不存在,則立刻建立。
        /// </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元件或者普通的類。當然使用起來也會比較麻煩。由於我們實際需要的記憶體池單元常常用於某種具體元件物件,比如子彈,那麼我們最好針對元件進一步實現。也就是說,定製一種適用於元件的記憶體池單元。同時也定製出相應的單元組,元件化的記憶體池結構。   另外,由於閒置的單元都需要被隱藏掉,我們在元件化的記憶體池單元中需要設定兩個GameObject節點,一個可見節點,一個隱藏節點。當元件單元工作時,其對應的GameObject被移動到可見節點下方(當然你也可以手動再根據需要修改它的父節點)。當元件單元閒置時,其對應的GameObject也會被移動到隱藏節點下方。   綜合以上,給出以下程式碼:

複製程式碼

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("執行父節點")]
        protected Transform m_work;
        [SerializeField][Tooltip("閒置父節點")]
        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
    {
        //單元狀態物件
        protected Pool_UnitState m_unitState = new Pool_UnitState();
        //父列表物件
        Pool_UnitList<Pooled_BehaviorUnit> m_parentList;
        /// <summary>
        /// 返回一個單元狀態,用於控制當前單元的閒置、工作狀態
        /// </summary>
        /// <returns>單元狀態</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);
            }
        }

    }
}

複製程式碼

  1. using System;

  2. using System.Collections.Generic;

  3. using System.Linq;

  4. using System.Text;

  5. using UnityEngine;

  6. namespace AndrewBox.Comp

  7. {

  8. public static class CompUtil

  9. {

  10. /// <summary>

  11. /// 在指定節點的下方建立一個新的GameObject

  12. /// </summary>

  13. /// <param name="_transform">父節點</param>

  14. /// <param name="name">名稱</param>

  15. /// <returns>新節點變換物件</returns>

  16. public static Transform Create(Transform _transform,string name)

  17. {

  18. GameObject goNew = new GameObject(name);

  19. Transform trNew = goNew.transform;

  20. if (_transform != null)

  21. {

  22. trNew.SetParent(_transform);

  23. }

  24. trNew.localPosition = Vector3.zero;

  25. trNew.localScale = Vector3.one;

  26. trNew.localRotation = Quaternion.identity;

  27. goNew.name = name;

  28. return trNew;

  29. }

  30. }

  31. }

3.4.6、記憶體池單元具體化

接下來,我們將Bullet具體化為一種記憶體池單元,使得它可以方便從記憶體池中創建出來。

複製程式碼

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、記憶體池的使用

最後就是寫一把槍來發射子彈了,這個邏輯也相對簡單。為了把記憶體池做成單例模式並存放在單獨的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>
    /// 單例單元管理器
    /// 你可以建立單例元件,每個單例元件對應一個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");//建立記憶體池
        m_compPool.getList<Bullet>().setTemplate(m_bulletTemplate);//設定模板
        Bullet bullet =  m_compPool.takeUnit<Bullet>();//索取單元
        bullet.restore(); //回收單元

最終當你正確使用它時,你的GameObject記憶體不會再無限制增長,它將出現類似的下圖迴圈利用。

物件池