KSFramework是一個Unity 5 Asset Bundle開發(fā)框架和工具集,專注于運(yùn)行時熱重載忌卤,使用了SLua作為腳本引擎。
相信每一個使用Unity引擎的老司機(jī)都會被它的一個功能所困擾過——AssetBundle楞泼。
變味的易用性
Unity是一款主打易用性的游戲引擎驰徊。它的開發(fā)團(tuán)隊(duì)笤闯,希望開發(fā)者可以低門檻、快速棍厂、容易地使用Unity開發(fā)游戲颗味,所以Unity在最初以類似JavaScript、類似Python的腳本語言作為主要開發(fā)語言牺弹。
在移動互聯(lián)網(wǎng)起步階段浦马,小游戲是手機(jī)游戲應(yīng)用的主旋律。無數(shù)的創(chuàng)業(yè)者需要一款跨平臺张漂、簡單易用的游戲引擎晶默,來快速開發(fā)3D游戲產(chǎn)品,這也是Unity近年大火的重要原因航攒。
隨著這兩年手機(jī)游戲過度到端游化磺陡、大型化,可以看到的是屎债,Unity已經(jīng)成為手游開發(fā)的首選方案了仅政,其自身的功能和各種圍繞它的技術(shù)生態(tài)日催完善,C#語言也當(dāng)仁不讓的成為首選開發(fā)語言盆驹。
但是圆丹,她的骨子里依舊還是那個標(biāo)榜易用性的游戲引擎,由于她“易用”這個特點(diǎn)躯喇,使用Unity開發(fā)大型游戲辫封,開發(fā)團(tuán)隊(duì)如果在開發(fā)之初按照Unity標(biāo)準(zhǔn)的易用方式來制作游戲,到了后期就免不了出現(xiàn)各種各樣的坑,需要花費(fèi)大量的時間去重新重構(gòu)和代碼維護(hù)。最典型的情況就是:
一個開頭快速功能迭代開發(fā)的游戲少欺,直到中后期才萌生熱更新的需求,而在Unity里做資源熱更(AssetBundle)和代碼熱更(Lua)是一個不小的工作欣福,需要耗費(fèi)相當(dāng)多的精力。
怎么辦焦履?加班或延期唄拓劝。
回想使用Unity這5年的搬磚經(jīng)歷,Unity里最令我沮喪的功能是它的AssetBundle——不論是打包還是加載嘉裤,都要花費(fèi)開發(fā)人員大量的時間成本去研究和應(yīng)用郑临。
起初,你以為它可以像Resource.Load那樣輕松加載資源屑宠?不是的厢洞,它還要手工碼代碼打包;你以為打包完了就能直接加載調(diào)用了?不是的躺翻,它還要在加載時注意處理依賴關(guān)系丧叽。
從Unity的資源方式說起
如前面所說,Unity是一款主打易用性的游戲引擎公你。它的資源打包方式有兩種蠢正。
一種是使用Resource模式,這種模式更像是端游時代的資源打包方式省店,把所有的游戲資源打包成一整塊的文件,然后通過索引文件去記錄索引笨触,各個具體資源文件散落不同大文件里面的不同索引位置懦傍。比如說,暴雪公司的魔獸世界芦劣、魔獸爭霸的mpq文件就是這樣的一個思路粗俱。
這種方式完全體現(xiàn)了Unity的易用性,比如一個圖片虚吟,不管它是PNG寸认、TGA還是PSD,只要丟到Unity里串慰,都會被統(tǒng)一轉(zhuǎn)化成Unity的Texture格式偏塞。簡單、傻瓜邦鲫,非常適合小游戲的開發(fā)灸叼。但是它也有缺點(diǎn),就是每一次發(fā)布最終編譯包的時候庆捺,都會重新對資源進(jìn)行一次打包古今,速度非常的慢,這也反映出它的致命問題滔以,由于資源全部堆砌在一塊了捉腥,要替換其中的資源變得困難——難以進(jìn)行資源熱更新。
另一個種模式你画,AssetBundle模式抵碟。相比較之下,這個模式看上去就像后來迭代版本的時候加出來的一個功能——基于原有Resource模式的不足撬即,提供一個對資源方式更自由控制的方式立磁。
在Resource模式中,開發(fā)者是幾乎完全不用操心他們的資源管理的技術(shù)細(xì)節(jié)剥槐。直接使用編輯器進(jìn)行資源編輯唱歧,用完以后開發(fā)完以后直接打包最終程序就可以了。而Asset Bundle模式則需要自己進(jìn)行資源的打包加載管理。
在Unity 5.x之前的版本颅崩,3.x和4.x几于,AssetBundle是一個非常難用的功能。你不但要操心資源的管理規(guī)范沿后,還要寫大量的代碼控制的它們的打包沿彭,更要命的是,打包不但速度慢尖滚,還有數(shù)之不盡的坑喉刘。相信不少開發(fā)團(tuán)隊(duì),都在AssetBundle上花費(fèi)過不少精力和時間漆弄。
我經(jīng)歷過了4個不同的中大型游戲規(guī)模的Asset Bundle打包睦裳,躺過其中相當(dāng)多的坑,逐漸的開始掌握它的脾性撼唾×兀回頭仔細(xì)想一下,其實(shí)很多坑完全是沒有必要的倒谷,但是前提是在設(shè)計(jì)之初給予相對的重視蛛蒙,提煉統(tǒng)一的方案,就不必說導(dǎo)致后期失控的狀況渤愁。
在Unity 5.x里牵祟,官方推出了一個全新的打包方案,對于程序員而言猴伶,可以僅用一行代碼打包所有的AssetBundle课舍。盡管它里面還是有一些坑,可是卻大大減輕了開發(fā)團(tuán)隊(duì)的工作他挎。打包方式變簡單了筝尾,大家可以集中精力研究怎么更好的去把這些Asset Bundle加載起來了。
更好的方式去加載Asset Bundle
Asset Bundle加載資源的API非常的簡單办桨,核心其實(shí)只是兩個函數(shù)筹淫,一個同步和一個異步。
// 同步加載呢撞,直接返回AssetBundle
AssetBundle.LoadFromFile(path);
// 異步加載损姜,返回AssetBundleCreateRequest
AssetBundle.LoadFromFileAsync(path);
相信每一個游戲開發(fā)團(tuán)隊(duì)都在官方的這些AssetBundle加載API基礎(chǔ)上,封裝出自己的加載管理類殊霞,這幾乎是必須的摧阅。封裝的方式千奇百怪,怎么樣去封裝绷蹲,去封裝的比較好棒卷?
接下來我所講述的是一種模仿面向?qū)ο蟮腁ssetBundle加載管理類封裝方式顾孽,實(shí)現(xiàn)方便加載的同時,又可以更容易的進(jìn)行實(shí)時調(diào)試比规。
這種基于面向?qū)ο蟮姆绞絹碓O(shè)計(jì)的AssetBundle加載管理器若厚,我們給他一個名字叫ResourceModule,方便下文講述蜒什。它的主要目的是為了讓開發(fā)者在方便的加載資源的同時测秸,提供方便的實(shí)時調(diào)試功能,并且你會在過程中了解到資源文件的熱更新策略灾常。
下邊將會分成五個部分來介紹ResourceModule:加載霎冯、調(diào)試、異步钞瀑、垃圾回收肃晚、路由。
加載器——基于追蹤對象
// 同步加載仔戈,return 直接返回AssetBundle對象
AssetBundle.LoadFromFile(path);
// 異步加載,return 返回AssetBundleCreateRequest對象
AssetBundle.LoadFromFileAsync(path);
在Unity的標(biāo)準(zhǔn)Asset Bundle加載接口中拧廊,同步加載返回了行為結(jié)果监徘,異步加載則返回了行為追蹤對象。具體來說吧碾,同步加載凰盔,直接就返回了資源的AssetBundle;異步加載倦春,則返回了異步加載的追蹤對象AssetBundleCreateRequest户敬。追蹤對象,用于之后進(jìn)行資源異步加載情況跟蹤睁本,被協(xié)程輪詢判斷是否已經(jīng)異步加載完畢尿庐,若完成了可從追蹤對象里獲取加載資源。
由于同步和異步的加載API不一樣呢堰,在項(xiàng)目實(shí)際應(yīng)用時抄瑟,往往沒有統(tǒng)一的加載接口。要避免這種情況枉疼,可以統(tǒng)一加載行為皮假,都返回追蹤對象。這也是ResourceModule加載方式的核心骂维。
函數(shù)式接口
在ResourceModule里惹资,提供了兩個最簡化的加載Asset Bundle API,看上去就跟Resources.Load一樣簡單航闺。
// 同步方式
var loader = ResourceModule.LoadBundle(path);
var ab = loader.Asset; // get UnityEngine.Object sync
// 異步方式
var loaderAsync = ResourceModule.LoadBundleAsync(path);
var abAsync = loaderAsync.Asset; // null asset, async loading.
while(!loaderAsync.IsFinished)
{
yield return null; // 協(xié)程等待
}
abAsync = loaderAsync.Asset; // get UnityEngine.Object async
這里的函數(shù)式接口跟官方的是不一樣的地方:ResourceModule的函數(shù)式接口的返回值褪测,將始終還是一個AbstractResourceLoader對象,也就是“追蹤對象”——對于異步加載,使用追蹤對象汰扭,可以判斷異步加載的進(jìn)度并獲取加載后的資源稠肘;對于同步加載,使用追蹤對象立即獲取資源萝毛;它也提供錯誤處理信息项阴;并且后續(xù)所講及的實(shí)時調(diào)試,也是基于這個追蹤對象笆包。
兩個接口的返回值類型是一樣的环揽。
abstract class AbstractResourceLoader {
bool IsComplete {get; set;}
bool IsError {get; set;}
object ResultObject;
// .......
}
看上去,Asset Bundle的加載接口很簡單庵佣。但本質(zhì)來說歉胶,這只是接下來Loader對象式加載的一個使用簡化。
Loader對象
ResourceModule.LoadBundle的本質(zhì)巴粪,是使用AssetFileLoader進(jìn)行加載行為通今,并把自己作為追蹤對象返回。AssetFileLoader本身是對UnityEngine.Object進(jìn)行處理肛根,它自身可以通過配置辫塌,修改成使用Resources.Load模式或AssetBundle模式。
當(dāng)AssetFileLoader配置成AssetBundle加載模式派哲,它就會調(diào)用AssetBundleLoader進(jìn)行AssetBundle加載行為臼氨,而AssetBundle本身則使用HotBytesLoader進(jìn)行AssetBundle文件字節(jié)碼進(jìn)行加載。
HotBytesLoader是一個熱更新橋接器——根據(jù)資源“相對路徑”和“熱更新資源目錄”芭届,當(dāng)熱更新資源目錄存在對應(yīng)路徑的文件時储矩,使用熱更新目錄的資源。
所以說褂乍,一次加載行為持隧,會有4個Loader產(chǎn)生,它們之間形成鏈?zhǔn)疥P(guān)系逃片。即AssetFileLoader -> AssetBundleLoader -> HotBytesLoader -> WWWLoader舆蝴。
如前所說,ResourceModule函數(shù)式加載其實(shí)Loader對象式加載的一個簡化题诵。每一次加載行為都會對應(yīng)一個Loader對象洁仗。那么基于AssetFileLoader,由于它是一個單獨(dú)的解耦對象性锭,我們還可以針對它一些特定需求的功能擴(kuò)展:
在不同類型的資源加載中赠潦,不同的行為被劃分成不同的Loader對象。來給資源加載代碼賦予更好的維護(hù)性和可讀性草冈。同時她奥,由于鏈?zhǔn)疥P(guān)系的存在瓮增,指定的AssetBundle文件,永遠(yuǎn)只會被加載一次——這樣來避免一些項(xiàng)目中常見的AssetBundle文件被重復(fù)加載問題哩俭。
每一個Loader對象绷跑,都有一個靜態(tài).Load函數(shù),這是一個工廠函數(shù)凡资,每一個Loader對象通過自身的Load靜態(tài)函數(shù)生成Loader砸捏,來確保引用計(jì)數(shù)、狀態(tài) 的正確隙赁。
AssetFileLoader.Load(path); //...
StaticAssetLoader.Load(path); // ...
對象式調(diào)試
Unity的Profiler可以方便的提供各種Unity運(yùn)行時資源的調(diào)試功能垦藏。它采用快照的方式,捕捉當(dāng)前運(yùn)行時狀態(tài)伞访。只要你對Profiler足夠的熟悉掂骏,大部分運(yùn)行性能問題都從中發(fā)現(xiàn)。
對于AssetBundle加載厚掷,Profiler一是不能實(shí)時獲取動態(tài)弟灼,二是即使發(fā)現(xiàn)了AssetBundle殘留,也難以發(fā)現(xiàn)具體是哪部分代碼殘留了冒黑。而這類調(diào)試的事情袜爪,是可以通過我們游戲里的統(tǒng)一加載接口來更好的發(fā)現(xiàn)的。
由于加載所使用的每一次行為都對應(yīng)著一個Loader追蹤對象薛闪,所以當(dāng)我們要對資源加載行為,進(jìn)行實(shí)時調(diào)試俺陋,簡單來說就是對這些追蹤對象進(jìn)行監(jiān)視豁延。這里用了一個偷懶的方式:Unity引擎編輯器本身就是基于游戲?qū)ο蟮摹?/p>
那好吧,我們把加載對象腊状,以游戲?qū)ο蟮姆绞接沼剑@示在編輯器上,每創(chuàng)建一個Loader缴挖,就緊跟著一個GameObject袋狞,達(dá)到可視化實(shí)時調(diào)試的目的。
從上圖可知映屋,每一個Loader追蹤對象(加載行為)都被一個靜態(tài)的全局列表包存起來苟鸯,因此可以額方便的在Unity編輯器上顯示它們的具體數(shù)量,我們把這些稱為“調(diào)試對象”棚点,點(diǎn)擊后右邊還能顯示其引用計(jì)數(shù)和資源路徑早处。
異步風(fēng)格
Unity的協(xié)程是一個非常好用的單線程異步編程方式,讓普通開發(fā)者在沒有線程編程瘫析、異步編程的基礎(chǔ)下砌梆,也可以方便的進(jìn)行異步編程默责。
另一種常見的單線程異步編程方式是回調(diào)Callback風(fēng)格,是非阻塞IO語言NodeJS的主要異步方式咸包。
無論是協(xié)程還是或者回調(diào)桃序,它們都有一個共同的特點(diǎn),都可以做到是基于單線程進(jìn)行了異步編程烂瘫。關(guān)于異步編程這個話題可以引申出很長的篇幅媒熊,這里就不多介紹了。
Unity開發(fā)中忱反,兩者各有優(yōu)點(diǎn)泛释。協(xié)程可以讓看起來同步的代碼實(shí)現(xiàn)了異步,但在Unity中它的一個蹩腳的地方是需要另寫一個IEnumerator ()函數(shù)温算; 而Callback風(fēng)格則由于C#中強(qiáng)大的匿名函數(shù)語法怜校,使得讓異步代碼寫起來更加的方便。
ResourceModule中兩種異步風(fēng)格并存注竿,可以根據(jù)喜好使用茄茁。
協(xié)程式
IEnumerator LoadSomething()
{
var loader = StaticAssetLoader.Load(path);
while (!loader.IsFinished)
{
yield return null;
}
if (loader.IsError)
{
// error
}
var asset = loader.Asset; // get asset...
// ...
}
這種協(xié)程可謂在Unity中最為常見、舒服的異步方式了巩割。使用起來跟Unity原生的WWW差不多裙顽。
回調(diào)式
StaticAssetLoader.Load(path, (isOk, asset) => {
// get asset async... do something....
});
相比而言,匿名函數(shù)回調(diào)的異步風(fēng)格宣谈,可以寫更好的代碼愈犹,并且調(diào)用代碼更緊密連接。
垃圾回收——基于引用計(jì)數(shù)
我們都知道Java/C#語言的核心是面向?qū)ο笪懦螅麄冎阅敲吹膹?qiáng)大還有一個殺手锏漩怎,就是完全自動垃圾回收機(jī)制。因其基于對象的設(shè)計(jì)嗦嗡,所有對象的生命期都是可以被監(jiān)視和管理的勋锤。
做過iOS開發(fā)的同學(xué)也知道,Objective-C語言的內(nèi)存管理使用引用計(jì)數(shù)的方式來實(shí)現(xiàn)的侥祭。
由于ResourceModule的加載行為都是基于對象叁执,多個Loader對象有互相引用的關(guān)系,ResourceModule模仿了Objective-C引用計(jì)數(shù)的方式來實(shí)現(xiàn)AssetBundle對象的管理矮冬。
點(diǎn)開調(diào)試對象的GameObject谈宛,就能看到調(diào)試對象的引用計(jì)數(shù)信息和加載所耗費(fèi)的時間。
資源的釋放
如要對加載Loader追蹤對象進(jìn)行引用計(jì)數(shù)遞減胎署,可以調(diào)用每個Loader里的Release函數(shù):
loader.Release(); // 引用計(jì)數(shù)-1
當(dāng)一個Loader的引用計(jì)數(shù)為0時入挣,它就會進(jìn)入到釋放隊(duì)列,待幾秒后釋放硝拧。
為什么不像java那樣能全自動的判斷對象是否無用自動釋放径筏?
嗯葛假,ResourceModule的加載器需要手工釋放引用計(jì)數(shù)。
因?yàn)闆]法捕捉GameObject對象刪除事件滋恬,Unity并沒有提供這樣的事件出來監(jiān)視游戲?qū)ο蟮膭h除事件聊训,所以無法捕捉說什么時候去把這一個對象的引用遞減,所以只能手動的去恢氯,進(jìn)行引用計(jì)數(shù)的管理带斑。
延遲清理
當(dāng)一個加載對象被引用計(jì)數(shù)減為0的時候,他不會被立刻釋放勋拟。因?yàn)榇嬖谶@樣一種場景:當(dāng)引用變成0的同一時間勋磕,同樣的資源又被創(chuàng)建一份新的,引用計(jì)數(shù)立刻變回1敢靡。所以如果說當(dāng)他引用計(jì)數(shù)為0時候挂滓,立刻就被清理了,同時又被創(chuàng)建啸胧,這里赶站,就會造成了重復(fù)的對這份內(nèi)存資源創(chuàng)建和釋放。
路由——管理資源加載的路徑
Unity是一個跨平臺的游戲引擎纺念,每一個平臺都會有它特殊的處理資源的路徑方式贝椿,在Unity中一般我們常見的是StreamingAssets和PersistentDataPath兩種路徑。
可是這里面陷谱,也隱含有不少的坑烙博,比如說,在windows平臺里面烟逊,路徑URL渣窜,斜杠必須得3個///。安卓平臺下焙格,StreamingAssets目錄是不能同步讀取的(APK內(nèi)目錄),但是包括iOS在內(nèi)的其他所有平臺都是可以通過同步File.ReadAllBytes讀取的夷都。
不僅如此眷唉,由于Asset Bundle的打包是平臺定向性的:打出Android的Asset Bundle,不能再iOS下使用囤官;反之亦然冬阳。因此,AssetBundleLoader加載器在實(shí)際運(yùn)行時党饮,需要一個路由管理器來告訴它什么樣的平臺肝陪,使用哪里的Asset Bundle目錄。我把這叫作“路由”刑顺。
所以在ResourceModule中氯窍,路由管理器做了很多路徑的識別的工作饲常,來統(tǒng)籌各種不同平臺下的資源路徑,來整個Asset Bundle模塊的開箱即用狼讨。
熱更新
我們使用AssetBundle贝淤,無非最想解決的就是一個需求——熱更新。
熱更新的兩個核心要素政供,資源路徑讀取與下載更新播聪。
資源路由管理器,除了平臺差異化路徑處理布隔,另外的核心功能就是熱更新路徑處理了——即此前所說的离陶,優(yōu)先判斷PersistentDataPath路徑是否存在指定的熱更文件。
后記
以上我們分別從加載衅檀、調(diào)試招刨、異步、垃圾回收术吝、路由5個方面计济,介紹了這種基于面向?qū)ο蟮乃枷朐O(shè)計(jì)的用來進(jìn)行AssetBundle加載的ResourceModule管理器。
它的本質(zhì)是將行為進(jìn)行對象化排苍。概括來說就是把加載行為以對象的方式保存起來沦寂。
它的代碼開源放在 「Github ResourceModule」 ,是Unity開發(fā)框架KSFramework的核心部分淘衙。對于很多使用者來說传藏,ResourceModule就像一個黑箱子,雖然一直能用彤守,但是一直不好理解它的內(nèi)部構(gòu)思毯侦,所以就有了本文。
Unity的資源管理是一個很大的話題具垫,本文僅僅從它的加載方式著手提出一種方案侈离,更多深入的細(xì)節(jié),更多的坑筝蚕,還得伴隨項(xiàng)目的進(jìn)度而慢慢積累經(jīng)驗(yàn)卦碾。更多的經(jīng)驗(yàn),可以私信跟我交流起宽,我也愿意跟你分享洲胖。
一不小心洋洋灑灑的寫了4000多字,篇幅稍長坯沪,如果對Asset Bundle機(jī)制沒有太深入了解的話绿映,當(dāng)中有一些地方可能不好讀懂。出現(xiàn)這種情況,那肯定不是你的問題叉弦,而是我沒有寫清楚丐一。希望在評論處留下寶貴的建議!