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); } } } }
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using UnityEngine;
- namespace AndrewBox.Comp
- {
- public static class CompUtil
- {
- /// <summary>
- /// 在指定節點的下方創建一個新的GameObject
- /// </summary>
- /// <param name="_transform">父節點</param>
- /// <param name="name">名稱</param>
- /// <returns>新節點變換對象</returns>
- public static Transform Create(Transform _transform,string name)
- {
- GameObject goNew = new GameObject(name);
- Transform trNew = goNew.transform;
- if (_transform != null)
- {
- trNew.SetParent(_transform);
- }
- trNew.localPosition = Vector3.zero;
- trNew.localScale = Vector3.one;
- trNew.localRotation = Quaternion.identity;
- goNew.name = name;
- return trNew;
- }
- }
- }
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace AndrewBox.Comp
{
public static class CompUtil
{
/// <summary>
/// 在指定節點的下方創建一個新的GameObject
/// </summary>
/// <param name="_transform">父節點</param>
/// <param name="name">名稱</param>
/// <returns>新節點變換對象</returns>
public static Transform Create(Transform _transform,string name)
{
GameObject goNew = new GameObject(name);
Transform trNew = goNew.transform;
if (_transform != null)
{
trNew.SetParent(_transform);
}
trNew.localPosition = Vector3.zero;
trNew.localScale = Vector3.one;
trNew.localRotation = Quaternion.identity;
goNew.name = name;
return trNew;
}
}
}
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內存不會再無限制增長,它將出現類似的下圖循環利用。
本頁完整資源下載地址:http://download.csdn.net/detail/andrewfan/9764702
本文爲博主原創文章,歡迎轉載。請保留博主鏈接http://blog.csdn.net/andrewfan