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