:用 Dictionary + List 在 Unity 中實現(xiàn)高效的 GameObject 復用(Unity3DTraining Object Pool2 源碼解析))
示例工程【免費下載鏈接】Unity3DTraining【Unity雜貨鋪】unity大雜燴~項目地址https://gitcode.com/gh_mirrors/un/Unity3DTraining點擊查看免費下載本篇文章聚焦 Unity3DTraining 倉庫中MemoryPool_ObjectPool/Object Pool2示例的完整實現(xiàn)它不依賴任何第三方插件僅用Dictionarystring, ListGameObject兩個基礎容器就寫出了一套可復用的 GameObject 對象池。讀完本文你將掌握對象池的借取Get/歸還Store核心模型、預制體命名與 Key 匹配的底層細節(jié)并能在自己的射擊、刷怪、彈幕等高頻實例化場景中直接落地這套方案。文章配套源碼與場景位于 MemoryPool_ObjectPool/Object Pool2同倉庫還收錄了一篇泛型對象池理論譯文MemoryPool_ObjectPool/Object Pool兩篇可對照閱讀。圖 1Game 運行視圖白色小方塊即是從對象池中重復借出/歸還的子彈實例圖 2Hierarchy 面板中Gun節(jié)點下掛載的多個bullet(Clone)實例正是對象池緩存未激活子彈的直接證據(jù)一、為什么需要對象池頻繁實例化的性能賬本在 Unity 中大量重復出現(xiàn)的物體——例如發(fā)射的子彈、爆炸特效、飄字——如果每次都走Instantiate/Destroy會造成兩筆開銷實例化/銷毀的引擎開銷創(chuàng)建與銷毀 GameObject、掛載組件、分配本地內(nèi)存均需時間成本托管堆壓力每次實例化都會產(chǎn)生托管堆分配頻繁分配會推高 GC 頻率引發(fā)幀率抖動。對象池的基本思路在源碼注釋中寫得很直白GameObjectPool.cs將用過的對象保存起來等下一次需要這種對象的時候再拿出來重復使用。恰當?shù)厥褂脤ο蟪乜梢栽谝欢ǔ潭壬蠝p少頻繁創(chuàng)建對象所造成的開銷。同時源碼注釋也給出了重要警示并非所有對象都適合池化。維護對象池本身也有開銷如果對象生成成本很低池化反而可能使維護池的開銷大于新建對象的開銷導致性能不升反降。這一點是使用對象池前必須做的取舍判斷。二、核心設計借Get與還Store兩個靜態(tài)方法整套池的對外接口只有兩個靜態(tài)方法職責單一方法職責對應時機GameObjectPool.Get(prefabName, position, rotation)從池中借出一個實例池空則新建發(fā)射子彈、生成敵人時GameObjectPool.Store(go)把用完的實例歸還進池隱藏而非銷毀子彈飛完、敵人死亡時內(nèi)部存儲結(jié)構GameObjectPool.csprivate static Dictionarystring, ListGameObject pool new Dictionarystring, ListGameObject();Key預制體的名字實際是實例化后的(Clone)名見下文ValueListGameObject存放該類型下所有已回收、未激活的實例。2.1 借取Get 的三條分支邏輯Get 方法 的完整實現(xiàn)public static GameObject Get(string prefabName, Vector3 position, Quaternion rotation) { // 拼接制作dic的key名因為instantiate出的gameobject都會自動命名為gameobject(Clone) // 這里是為了通下面store方法里給key的命名匹配 string key prefabName (Clone); GameObject go; // 如果字典里有這個key 并且key對應的數(shù)組不為空有該種類子彈且該種類子彈中有已經(jīng)創(chuàng)建過的未激活的子彈 if (pool.ContainsKey(key) pool[key].Count 0) { // 從gameobjectname這個key位置取出數(shù)組 ListGameObject list pool[key]; // 取出一號位的子彈 go list[0]; // 從列表中去除這個子彈拿出來用 list.RemoveAt(0); // 初始化狀態(tài) go.SetActive(true); go.transform.position position; go.transform.rotation rotation; } // 如果對象池中沒有對象 else { go Instantiate(Resources.Load(prefabName), position, rotation) as GameObject; } return go; }三個關鍵細節(jié)值得注意Key 拼接規(guī)則key prefabName (Clone)。因為 UnityInstantiate出來的對象會被自動命名為xxx(Clone)Store時直接取go.name作為 Key二者天然對齊。這是整套池能正確分類的前提復用時的狀態(tài)重置從List[0]取出后要RemoveAt(0)隨后重新設置SetActive(true)、position、rotation保證復用對象狀態(tài)與新建對象一致池空的兜底路徑直接Instantiate(Resources.Load(prefabName), position, rotation)。注意這里用Resources.Load所以預制體必須放在Resources目錄下本示例位于 Assets/Resources/bullet.prefab。2.2 歸還Store 的隱藏而非銷毀策略Store 方法public static GameObject Store(GameObject go) { // 獲取gameobject的名字會是一個在上面get方法里創(chuàng)建的預設體的gameobject名字會是gameobject(Clone) string key go.name; if (pool.ContainsKey(key)) { // 就在這個key所對應的數(shù)組中加入這個g // 這個g就是已經(jīng)用完的子彈放到這個數(shù)組里的gameobject都是不銷毀只是取消激活等待再次利用的gameobject pool[key].Add(go); } else { // 建立一個這個key的arraylist 并把g加進去 pool[key] new ListGameObject() { go }; } go.SetActive(false); return go; }歸還的核心動作是SetActive(false)而非Destroy。隱藏后對象仍駐留內(nèi)存等待下一次Get時被重新激活。Dictionary的ContainsKey分支保證了每種類型首次歸還時自動創(chuàng)建對應的List無需預初始化。三、演示場景PoolTest 發(fā)射 AutoDestroy 自動回收3.1 觸發(fā)端按空格借出子彈并施加沖量PoolTest.csvoid Update () { if (Input.GetKeyDown(KeyCode.Space)) { GameObject bullet GameObjectPool.Get(bullet, trans.position, Quaternion.identity); bullet.GetComponentRigidbody().AddForce(trans.forward*30, ForceMode.Impulse); } }trans是一個Transform字段指向槍口/發(fā)射點在 Inspector 中拖拽賦值Get(bullet, ...)傳入的bullet正是Resources下預制體的名字不含擴展名借出的子彈立即被AddForce(trans.forward * 30, ForceMode.Impulse)以沖量方式賦予前向速度——這也解釋了為什么子彈預制體需要掛Rigidbody。3.2 回收端3 秒后自動歸還AutoDestroy.cs 掛在子彈預制體上實現(xiàn)用完自動回池void Update () { StartCoroutine(DestroySelf()); } private IEnumerator DestroySelf() { yield return new WaitForSeconds(3f); GameObjectPool.Store(this.gameObject); }邏輯一目了然子彈飛出 3 秒后調(diào)用GameObjectPool.Store(this.gameObject)將自己歸還。配合圖 2 的 Hierarchy 截圖可以看到Gun節(jié)點下累積了 8 個bullet(Clone)正是這些歸還后處于未激活緩存狀態(tài)的實例。小提醒StartCoroutine放在Update里會每幀重復啟動協(xié)程生產(chǎn)代碼通常應改為在實例激活時啟動一次本示例旨在演示回收思路讀者接入自己項目時按需調(diào)整即可。四、對象池設計要點與進階方向4.1 本方案的設計取舍可據(jù)此擴展從 GameObjectPool.cs 源碼結(jié)構可以歸納出該方案的特征按類型分類緩存不同預制體通過 Key 自然隔離互不干擾靜態(tài)訪問pool與Get/Store均為static全局任意腳本可直接調(diào)用Awake中的_instance僅為演示預留的單例賦值實際邏輯并未依賴它無容量上限、無預熱首次借取時新建后續(xù)回收復用實現(xiàn)極簡線程模型依賴 Unity 主線程的Update/協(xié)程調(diào)用未做線程安全處理——這也是 Unity 對象池的常見形態(tài)與 MemoryPool_ObjectPool/Object Pool 中Unity 只有主線程可調(diào)用 API池無需支持多線程的結(jié)論一致。4.2 與泛型對象池的對照倉庫的 Object Pool/README.md 提供了一份 C# 泛型對象池譯文展示了面向普通 C# 類的池化思路StackTNew()/Store()、支持重置閉包ResetAction、按需初始化OnetimeInitAction、IResetable自重置、幀末集體重置ResetAll等進階設計。兩者互補泛型池管類實例本示例管場景 GameObject。前者適合ListVector3、自定義數(shù)據(jù)結(jié)構等內(nèi)存敏感場景后者直接接管 GameObject 的生命周期與激活狀態(tài)更適合射擊、刷怪這類場景級高頻復用。4.3 落地到真實項目時建議補充的能力預熱與上限場景加載時為高頻類型預先GetStore一批實例并設定單類型池容量上限避免峰值時大量新建重置鉤子復用對象若有復雜狀態(tài)血條、Buff、動畫建議在Get時統(tǒng)一重置保證借出即干凈場景切換清理Store進池的對象若在切換場景時被自動銷毀需同步清空對應 Key 的緩存避免殘留引用。五、總結(jié)Unity3DTraining 的Object Pool2示例用約 70 行代碼完成了對象池的完整閉環(huán)Get借出池空則Resources.LoadInstantiate兜底→Store歸還SetActive(false)隱藏緩存→ 以預制體名(Clone)作為字典 Key 分類管理。它證明了對象池的核心并不復雜關鍵在于復用的對象必須重置到可用狀態(tài)以及緩存要按類型隔離。結(jié)合圖 1、圖 2 的運行效果你可以快速驗證反復按空格發(fā)射子彈Hierarchy 中的bullet(Clone)數(shù)量不會無限增長因為飛出的子彈 3 秒后都會回到池中等待再次借出。若需要更理論化的對象池設計討論重置策略、集體重置、容量收縮等可繼續(xù)閱讀 MemoryPool_ObjectPool/Object Pool/README.md并把它與本篇的 GameObject 版實現(xiàn)互為參照搭建出適合自己項目的完整內(nèi)存優(yōu)化方案。贊分享示例工程【免費下載鏈接】Unity3DTraining【Unity雜貨鋪】unity大雜燴~項目地址https://gitcode.com/gh_mirrors/un/Unity3DTraining點擊查看免費下載相關推薦BMAD-METHOD 官方模塊體系解析bmb / cis / gds / tea 四大模塊、安裝器分發(fā)機制與版本通道BMAD METHOD 官方模塊體系解析bmb / cis / gds / tea 四大模塊、安裝器分發(fā)機制與版本通道 BMAD METHOD 的核心功能由內(nèi)AI 技能人工智能開發(fā)工具Gonum內(nèi)存池實現(xiàn)sync.Pool在矩陣對象復用中的應用Gonum內(nèi)存池實現(xiàn)sync.Pool在矩陣對象復用中的應用 在數(shù)值計算領域頻繁創(chuàng)建和銷毀矩陣對象會導致嚴重的性能損耗和內(nèi)存碎片化。Gonum作為Go語言的科學計算在 Garnet 中開發(fā)自定義對象Custom Object從繼承 CustomObjectBase 到注冊自定義命令的完整實戰(zhàn)在 Garnet 中開發(fā)自定義對象Custom Object從繼承 CustomObjectBase 到注冊自定義命令的完整實戰(zhàn) 導讀 Garnet 是緩存KV存儲后端上一篇mikro-orm 虛擬實體Virtual Entities實戰(zhàn)用動態(tài) SQL 與 MongoDB 聚合映射只讀實體下一篇探索TelegraphiOS、tvOS和macOS的安全Web服務器創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考