工具
Unity 中的資源來源有三個途徑:一個是Unity自動打包資源,一個是Resources,一個是AssetBundle。
? ? Unity自動打包資源是指在Unity場景中直接使用到的資源會隨著場景被自動打包到游戲中,這些資源會在場景加載的時候由unity自動加載。這些資源只要放置在Unity工程目錄的Assets文件夾下即可,程序不需要關(guān)心他們的打包和加載,這也意味著這些資源都是靜態(tài)加載的。但在實際的游戲開發(fā)中我們一般都是會動態(tài)創(chuàng)建GameObject,資源是動態(tài)加載的,因此這種資源其實不多。
? ? Resources資源是指在Unity工程的Assets目錄下面可以建一個Resources文件夾,在這個文件夾下面放置的所有資源,不論是否被場景用到,都會被打包到游戲中,并且可以通過Resources.Load方法動態(tài)加載。這是平時開發(fā)是常用的資源加載方式,但是缺點是資源都直接打包到游戲包中了,沒法做增量更新。
? ? AssetBundle資源是指我們可以通過編輯器腳本來將資源打包成多個獨立的AssetBundle。這些AssetBundle和游戲包是分離的,可以通過WWW類來加載。AssetBundle的使用很靈活:可以用來做分包發(fā)布,例如大多數(shù)頁游資源是隨著游戲的過程增量下載的,或者有些手游資源過大,渠道要求發(fā)布的包限制在100M以內(nèi),那只能把一開始玩不到的內(nèi)容做成增量包,等玩家玩到的時候通過網(wǎng)絡(luò)下載。AssetBundle 也可以用來做我們下面討論的自動增量更新。
Unity5相比之前的版本,AssetsBundle的打包過程有所簡化。之前打包需要通過代碼來設(shè)置需要打入包的資源并自己建立包的依賴關(guān)系,Unity5可以通過每個資源Inspector底部的AssetBundle下拉來指定該資源要打入哪個包,不指定就是不打包。打包過程只需要BuildPipeline.BuildAssetBundles一句話就行了,Unity5會根據(jù)依賴關(guān)系自動生成所有的包。每個包還會生成一個manifest文件,這個文件描述了包大小、crc驗證、包之間的依賴關(guān)系等等,通過這個manifest打包工具在下次打包的時候可以判斷哪些包中的資源有改變,只打包資源改變的包,加快了打包速度。manifest只是打包工具自己用的,發(fā)布包的時候并不需要。
關(guān)于自動生成依賴關(guān)系這個有必要提下,Unity確實會自動給你建立依賴關(guān)系,前提是你依賴的資源必須已經(jīng)在Inspector中設(shè)置了BundleName。如果沒有,Unity會把這個公用的資源重復(fù)打到多個用到的包中,因為這個公用資源不在一個獨立的包中,Unity也不會智能地給你發(fā)現(xiàn)它是公用的,然后生成一個公用包。更深的坑在于,如果你公用的是一個FBX模型,你只給這個模型設(shè)置BundleName還不行,它用到的貼圖,材質(zhì)都要設(shè),否則模型是公用了,貼圖沒有公用,結(jié)果貼圖還是被打包到多個包中了。所以設(shè)置BundleName這個工作最好還是由編輯器腳本來完成。
方案
Unity提供的就這些了,下面就自己發(fā)揮:如何做一個方便的資源管理方案,既可以開發(fā)時方便,又可以方便發(fā)布更新包呢?開發(fā)過程全用AssetsBundle是不合適的,因為開發(fā)中資源經(jīng)常添加和更新,每次添加或者更新都生成一下AssetsBundle才能運行是很麻煩的。而且我們要做的是自動更新而不是分包下載,這也就是說在發(fā)布游戲的時候這些資源應(yīng)該都是在游戲包中的,所以他們也不該從AssetsBundle加載。
分析完需求,方案也就出來了:資源還是放在Resources下面,但是這些資源同時也會打包到AssetBundle中。代碼中所有加載資源的地方都通過自己的ResourceManager來加載,由ResourceMananger來決定是調(diào)用Resources.Load來加載資源還是從AssetsBundle加載。在開發(fā)環(huán)境下(Editor)這些資源顯然是直接從Resources加載的,發(fā)布的完整安裝包資源也是從Resources加載,只有當(dāng)有一個增量版本時,游戲主程序才會去服務(wù)器把增量的AssetBundle下載下來,然后從AssetBundle加載資源。
實現(xiàn)
實現(xiàn)中我們首先要考慮的是AssetBundle的粒度,即每個AssetBundle包含多少資源。增量包的最小粒度就是AsssetBundle, 如果單個AssetBundle過大,只要這個AssetBundle中有一個資源改變了就需要重新下載整個AssetBundle,浪費流量和玩家的等待時間;如果單個AssetBundle過小,極端情況是每個資源一個AssetBundle,雖然實現(xiàn)了更新最小化,但是帶來了額外開銷:AssetBundle本身也是有大小的,而且查找加載AssetBundle也是需要時間的。大家都往U盤里面拷過東西,拷一個1G的文件比拷1千個1M的文件要快很多。比較合理的做法是根據(jù)邏輯來,例如每個角色可以有獨立的AssetBundle,公用的一些UI資源可以打到一個AssetBundle里面,每個場景獨立的UI資源可以打成獨立的AssetBundle。這樣做資源預(yù)加載的時候也方便,每個場景需要用到幾個Bundle就加載幾個Bundle,無關(guān)的資源不會被加載。
下面要考慮的是如何來確定一個資源是從Resources加載還是AssetBundle加載。為此我們需要一個配置文件resourcesinfo。這個文件隨打包過程自動生成。里面包含了資源版本號version,所有包的名字,每個包的HashCode以及每個包里面包含的資源的名字。HashCode直接可以從Unity生成的manifest中得到(AssetBundleManifest.GetAssetBundleHash),用來檢查包的內(nèi)容是否發(fā)生變化。這個resourceinfo每次打包AssetBundle時都會生成一個,發(fā)布增量時將它和新的Bundle一起全部復(fù)制到服務(wù)器上。同時在Resources文件夾下也存一份,隨完整安裝包發(fā)布,這就保證了新安裝游戲的玩家手機上也有一份完整的資源配置文件,記錄了這個完整包包含的資源。
當(dāng)游戲啟動時,首先請求服務(wù)器檢查版本號,前端用的版本號就是Resources下面的這個resourcesinfo中的version。服務(wù)器比對這個版本號來告訴前端是否需要更新。如果需要更新,前端就去獲取服務(wù)器端的新resourcesinfo,然后比對里面每個bundle的HashCode,把HashCode不同的bundle記錄下來,然后通過WWW類來下載這些發(fā)生改變的bundle,當(dāng)然如果服務(wù)器版的resourcesinfo中包含了本地resourceinfo中所沒有的Bundle,這些Bundle就是新增的,也需要下載下來。所有下載完成后,前端將這個新的resourceinfo保存到本地存儲中,后面前端的所有操作都將以這個resourceinfo為準(zhǔn)而不再是Resources下面的resourceinfo了。Resources下的resourceinfo可以退出歷史舞臺了,除非一種情況:本地存儲的resourceinfo被認(rèn)為刪除了。手機端玩家清理應(yīng)用的數(shù)據(jù)就會造成下載的bundle以及resourceinfo被刪除。沒關(guān)系,這時候前端由于找不到外部的resourceinfo了,還會使用Resources下面的resourceinfo和服務(wù)器比對,把新的bundle重新下載下來。
現(xiàn)在從哪里加載資源就很明確了:ResourceMananger先讀取resourcesinfo,知道了游戲中所有的Bundle和每個Bundle包含的資源,然后去外部存儲查找這些Bundle是否存在,如果存在,就記錄下這個Bundle的資源應(yīng)該從外部的AssetBundle加載,如果不存在,就從內(nèi)部的Resources加載。在開發(fā)過程(Editor)中,由于不存在外部存儲的bundle,資源自然都是從Resources加載的,達(dá)到了我們開發(fā)方便的目的。這個過程隱含了一點:不是所有的資源都需要有BundleName而被打包到AssetBundle中,游戲內(nèi)不需要后續(xù)更新的資源就不要設(shè)置BundleName,它們不會被打包更新,這樣的資源ResourceManager在resourceinfo中是找不到的,直接去Resources文件夾下面讀取就行了。
加載AssetBundle,我們直接使用WWW類而不用, 因為我們的資源在游戲開始的時候已經(jīng)下載到外部存儲了,不要再Download也不要再Cache。注意WWW類加載是異步的,在游戲中我們需要同步加載資源的地方就要注意把資源預(yù)加載好存在ResourceManager中,不然等用的時候加載肯定要寫異步代碼了。大部分時候我們應(yīng)該在一個場景初始化時就預(yù)加載好所有資源,用的時候直接從ResourceManager的緩存取就可以了。
資源加載卸載
最后簡單說下資源的加載卸載,這個網(wǎng)上也有很多文章介紹。
從我理解來看Resources是一個缺省自動打包的特殊AssetBundle。無論從WWW還是AssetBundle.CreateFromFile創(chuàng)建AssetBundle其實是創(chuàng)建了一個文件內(nèi)存鏡像。這時候是沒有Asset的。AssetBundle.LoadAsset 和Resource.Load才真正創(chuàng)建出了Asset,而Instaniate復(fù)制了這個Asset。注意這個復(fù)制有兩種,學(xué)C++的都知道淺拷貝和深拷貝,這里的復(fù)制有的是正真的復(fù)制,有的是引用。為什么要這樣呢?因為有些游戲資源是只讀的,像貼圖Texture,這么大而且只讀,當(dāng)然不需要再去完全復(fù)制一份。但像GameObject這種資源它的屬性是可以通過腳本改變的,必須要復(fù)制一份。所以一個資源從AssetBundle到場景中被實例化,其實有3塊內(nèi)存被創(chuàng)建,這3快內(nèi)存的釋放是有不同方法的。
文件內(nèi)存鏡像是通過AssetBundle.Unload(false)來釋放的。
Instaniate出來的Object內(nèi)存通過Object.Destory來釋放。
AssetBundle.Unload(true)不單會釋放文件內(nèi)存鏡像,還會釋放AssetBundle.Load創(chuàng)建的Assets。這個方法是不安全的,除非你能保證這些Assets沒有Object在引用,否則就出問題了。
Resources.UnloadAsset和Resources.UnloadUnusedAssets可以用來釋放Asset。
下面這個圖很直觀:
這是老Unity的圖,Unity5已經(jīng)把AssetBundle.Load 改成了AssetBundle.LoadAsset。這個改動讓我們更明確了Load出來的是Asset這塊內(nèi)存區(qū)域。什么時候把Resource.Load也改了吧。
注意事項?
? ? Resources.Load方法傳入的資源路徑需是從Resources文件夾下一級開始的相對路徑且不能包含擴展名;而AssetBundle.LoadAsset方法傳入的資源名需是從Assets文件開始的全路徑且要包含擴展名。路徑不區(qū)分大小寫,建議全用小寫,因為AssetBundle.GetAllAssetNames方法返回的資源名都是小寫的。
? ? Unity5打包AssetBundle時會自動處理依賴關(guān)系,但是在運行時加載的時候卻不會,程序需要自己處理,先加載依賴包。
? ? AssetBundle.CreateFromFile不能加載壓縮過的AssetBundle,所以我們只能用WWW來異步加載AssetBundle。
? ? 目前我用的Unity5.0.2f1的Resources.Load方法在手機端比原來慢了很多,如果以前可以不緩存每次用的時候都調(diào)用Resource.Load現(xiàn)在就不行了。頻繁的調(diào)用會導(dǎo)致明顯的性能開銷,不知道是不是Bug。
評論
查看更多