Unity的AssetBundle打包是一件讓人頭疼的事情,當我接手這項工做時,我覺得最多隻用兩個周就能夠把整個打包和資源熱更新的流程搞定,結果仍是花了一個月,期間踩坑無數,總結出來但願可以節約別人的時間。html
(一)你的遊戲項目是什麼類型的?算法
在開始寫打包的Editor腳本以前,你最好先詳細考察一下大家的遊戲項目是什麼類型?是端遊,手遊仍是頁遊?由於這三者涉及到bundle包的資源管理策略大相徑庭,若是大家是跨平臺發佈,那我建議你最好用宏來切換管理策略。緩存
(二)採用什麼樣的bundle包加載策略?多線程
AssetBundle加載有如下幾種方式:異步
(1)CreateFromMemory/CreateFromMemoryImmediateide
這種方式直接從內存構建,可同步可異步,能夠先經過C#的IO函數從磁盤加載進內存,再用這個API構建AssetBundle內存鏡像,佔用內存大。不只有構建出來的AssetBundle內存鏡像,還有用來構建的bundle包的那部分託管堆內存byte[],要等待垃圾回收。函數
同步構建速度比較快,異步構建的速度很是慢,可是多個bundle包一塊兒異步構建在Unity底層有優化,測試要快過一個個的構建。工具
(2)WWW加載測試
這種方式爲異步加載到內存,多個www對象有多線程優化。相比CreateFromMemory少掉了託管堆那部份內存。優化
(3)WWW.LoadFromCacheOrDownload
這種方式佔用內存小,是由於Unity會在硬盤上開闢一塊空間,用於緩存解壓後(時間主要浪費在解壓這一步)的AssetBundle,而後再從這塊硬盤緩存上構建AssetBundle包,這種方式佔用內存較小,由於構建出來的AssetBundle包主要是對磁盤文件的引用,只有在實例化的時候纔會分配資源佔用的內存。可是磁盤緩存有上限的,超過了上限以後仍然會變成普通的www所有加載到內存。並且你要有個版本號文件管理傳入的version參數,不然有可能加載到老的assetbundle。
(4)CreateFromFile
直接從硬盤構建,也是隻構建引用,因此速度快且AssetBundle包自己佔用內存最小。推薦這種方式,由於同步的代碼比較好寫,尤爲是對於項目後期才引用bundle包機制的,把之前的全部資源加載都改爲異步的邏輯工做量太大。
(三)從構建好的assetbundle裏load資源
AssetBundle.Load/AssetBundle.LoadAssetAsync
在PC上紋理的上傳就發生在這一步。我測試過一個1024*1024的紋理上傳所花費的時間每每10倍於512*512的上傳時間,因此減少紋理大小纔是性價比最高的優化。對於2d mmorpg常用的大圖2048*2048,若是你使用同步load一個多幀動畫,能夠明顯的感受到卡一下。若是使用異步則徹底不掉幀,估計Unity是採用sub-image的方式一次鎖定一小塊區域的紋理上傳顯卡,可是比較慢,且沒有方法調整哪一個參數來加速這個步驟。像大型2d微端這種須要在場景上實時加載不少大圖的效果不能使人滿意,能夠採用切圖的方式來優化。
還有unity對象的構建花費的時間也很長,不少遊戲過關卡時間太長,主要就是prefab的構建和實例化。能夠採用pool manager的方式將實例化出來的對象保存起來,過關卡時只卸載其佔用內存較大的紋理音效等資源,下次須要時再加回來。這樣能夠大大減小過關卡的時間,可是這種方式卻會給assetbundle的管理帶來一些麻煩,我會在後面bundle包卸載那裏提到。
(四)Assetbundle打包
(1)依賴打包
最頭痛的就是這一步了,你要考慮怎樣處理資源間的依賴,以免產生資源冗餘。Unity提供了PushDependencies和PopDependencies來處理依賴包的共享資源問題,例如你有以下依賴關係
(A,B)->C->D
則打包腳本爲
push
build D
push
build C
push
build A
build B
pop
pop
pop
這是一個棧結構,後入棧的資源若是有包含先入棧的資源,則不會重複打包進去,而是依賴於這個包。加載時你要確保先加載被依賴的包,再加載最後的包纔不會出錯。可是被依賴的包是能夠不分前後亂序加載的,若是你使用www加載,能夠考慮幾個www一塊兒加。
還要你要搞清楚pushDependencies/popDependencies打包時設置的依賴關係和加載時的依賴關係其時是兩碼事,這也是一開始困惑個人地方。好比你有以下的依賴結構
A->(B1 B2 B3 B4)->C
D->(B3,B4,B5,B6)->G
則你的打包腳本應該是這樣的
push
build C,build G
push
build B1,B2,B3,B4,B5,B6
push
build A,D
pop
pop
pop
看起來好像A和D都依賴於B1-B6了,其實否則,這樣打包出來A包和D包仍是隻會依賴於包含相同資源的那些包,好比加載D包的時候你也只須要加載B3-B4 只要你打包的參數設置正確,當B1,B2變更時,走這個流程打包出來的D包二進制仍然沒有變化的。
(2) 打包時的參數設置
BuildAssetBundleOptions.DeterministicAssetBundle
設置了這個參數每次打包出來的包才能確保二進制不變,只要被依賴的包不變,打包的流程不變。因此要作資源更新,這個參數不可少,不然在資源不變化的狀況下重複打包出來的MD5都不同,怎麼確保更新功能的正常?
BuildAssetBundleOptions.CollectDependencies
這個參數用來收集全部依賴的包,雖然咱們會手動收集依賴關係用於push/pop dependencies,可是仍然須要加上這個參數,由於你不會把一個包依賴的全部資源都收集完,你只會先push幾個它依賴的資源,而後再用collectDependencies打最後這個包,確保這個包依賴的全部資源都打進去了。
BuildAssetBundleOptions.CompleteAssets
強制包含整個資源
BuildAssetBundleOptions.UncompressedAssetBundle
採用不壓縮的方式打包一個bundle包
咱們打包的時候這四個參數都用了,只有最後一個參數視狀況而定。
(3)收集依賴關係
打包前先使用AssetDatabase.CollectDependencies遍歷全部資源收集他們間的依賴關係,在後面打包的時候按照每一個資源被依賴的深度進行分級,先打包級別較低的,如shader,script這些資源被其餘資源依賴但不會依賴別的資源,級別最低。如prefab依賴前面的全部資源,級別最高,放在最後打包。通常是按照資源的類型(prefab,mesh,animator,texture,script…)進行分級。即便這樣按類型分好級後還是不夠的,由於同一級的資源也有可能產生相互依賴的關係。好比使用NGUI,一個面板prefab依賴於幾個掛UIAtlas的prefab,這種同級的依賴須要用深度優先遍歷對他們進行排序以肯定依賴關係。這個依賴關係使用序列化文件記錄下來,供後面加載包的時候先加載全部被依賴的包使用。每次更新的時候這個依賴關係的序列化文件也要同其餘資源一塊兒更新。
(4)打包時可能遇到的一些問題
若是你使用www.LoadFromCacheOrDownload 請在調試的時候遊戲開始時調用一次ClearCache。即便你的代碼有動態更新LoadCache時傳入的version參數的機制,調試的時候仍是要謹慎,若是BUG致使你傳的version跟上次同樣,相互依賴的包緩存的版本不匹配,就可能引發一些稀奇古怪的問題。
檢查你的打包流程所記錄的依賴結構是否穩定。這裏的穩定是指,在CollectDependencies的時候有沒有處理到的被依賴的資源,有可能在打包同級資源的時候出現相互吃資源的狀況。好比
A->(B c)->D
E->(F c)->G
打包腳本
push
build D,G
push
build B,F
push
build A,E
pop
…
在收集依賴關係的時候,c是咱們忽視的資源,打包時B和F放在同一級打包,A和E在同一級,因爲使用了CollectDependencies,A包會把c給收進去,可是因爲B包在同一級跟A一塊兒打的,就會出現c打進A了就再也不打進B了,但你加載B的時候又沒有加載A,因此B就工做不正常。
排查這個BUG的方式就是先打一兩個角色或面板,備份,再打所有資源。把兩份資源用二進制工具作比較(推薦BeyondCompare,能夠對比目錄),若是有不穩定的結構立馬就能發現。
還有texture的寬高請使用2的倍數,我在測試不標準的圖的時候發現Unity對於這種圖會產生一個fmt-512*512(sprite)的臨時資源,這個資源get他的硬盤地址時get不到,因此也沒有記錄進依賴關係文件。當有兩張圖不規範時,一張圖的bundle包收錄了臨時資源另外一張圖就沒有,加載出來就會不正常。固然通常遊戲項目爲了優化使用的圖都比較規範,不會遇到這個問題。
在IOS真機調試時報Could not produce class with ID..這是由於你勾選了strip code,有些腳本類是被Resource下的資源引用的,打包後將Resource下的資源移除出去了,一些代碼因爲檢測不到引用就被strip掉了,可是從AssetBundle里加載出來又須要根據ID打到對應代碼。解決辦法在這裏http://docs.unity3d.com/Manual/ClassIDReference.html找到ID對應的class,而後在Assets目錄下新建文件link.xml,把不應strip掉的類加進去就好了。個人link.xml文件
<?xml version="1.0" encoding="utf-8"?> <linker> <assembly fullname="System"> <type fullname="System.Net.HttpRequestCreator" preserve="all"/> </assembly> <assembly fullname="UnityEngine"> <type fullname="UnityEngine.CircleCollider2D" preserve="all"/> </assembly> </linker>
有些類好比 AnimatorController(ID 91)屬於Editor包裏的,不能用link.xm加回來,能夠在Resource下建一個空的prefab,在上面掛一個AnimatorController,打包時留下這個prefab就能夠確保這個類不被strip掉了。
(五)更新機制
更新機制比較簡單,收集全部bundle包的md5碼和文件size,作成一個列表。進遊戲時先比對遊戲版本號提示更新遊戲程序,再比對資源版本號,若是發現新版本號就開始下載md5列表,與本地的md5列表作對比,找出須要更新的資源用http下載就好了。
這個過程仍是有許多東西要考慮,好比你的http下載要有下載失敗重試幾回的機制,要有超時的檢測,要知道在下載哪幾個資源時整個更新流程卡住了,記錄日誌。即便遇到更新過程當中出錯,對於已經更新的資源下次進不用再重複更新,因此最好每更新10條資源就寫回一次md5,而不是全更完再寫回。
md5列表的比較,之前有的PC遊戲會在遠端先作好與上一個版本的對比,而後生成一個ver x 到ver x+1 的資源更新文件。在更新的時候若是遊戲的資源版本號是ver x-1 就先下載 ver x的資源更新文件更新,再從ver x更新到ver x+1。可是這一套用在Unity手機資源更新上有風險,假設有些手機的清理軟件提示這個程序的資源佔用過大,一不當心點了致使清掉了部分資源,但你的ver x文件還在,那你更新時被清掉的這部分資源就找不回來了。因此仍是每次在客戶端對比全部md5比較穩妥,在提取本地的md5列表中的一項時同時檢測本地是否存在這個資源文件,不存在的加入更新列表,這樣即便被意外清掉的資源也能夠找回。
(六)壓縮bundle包
由於咱們使用的是CreateFomeFile的同步機制加載包,而CreateFromFile只能用BuildAssetBundleOptions.UncompressedAssetBundle,打包出來後本身壓縮再在更新時解壓。因此採用什麼壓縮算法就是一個值得商榷的問題。
壓縮你要考慮兩個方面:壓縮率與解壓時間。
(待續)
(十)AssetBundle包卸載