
上周介紹了Unity項(xiàng)目中的資源配置,今天和大家分享一個(gè)AssetBundle打包工具。相信從事Unity開(kāi)發(fā)或多或少都了解過(guò)AssetBundle,但簡(jiǎn)單的接口以及眾多的細(xì)碎問(wèn)題也給工作帶來(lái)較多的困擾。今天分享AssetBundle工具的實(shí)踐與想法,相信這塊內(nèi)容對(duì)幫助理解AssetBundle有較大的幫助。
Unity提供了兩種資源加載方式,一種是Resources,另外種就是AssetBundle。所有的資源只要放在Resources目錄下,在打包的時(shí)候會(huì)自動(dòng)打進(jìn)去,并可以通過(guò)相應(yīng)的接口加載。正常情況下Resources非常方便,可以滿足日常的需求,但資源放Resources會(huì)帶來(lái)資源更新上的問(wèn)題。之前寫過(guò)一篇文章Unity資源目錄及加載接口介紹可以了解些細(xì)節(jié)。
假設(shè)首包所有資源都放Resources,后續(xù)更新資源的走AssetBundle,會(huì)發(fā)現(xiàn)AssetBundle和Resources的資源互相不兼容。當(dāng)調(diào)整一個(gè)模型的材質(zhì)參數(shù)后,對(duì)模型進(jìn)行打包仍需要把Mesh,Texture等資源都打進(jìn)去。這會(huì)導(dǎo)致更新包過(guò)大,同時(shí)在加載這個(gè)模型時(shí),這些資源是不共用的,相同的資源可能在內(nèi)存中存在兩份。所以正常情況下,項(xiàng)目發(fā)布時(shí)所有需要更新的資源要打成AssetBundle。
正常項(xiàng)目中資源的提交與變更非常頻繁,手工對(duì)每個(gè)資源配置Bundle費(fèi)時(shí)費(fèi)力,基本不可取。所以一般項(xiàng)目中的Bundle都是程序自動(dòng)創(chuàng)建的。同時(shí)為了避免有多余的資源被打包,通常需要配置哪些資源是發(fā)布資源(直接加載的),其他資源通過(guò)引用的形式獲取。這個(gè)配置需要方便修改,來(lái)滿足日常變更。
Bundle的打包規(guī)則對(duì)資源加載速度,更新大小,重復(fù)資源數(shù)量以及最終包數(shù)量等等都有較大影響。一個(gè)可靠的Bundle打包方案應(yīng)該是根據(jù)實(shí)際情況對(duì)Bundle打包規(guī)則做調(diào)整慢慢產(chǎn)生的。
在Unity 4,只有最基礎(chǔ)的幾個(gè)打包接口可以用于打包。Unity 5簡(jiǎn)化了Bundle打包時(shí)候的依賴關(guān)系,但實(shí)際如何創(chuàng)建Bundle以及對(duì)依賴資源的配置都節(jié)省不了。遠(yuǎn)遠(yuǎn)不能滿足項(xiàng)目對(duì)資源打包這塊的需求。
這里實(shí)現(xiàn)的AssetBundle打包工具幫助簡(jiǎn)化這個(gè)繁瑣的打包過(guò)程,同時(shí)方便做規(guī)則調(diào)整,得到更優(yōu)的打包方案。目前工具BundleBuildTool已經(jīng)放在GitHub,可以作為一份打包實(shí)現(xiàn)的參考,也可以直接使用這工具來(lái)進(jìn)行打包。
AssetBundle
An AssetBundle is an archive file containing platform specific Assets (Models, Textures, Prefabs, Audio clips, and even entire Scenes) that can be loaded at runtime.
資源類型
不同類型資源會(huì)有不同的打包方式,比如場(chǎng)景文件的打包接口和其他資源的打包接口就是不一樣的。通過(guò)定義不同的資源類型,可以實(shí)現(xiàn)不同的打包方式,支持更多資源的打包。
public enum BundleType
{
None = 0,
Script, // .cs
Shader, // .shader or build-in shader with name
Font, // .ttf
Texture, // .tga, .png, .jpg, .tif, .psd, .exr
Material, // .mat
Animation, // .anim
Controller, // .controller
FBX, // .fbx
TextAsset, // .txt, .bytes
Prefab, // .prefab
UnityMap, // .unity
}
對(duì)于特殊類型的資源,通過(guò)類型可以做一些定制化操作。比如把所有的Script配置在一個(gè)Bundle里面,然后在啟動(dòng)的時(shí)候?qū)@個(gè)Bundle做預(yù)加載。通常情況下也會(huì)把所有的Shader配置到一個(gè)Bundle里面。
正常一個(gè)模型會(huì)有自己的Texture,Mesh & Animation,把資源按類型打成三個(gè)包,在加載的時(shí)候可以得到更高的加載速度。Unity異步加載接口會(huì)同時(shí)進(jìn)行多個(gè)資源加載,資源配置在不同的包里,可以有較好的加載速度提升,所以一般是按資源類型來(lái)進(jìn)行打包。不過(guò)要注意如果太分散的話,一樣會(huì)影響加載速度。
資源加載速度這個(gè)是在文章Asset Bundles vs. Resources: A Memory Showdown提及。
These blocks sizes are optimized for loading multiple Assets and bundles in parallel. For example, you should be able to load objects from 4 to 5 Asset Bundles at the same time without the the allocators for Asset Bundle Async loading or Type Trees needing new blocks.
資源依賴
處理資源依賴應(yīng)該是打包過(guò)程最復(fù)雜的一塊功能,這里把獲取資源依賴文件列表單獨(dú)設(shè)計(jì)一個(gè)類,做一些特殊情況處理。如果發(fā)現(xiàn)一些依賴關(guān)系上的錯(cuò)誤,除了修改資源本身外,也可以在打包環(huán)節(jié)實(shí)現(xiàn)一些腳步做保障。
正常情況下,通過(guò)AssetDatabase.GetDependencies即可獲取一個(gè)資源的所以依賴文件。但實(shí)際情況中,Unity內(nèi)部是通過(guò)分析內(nèi)部guid來(lái)生成依賴文件。有時(shí)候在文件里面會(huì)存在一些臟的guid這會(huì)產(chǎn)生多余的依賴。比如你修改一個(gè)材質(zhì)貼圖屬性名,然后設(shè)置了一張新的貼圖給這個(gè)新的屬性名。打開(kāi)材質(zhì)文件會(huì)發(fā)現(xiàn)舊的屬性名以及引用guid出現(xiàn)在材質(zhì)文件,通過(guò)GetDependencies獲取的最后結(jié)果也包含這個(gè)數(shù)據(jù)。實(shí)現(xiàn)自己獲取依賴函數(shù)來(lái)處理這種多余依賴關(guān)系。同時(shí)提供帶緩存接口,提高打包效率。
下面是對(duì)材質(zhì)依賴貼圖文件獲取的代碼實(shí)現(xiàn)。
...
MaterialProperty[] proTes = MaterialEditor.GetMaterialProperties(new Object[] {mat});
for (int i = 0; proTes != null && i < proTes.Length; ++i)
{
if (proTes[i].type == MaterialProperty.PropType.Texture)
{
Texture tex = mat.GetTexture(proTes[i].name);
string path = AssetDatabase.GetAssetPath(tex);
if (!dict.ContainsKey(path))
{
dict.Add(path, path);
}
Resources.UnloadAsset(tex);
}
}
...
資源剔除
處理完資源依賴后,還碰到一個(gè)問(wèn)題就是最后打包Assets資源。通過(guò)AssetDatabase.LoadAllAssetAtPath獲取這個(gè)文件依賴的所有的Assets資源。如果對(duì)所有的這些Assets資源都做打包的話,會(huì)發(fā)現(xiàn)一些編輯器用數(shù)據(jù)也會(huì)被打包進(jìn)去。特別是對(duì)于FBX類型文件,通常會(huì)存在一個(gè)"__preview_Take 001"的動(dòng)作資源使包體變大很多。對(duì)于這些不必要的數(shù)據(jù),在打包環(huán)節(jié)中增加一個(gè)剔除規(guī)則,減少包體大小。
public static List<UnityEngine.Object> FilterObjectByType(UnityEngine.Object[] assets, BundleType bundleType)
{
List<UnityEngine.Object> ret = new List<UnityEngine.Object>();
foreach (UnityEngine.Object asset in assets)
{
switch (bundleType)
{
case BundleType.FBX:
if (!(asset.GetType() == typeof(AnimationClip) && asset.name == "__preview_Take 001"))
{
ret.Add(asset);
}
break;
default:
ret.Add(asset);
break;
}
}
return ret;
}
Unity 5剛出的時(shí)候會(huì)把這個(gè)數(shù)據(jù)打進(jìn)AssetBundle造成包體過(guò)大,后面版本觀察已經(jīng)修復(fù)這個(gè)問(wèn)題。不過(guò)也可以發(fā)現(xiàn)這個(gè)環(huán)節(jié)的必要性,如果發(fā)現(xiàn)資源出問(wèn)題在這個(gè)環(huán)節(jié)處理即可。
這個(gè)環(huán)節(jié)不僅可以剔除不必要的數(shù)據(jù),還可以直接修改數(shù)據(jù)本身。就拿Mesh數(shù)據(jù)舉例,美術(shù)在制作過(guò)程中會(huì)導(dǎo)出多余的頂點(diǎn)數(shù)據(jù)在文件里面(uv3,uv4...)。通常配置Optimize Mesh可以干掉這些無(wú)用數(shù)據(jù),不過(guò)直接啟用可能會(huì)出現(xiàn)刪除了需要數(shù)據(jù)情況,比如color數(shù)據(jù)丟失。所以自己來(lái)做,通過(guò)把Mesh對(duì)象上不需要的對(duì)象數(shù)據(jù)置空,然后再打包即可。在之前分享的資源配置工具里已經(jīng)做了對(duì)Mesh頂點(diǎn)數(shù)據(jù)的配置,基本上就是為這個(gè)打包環(huán)節(jié)服務(wù),因?yàn)闊o(wú)法修改FBX文件,只能美術(shù)重新導(dǎo)出。
資源大小
資源大小影響最后的包體大小,如果對(duì)包體大小以及更新量有關(guān)注的話,對(duì)資源大小做預(yù)估是一個(gè)非常有必要的環(huán)節(jié)。在資源大小計(jì)算環(huán)節(jié),不能疏漏之前二個(gè)資源環(huán)節(jié)對(duì)資源的處理,同時(shí)不同類型的資源統(tǒng)計(jì)方式不一樣。
通常通過(guò)下面兩個(gè)方式預(yù)估資源大小
int resSize = UnityEngine.Profiling.Profiler.GetRuntimeMemorySize(asset);
FileInfo fileInfo = new FileInfo(assetPath);
int fileSize = fileInfo.Length;
如何對(duì)一個(gè)資源做一個(gè)大小估算,并不是一件非常方便的事情的。如果依賴資源已經(jīng)在之前打包了,那這個(gè)資源的實(shí)際大小是要考慮減去依賴資源那部分的大小。如果不統(tǒng)計(jì)依賴資源的大小,那這個(gè)資源的包的大小也是不準(zhǔn)確的。所以這里的實(shí)際邏輯較為復(fù)雜,但實(shí)際一個(gè)大致的值就可以了,然后觀察最后的包大小做一些配置微調(diào)即可。
Bundle模型
討論完資源上的一些細(xì)節(jié),下面開(kāi)始Bundle設(shè)計(jì)的介紹。一個(gè)Bundle模型用name做唯一標(biāo)識(shí),為了方便管理加入了parent與children數(shù)據(jù)。同時(shí)一個(gè)Bundle應(yīng)該有一個(gè)固定資源類型。為了方便對(duì)包大小做限制加入了size屬性,作為資源大小的預(yù)估。
public class BundleData
{
public string name = string.Empty;
public string parent = string.Empty;
public BundleType type = BundleType.None;
public BundleLoadState loadState = BundleLoadState.UnloadImmediately;
public int size = 0;
public List<string> includs = new List<string>();
public List<string> children = new List<string>();
}
最后一個(gè)Bundle包含多個(gè)資源文件路徑。盡管AssetBundle是按Assets打包的,但在正常環(huán)境下的資源是以文件存在的。一個(gè)資源文件可能包含多個(gè)資源,也可能引用到其他資源。資源文件可以用路徑來(lái)標(biāo)識(shí),Unity內(nèi)部通過(guò)GUID來(lái)標(biāo)識(shí)資源文件,所以即使你挪動(dòng)文件因?yàn)镚UID不變,還是可以找到這個(gè)文件。這里決定直接用資源路徑來(lái)標(biāo)識(shí)資源而不是使用GUID,因?yàn)榕矂?dòng)資源目錄有較多的風(fēng)險(xiǎn),原則上禁止挪動(dòng)資源。如果真挪動(dòng)了資源,按最新的資源路徑生成Bundle是一個(gè)不錯(cuò)的選擇。
如果有對(duì)Bundle有其他屬性上的需求,在這個(gè)類擴(kuò)展就好。
Bundle創(chuàng)建規(guī)則
定義Bundle后,創(chuàng)建Bundle是很困擾的一個(gè)問(wèn)題。在大型項(xiàng)目中,資源的量非常大,資源之間的互相引用也較為復(fù)雜。這里定義一個(gè)數(shù)據(jù)結(jié)構(gòu)幫忙創(chuàng)建Bundle。
public class BundleImportData
{
public string RootPath = "";
public string FileNameMatch = "*.*";
public int Index = -1;
public int TotalCount = 0;
public BundleType Type = BundleType.None;
public BundleLoadState LoadState = BundleLoadState.OnUnloadAsset;
public bool Publish = false;
public int LimitCount = -1;
public int LimitKBSize = -1;
public bool PushDependice = false;
public bool SkipData = false;
}
對(duì)于一個(gè)Bundle,可以約束它的大小,對(duì)象數(shù)量、類型、加載方式、打包方式。然后根據(jù)規(guī)則,自動(dòng)給每個(gè)資源文件配置Bundle。
資源分加載資源和被依賴引用到的資源,對(duì)于直接加載的資源,需要配置Publish為True。Bundle創(chuàng)建就是從這些配置了Publish的資源文件以及其依賴生成的。
對(duì)所有可能被打包的資源配置打包規(guī)則,沒(méi)有被配置資源文件,則會(huì)被一起打倒最后資源的包里面。這里會(huì)碰到一個(gè)問(wèn)題,有些資源需要補(bǔ)分包,但是通用規(guī)則會(huì)包含不需要分包的資源。這里增加了一個(gè)SkipData屬性,當(dāng)為True時(shí)這些資源不單獨(dú)創(chuàng)建Bundle。
然后討論下PushDependice屬性,正常情況下只有在打Prefab類型的資源的時(shí)候才會(huì)做這個(gè)操作。因?yàn)镻refab數(shù)據(jù)本身是不共享的,然后避免Prefab與Prefab之間的復(fù)雜依賴。
最后討論下打包的順序,因?yàn)橘Y源之間有互相依賴,所以需要配置資源的打包順序。這里資源的打包順序就是BundleImportData創(chuàng)建的順序。這里需要對(duì)資源之間的依賴以及資源類型有一定的認(rèn)識(shí)。
已經(jīng)配置過(guò)Bundle的資源不會(huì)變更,新增的資源會(huì)按規(guī)則配置相應(yīng)的Bundle。通常規(guī)則發(fā)生變更會(huì)影響非常多的資源,如果所有資源重新配置會(huì)導(dǎo)致更新包過(guò)大。
Bundle構(gòu)建
首次創(chuàng)建的Bundle,由于本地文件不存在,會(huì)觸發(fā)構(gòu)建。然后資源之間有互相依賴,所有被依賴的Bundle也需要參加構(gòu)建。對(duì)于增量構(gòu)建,這里做了一個(gè)簡(jiǎn)化設(shè)計(jì),不自己去計(jì)算文件是否變更,而是由外部提供一個(gè)文件變化列表。通過(guò)這個(gè)列表工具自動(dòng)生成Bundle構(gòu)建列表,提高打包速度。
在配置打包參數(shù)為BuildAssetBundleOptions.DeterministicAssetBundle后,如果不對(duì)資源做修改,兩次打包的文件是一樣的。所以即使有很多資源因?yàn)橐蕾囈匦麓虬?,最后的文件未發(fā)生變化,就不會(huì)觸發(fā)更新。
Bundle索引
Bundle構(gòu)建完后只是一堆二進(jìn)制文件,需要根據(jù)Bundle之間的依賴關(guān)系生成出一份數(shù)據(jù)。除了需要知道Bundle之間的依賴之外,同時(shí)還需要知道資源路徑與Bundle之間的映射關(guān)系。最后還要把Bundle狀態(tài)信息保存下來(lái),用于Bundle更新、加載和卸載。
public class BundleState
{
public string bundleID = string.Empty;
public uint crc = 0;
public uint compressCrc = 0;
public int version = -1;
public long size = -1;
public BundleLoadState loadState = BundleLoadState.OnUnloadAsset;
public BundleStorePos storePos = BundleStorePos.Building;
}
// like UnityEngine.AssetBundleManifest
public class BundleManifest { ... }
這個(gè)文件自己定義形式,可以使分散的多個(gè)文件,也可以統(tǒng)一放到一個(gè)文件里面,自己實(shí)現(xiàn)可以優(yōu)化數(shù)據(jù)結(jié)構(gòu)減少內(nèi)存開(kāi)銷。
通用的Bundle打包方案
下面是在Unity Standard Assets資源上做配置后的結(jié)果

按大小配置基礎(chǔ)資源,然后對(duì)于Prefab和Unity文件限定下個(gè)數(shù),避免過(guò)多的資源依賴。配置結(jié)束后點(diǎn)擊CreateBundle就可以得到下面的結(jié)果。
[完 2017-07-13 Carber]
- AssetBundle打包工具
- 本文首發(fā)于我的簡(jiǎn)書博客(鏈接)
- 本文同時(shí)發(fā)布在知乎專欄(鏈接)