Unity AssetBundle打包與資源更新


Unity的AssetBundle打包是一件讓人頭疼的事情,當我接手這項工作時,我以為最多只用兩個周就可以把整個打包和資源熱更新的流程搞定,結果還是花了一個月,期間踩坑無數,總結出來希望能夠節約別人的時間。

(一)你的游戲項目是什么類型的?

在開始寫打包的Editor腳本之前,你最好先詳細考察一下你們的游戲項目是什么類型?是端游,手游還是頁游?因為這三者涉及到bundle包的資源管理策略截然不同,如果你們是跨平台發布,那我建議你最好用宏來切換管理策略。

 

(二)采用什么樣的bundle包加載策略?

AssetBundle加載有以下幾種方式:

     (1)CreateFromMemory/CreateFromMemoryImmediate

     這種方式直接從內存構建,可同步可異步,可以先通過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包卸載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM