1. 程式人生 > >一個靈活的AssetBundle打包工具

一個靈活的AssetBundle打包工具

復雜 TE 編輯器 scene clip ons 定制化 enum 了解

技術分享圖片 尼爾:機械紀元

上周介紹了Unity項目中的資源配置,今天和大家分享一個AssetBundle打包工具。相信從事Unity開發或多或少都了解過AssetBundle,但簡單的接口以及眾多的細碎問題也給工作帶來較多的困擾。今天分享AssetBundle工具的實踐與想法,相信這塊內容對幫助理解AssetBundle有較大的幫助。

Unity提供了兩種資源加載方式,一種是Resources,另外種就是AssetBundle。所有的資源只要放在Resources目錄下,在打包的時候會自動打進去,並可以通過相應的接口加載。正常情況下Resources非常方便,可以滿足日常的需求,但資源放Resources會帶來資源更新上的問題。之前寫過一篇文章Unity資源目錄及加載接口介紹可以了解些細節。

假設首包所有資源都放Resources,後續更新資源的走AssetBundle,會發現AssetBundle和Resources的資源互相不兼容。當調整一個模型的材質參數後,對模型進行打包仍需要把Mesh,Texture等資源都打進去。這會導致更新包過大,同時在加載這個模型時,這些資源是不共用的,相同的資源可能在內存中存在兩份。所以正常情況下,項目發布時所有需要更新的資源要打成AssetBundle。

正常項目中資源的提交與變更非常頻繁,手工對每個資源配置Bundle費時費力,基本不可取。所以一般項目中的Bundle都是程序自動創建的。同時為了避免有多余的資源被打包,通常需要配置哪些資源是發布資源(直接加載的),其他資源通過引用的形式獲取。這個配置需要方便修改,來滿足日常變更。

Bundle的打包規則對資源加載速度,更新大小,重復資源數量以及最終包數量等等都有較大影響。一個可靠的Bundle打包方案應該是根據實際情況對Bundle打包規則做調整慢慢產生的。

在Unity 4,只有最基礎的幾個打包接口可以用於打包。Unity 5簡化了Bundle打包時候的依賴關系,但實際如何創建Bundle以及對依賴資源的配置都節省不了。遠遠不能滿足項目對資源打包這塊的需求。

這裏實現的AssetBundle打包工具幫助簡化這個繁瑣的打包過程,同時方便做規則調整,得到更優的打包方案。目前工具BundleBuildTool已經放在GitHub,可以作為一份打包實現的參考,也可以直接使用這工具來進行打包。

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.

資源類型

不同類型資源會有不同的打包方式,比如場景文件的打包接口和其他資源的打包接口就是不一樣的。通過定義不同的資源類型,可以實現不同的打包方式,支持更多資源的打包。

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
}

對於特殊類型的資源,通過類型可以做一些定制化操作。比如把所有的Script配置在一個Bundle裏面,然後在啟動的時候對這個Bundle做預加載。通常情況下也會把所有的Shader配置到一個Bundle裏面。

正常一個模型會有自己的Texture,Mesh & Animation,把資源按類型打成三個包,在加載的時候可以得到更高的加載速度。Unity異步加載接口會同時進行多個資源加載,資源配置在不同的包裏,可以有較好的加載速度提升,所以一般是按資源類型來進行打包。不過要註意如果太分散的話,一樣會影響加載速度。

資源加載速度這個是在文章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.

資源依賴

處理資源依賴應該是打包過程最復雜的一塊功能,這裏把獲取資源依賴文件列表單獨設計一個類,做一些特殊情況處理。如果發現一些依賴關系上的錯誤,除了修改資源本身外,也可以在打包環節實現一些腳步做保障。

正常情況下,通過AssetDatabase.GetDependencies即可獲取一個資源的所以依賴文件。但實際情況中,Unity內部是通過分析內部guid來生成依賴文件。有時候在文件裏面會存在一些臟的guid這會產生多余的依賴。比如你修改一個材質貼圖屬性名,然後設置了一張新的貼圖給這個新的屬性名。打開材質文件會發現舊的屬性名以及引用guid出現在材質文件,通過GetDependencies獲取的最後結果也包含這個數據。實現自己獲取依賴函數來處理這種多余依賴關系。同時提供帶緩存接口,提高打包效率。

下面是對材質依賴貼圖文件獲取的代碼實現。

...
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);
    }
}   
...

資源剔除

處理完資源依賴後,還碰到一個問題就是最後打包Assets資源。通過AssetDatabase.LoadAllAssetAtPath獲取這個文件依賴的所有的Assets資源。如果對所有的這些Assets資源都做打包的話,會發現一些編輯器用數據也會被打包進去。特別是對於FBX類型文件,通常會存在一個"__preview_Take 001"的動作資源使包體變大很多。對於這些不必要的數據,在打包環節中增加一個剔除規則,減少包體大小。

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剛出的時候會把這個數據打進AssetBundle造成包體過大,後面版本觀察已經修復這個問題。不過也可以發現這個環節的必要性,如果發現資源出問題在這個環節處理即可。

這個環節不僅可以剔除不必要的數據,還可以直接修改數據本身。就拿Mesh數據舉例,美術在制作過程中會導出多余的頂點數據在文件裏面(uv3,uv4...)。通常配置Optimize Mesh可以幹掉這些無用數據,不過直接啟用可能會出現刪除了需要數據情況,比如color數據丟失。所以自己來做,通過把Mesh對象上不需要的對象數據置空,然後再打包即可。在之前分享的資源配置工具裏已經做了對Mesh頂點數據的配置,基本上就是為這個打包環節服務,因為無法修改FBX文件,只能美術重新導出。

資源大小

資源大小影響最後的包體大小,如果對包體大小以及更新量有關註的話,對資源大小做預估是一個非常有必要的環節。在資源大小計算環節,不能疏漏之前二個資源環節對資源的處理,同時不同類型的資源統計方式不一樣。

通常通過下面兩個方式預估資源大小

int resSize = UnityEngine.Profiling.Profiler.GetRuntimeMemorySize(asset);
FileInfo fileInfo = new FileInfo(assetPath);
int fileSize = fileInfo.Length;

如何對一個資源做一個大小估算,並不是一件非常方便的事情的。如果依賴資源已經在之前打包了,那這個資源的實際大小是要考慮減去依賴資源那部分的大小。如果不統計依賴資源的大小,那這個資源的包的大小也是不準確的。所以這裏的實際邏輯較為復雜,但實際一個大致的值就可以了,然後觀察最後的包大小做一些配置微調即可。

Bundle模型

討論完資源上的一些細節,下面開始Bundle設計的介紹。一個Bundle模型用name做唯一標識,為了方便管理加入了parent與children數據。同時一個Bundle應該有一個固定資源類型。為了方便對包大小做限制加入了size屬性,作為資源大小的預估。

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>();
}

最後一個Bundle包含多個資源文件路徑。盡管AssetBundle是按Assets打包的,但在正常環境下的資源是以文件存在的。一個資源文件可能包含多個資源,也可能引用到其他資源。資源文件可以用路徑來標識,Unity內部通過GUID來標識資源文件,所以即使你挪動文件因為GUID不變,還是可以找到這個文件。這裏決定直接用資源路徑來標識資源而不是使用GUID,因為挪動資源目錄有較多的風險,原則上禁止挪動資源。如果真挪動了資源,按最新的資源路徑生成Bundle是一個不錯的選擇。

如果有對Bundle有其他屬性上的需求,在這個類擴展就好。

Bundle創建規則

定義Bundle後,創建Bundle是很困擾的一個問題。在大型項目中,資源的量非常大,資源之間的互相引用也較為復雜。這裏定義一個數據結構幫忙創建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;
}

對於一個Bundle,可以約束它的大小,對象數量、類型、加載方式、打包方式。然後根據規則,自動給每個資源文件配置Bundle。

資源分加載資源和被依賴引用到的資源,對於直接加載的資源,需要配置Publish為True。Bundle創建就是從這些配置了Publish的資源文件以及其依賴生成的。

對所有可能被打包的資源配置打包規則,沒有被配置資源文件,則會被一起打倒最後資源的包裏面。這裏會碰到一個問題,有些資源需要補分包,但是通用規則會包含不需要分包的資源。這裏增加了一個SkipData屬性,當為True時這些資源不單獨創建Bundle。

然後討論下PushDependice屬性,正常情況下只有在打Prefab類型的資源的時候才會做這個操作。因為Prefab數據本身是不共享的,然後避免Prefab與Prefab之間的復雜依賴。

最後討論下打包的順序,因為資源之間有互相依賴,所以需要配置資源的打包順序。這裏資源的打包順序就是BundleImportData創建的順序。這裏需要對資源之間的依賴以及資源類型有一定的認識。

已經配置過Bundle的資源不會變更,新增的資源會按規則配置相應的Bundle。通常規則發生變更會影響非常多的資源,如果所有資源重新配置會導致更新包過大。

Bundle構建

首次創建的Bundle,由於本地文件不存在,會觸發構建。然後資源之間有互相依賴,所有被依賴的Bundle也需要參加構建。對於增量構建,這裏做了一個簡化設計,不自己去計算文件是否變更,而是由外部提供一個文件變化列表。通過這個列表工具自動生成Bundle構建列表,提高打包速度。

在配置打包參數為BuildAssetBundleOptions.DeterministicAssetBundle後,如果不對資源做修改,兩次打包的文件是一樣的。所以即使有很多資源因為依賴要重新打包,最後的文件未發生變化,就不會觸發更新。

Bundle索引

Bundle構建完後只是一堆二進制文件,需要根據Bundle之間的依賴關系生成出一份數據。除了需要知道Bundle之間的依賴之外,同時還需要知道資源路徑與Bundle之間的映射關系。最後還要把Bundle狀態信息保存下來,用於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 { ... }

這個文件自己定義形式,可以使分散的多個文件,也可以統一放到一個文件裏面,自己實現可以優化數據結構減少內存開銷。

通用的Bundle打包方案

下面是在Unity Standard Assets資源上做配置後的結果

技術分享圖片 BundleBuildTool

按大小配置基礎資源,然後對於Prefab和Unity文件限定下個數,避免過多的資源依賴。配置結束後點擊CreateBundle就可以得到下面的結果。

[完 2017-07-13 Carber]

  • AssetBundle打包工具
  • 本文首發於我的簡書博客(鏈接)
  • 本文同時發布在知乎專欄(鏈接)


作者:carber
鏈接:https://www.jianshu.com/p/a7720cbbe4c4
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並註明出處。

一個靈活的AssetBundle打包工具