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