一個(gè)靈活的AssetBundle打包工具

尼爾:機(jī)械紀(jì)元

上周介紹了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é)果

BundleBuildTool

按大小配置基礎(chǔ)資源,然后對(duì)于Prefab和Unity文件限定下個(gè)數(shù),避免過(guò)多的資源依賴。配置結(jié)束后點(diǎn)擊CreateBundle就可以得到下面的結(jié)果。

[完 2017-07-13 Carber]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 翻譯:莫銘原文地址:AssetBundle usage patterns 本系列中的上一篇文章覆蓋了AssetBu...
    莫銘閱讀 5,695評(píng)論 1 12
  • 這部分主要討論了AssetBundle的如下知識(shí): AssetBundle的基礎(chǔ)知識(shí) 使用AssetBundle的...
    Wenchao閱讀 1,820評(píng)論 0 5
  • 1、什么是AssetBundle AssetBundle 是Unity pro提供的一種用來(lái)存儲(chǔ)資源的文件格式,它...
    好怕怕閱讀 7,874評(píng)論 1 8
  • 工具Unity 中的資源來(lái)源有三個(gè)途徑:一個(gè)是Unity自動(dòng)打包資源,一個(gè)是Resources,一個(gè)是AssetB...
    某人在閱讀 9,198評(píng)論 0 5
  • 護(hù)士的日常不是上班就是睡覺(jué)。一入此門深似海,從此休閑是奢望。
    斯文女漢子閱讀 225評(píng)論 0 0

友情鏈接更多精彩內(nèi)容