淺談Unity的資源管理系統

一. 外部資源的導入

外部導入的資源主要有圖片、模型(可能包含動畫)、音頻等。每種類型的文件都有很多種類型,如圖片有png、psd、jpeg、tga等,當導入這些文件的時候Unity會將其處理問自己的數據格式YAML,保存在Library下面,同時在Assets下面在該資源文件的位置生成meta文件,記錄其GUID和一些設置信息,如FBX文件的設置、圖片導入後用戶配置的Sprite的信息。

含有子資源的文件除了GUID外,其子資源還會被分配一些FileID,或者叫LocalID,GUID + FileID在整個Assets下面是唯一的。其中每一個GUID和Library下面的文件夾一一對應,將Library下面的文件後綴添加爲.asset,再放到Assets下面,就能理解什麼是"Unity自己的數據格式"(YAML格式,數據都放在該文件的一個Data字段種)。平時我們遇到奇葩的資源問題或者報錯問題經常ReimportAll其原理就是重新生成一遍Library下面的Unity數據格式。

這裏比較複雜的是FBX,導入FBX後Unity會在Assets下面將FBX顯示爲一個不可編輯的Prefab,其圖標是Prefab的圖標加上一個白色的正方形,而且存在SubAssets,即子資源,這些資源如Mesh、AnimationClip、Avatar,顯示在該不可編輯Prefab的下方。

二. 內部資源的創建

內部創建的資源全部是序列化的資源,有Prefab、Material、Shader、場景、AnimatorController、OverrideController。這些文件基本是用來保存狀態信息或者配置信息。

"序列化"簡單來說就是將對象的狀態保存在存儲設備上,類似XML或者遊戲存檔;反序列化就是從存儲設備上讀取並恢復該對象的狀態,這也正是Prefab和場景的原理(格式是YAML)。在Unity裏將文本設置爲ForceText之後,用記事本打開這些序列化文件就可以看到這些對象的關係,如引用關係、Inspector面板上的屬性等等,遵循YAML的語法規則。

深入理解Unity序列化系統之後,就可以輕易的解決諸如如何查找Prefab使用了哪些組件?資源被什麼Prefab引用了?如何實現Prefab嵌套?等。想深入理解這一部分,免不了使用AssetDataBase下面的方法和研究Library文件夾裏面的文件是什麼以及研究一遍AssetBundlde得打包流程、去讀讀官網的YAML ClassID Reference。

三. 資源的打包

打包即是生成AssetBundle,5.X版本以上版本的過程如下(4.X手動再見,王者榮耀都從4.6.9升級到5.X了),首先用戶指定要打包的資源和要把資源放到哪個AssetBundle種,隨後Unity會自己收集用戶指定資源的依賴資源並跟隨這些用戶指定的資源,接着生成每個AssetBundle的資源對應關係。隨後進入打包階段,Unity會取Library下面的自己數據格式的資源將其加入AssetBundle,並做一些數據剔除操作,如去掉編輯器下使用的數據信息。

我們經常會碰到編輯器下面和打包出來的效果不一致,這時候一般會Reimport一下,其實原因就是Unity在生成AssetBundle的時候取得是Library下面已經生成好的數據。

打包涉及到得技術難點也非常多,如何去除冗餘得資源?如何規劃AssetBundle得粒度?如何加快打包速度?如何針對AssetBundle做加密防止被UnityStudio輕易地Dump?這裏得每個問題挑出來都能再寫一篇論文了,本文不再展開敘述。

四. 資源的加載

一個良好的資源加載系統一般有三層Cache,第一層是AssetBundle的Cache,第二層則是Asset(或者稱Resource)層,最後一層是GameObject/Material等序列化資源的實例,我們經常聽到的資源對象池(區別於Class的對象池)其實就是第三層。

要加載一個資源,第一步是加載該資源所在的AssetBundle,而要加載整個AssetBundle,又要先加載該AssetBundle依賴的AssetBundle,先加載其實這裏也不是必須的,只要在LoadAsset之前把依賴的AssetBundle都加載了即可(這裏的加載其實就是打開的意思)。第二部是從AssetBundle種加載資源,單個資源直接LoadAsset即可,含有大量子資源的則需要緩存起來免得每次加載都要LoadAllSubAssets,一次性加載緩存起來的速度更快,如Sprite,在合適的釋放即可。最後一步,如果你的加載的資源是序列化類型(GameObject、Material等),那麼你加載出來的資源是隻讀的,需要Instantiate一個才能使用(最好配合對象池免去頻繁的創建和銷燬帶來的開銷)。如果是Texture、Audio這些跳過第三步直接使用即可。

而上面所說的加載又可以分爲同步加載和異步加載,可以用回調、輪詢、Coroutine的方式來完成異步加載流程,這裏不再展開敘述。沒有完美的加載方案,適合項目的纔是最好的,比如MOBA這種即時性比較強的比如釋放技能立刻就要看見特效這種沒法使用異步加載,只能使用同步 + Preload的方式。而MMO遊戲由於周圍角色的技能釋放玩家並不是很關心,可以使用異步,邏輯層在加載過程種可以先行更改數據,在資源加載完畢後校正位置即可。

Demo或者開發階段很多人習慣把資源放在Resources下面,直接使用Resources.Load(Async)接口來加載,好處是無需打AssetBundle,無冗餘資源,缺點Unity官方博客說的挺全面了,啓動慢(建立ResourceManager表),無法直接更新資源,默認是LZMA格式,加載速度慢(5.x某個版本之後可以對apk整體使用LZ4格式了),難加密等等。

更多的人則選擇了AssetBundle,雖然AssetBundle粒度細的情況下打包速度極慢、雖然資源極易產生冗餘、雖然依賴加載很容易出錯、雖然Bundle卸載很容易造成空指針....除了小心翼翼地克服這幾個問題,還有一些比較糾結的問題,比如要不要第一次啓動遊戲的時候把AssetBundle拷貝到PersistentDataPath下面?不同的AssetBundle粒度下是Unload(true)還是Unload(false)?解決或者將就了以上問題之後,AssetBundle的好處是顯而易見的,熱更新修復Bug(特別是採用Lua做腳本的遊戲)、無需換安裝包更新資源、更快的加載速度、直接offset讀取與加密等等。

五. 資源的卸載

很棘手的一個問題,試想兩個極端,如果AssetBundle粒度非常細,每一個AssetBundle都只包含一個資源,那麼在LoadAsset之後直接Unload(false)即可。而另一個極端則是AssetBundle粒度非常粗,所有的資源都打入一個AssetBundle中,那麼該AssetBundle就不能Unload,當然這個巨大的AssetBundle也無需在遊戲運行時多次被加載。對比關係如下圖表格所示:

AssetBundle粒度粒度極細(每個資源一個AssetBundle)粒度極粗(一個AssetBundle包含所有資源)
I/O開銷多次加載/卸載(取決於Asset緩存時間),發熱嚴重1次Load,常駐,之後的IO開銷相對小
內存開銷小,加載完畢立刻Unload大,AssetBundle常駐
補丁大小小,Patch只包含變化的資源大,每次都需要AssetBundle全量更新
加密重要數據加密的代價小基本上不能加密(Offest或者加密TextAsset)
.........
我們的項目一般是介於兩者之間,因此設計粒度要適合項目。Unload一般有兩種方法,一是自己做引用計數,要求上層開發嚴格按照提供的接口Get和Release,保證正確的引用關係,隨後定期掃描引用計數爲0的Asset,卸載Asset(序列化資源無法Unload),同理卸載AssetBundle。另一種方法是粗粒度的管理,採用WeakReference,適合粒度小的AssetBundle,使用Resources.UnloadUnusedAssets定期清理。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章