unity 之 AssetBundle

AssetBundles

 AssetBundle 是一个整合的没有代码的文件,它包括模型,贴图,prefab,音频,甚至整个场景,最重要的一点是它可以在运行时动态加载. AssetBundles 可以表述成彼此之间的依赖关系 dependencies
between each other; 比如,一个 AssetBundle 中的材质球可以引用另一个 AssetBundle的贴图,为了能够快速的加载进游戏(也就是减小ab包的大小), 可以选择性的选择内置的压缩算发压缩 AssetBundles ,比如 (LZMA and LZ4)压缩方法

AssetBundles 对需要运行时加载的内容很重要,减少安装包的大小,针对特定的平台加载特定的资源,减少运行时内存的压力.

 

What’s in an AssetBundle?

“AssetBundle” 顾名思义,就是资源纽带,它可以表述成两个不同但相关的事物.

首先是硬盘上真是存在的资源,它可以称为 AssetBundle 文档. AssetBundle 文档是一个容器,就像文件夹,包含了一些额外的文件. 这些额外的文件由两种类型组成:

一种是序列化文件,Asset分解成各自的对象并写入到这个文件中。

一种是资源文件,它是一个二进制块,独立(分开)存储着你的资源,比如贴图,音频文件,能够快速的从硬盘上被加载到游戏当中

“AssetBundle”也可以指你通过代码从特定的AssetBundle文档中加载资源(Asset)的对象。此对象包含添加到此AssetBundle中的Asset的所有文件路径的映射。就是包含了你资源的路径,以供加载

 

AssetBundle Workflow

构建AssetBundles

  1. 在Project 的面板中选择一个物体
  2. 在属性面板底部,声明assetbundle的名字,后面是它的后缀,后缀可以是任意的
  3. 名字可以以路径的方式命名,比如environment/forest,会在environment文件夹生成一个forest文件

Build the AssetBundles

using UnityEditor;
using System.IO;

public class CreateAssetBundles
{
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAllAssetBundles()
    {
        string assetBundleDirectory = "Assets/AssetBundles";
        if(!Directory.Exists(assetBundleDirectory))
        {
            Directory.CreateDirectory(assetBundleDirectory);
        }
        BuildPipeline.BuildAssetBundles(assetBundleDirectory, 
                                        BuildAssetBundleOptions.None, 
                                        BuildTarget.StandaloneWindows);
    }
}

 

 

Loading AssetBundles and Assets

本地加载资源

For users intending to load from local storage, you’ll be interested in the AssetBundles.LoadFromFile API. Which looks like this:

public class LoadFromFileExample extends MonoBehaviour {
    function Start() 
     {
        var myLoadedAssetBundle=AssetBundle.LoadFromFile
       (Path.Combine(Application.streamingAssetsPath,"myassetBundle.ab"));
       //这个地方是完整的路径,要加上后缀名
        if (myLoadedAssetBundle == null) {
            Debug.Log("Failed to load AssetBundle!");
            return;
        }
//"MyObject"是打包物体的名字,比如cube打包成assetbundle,名字为“myassetBundle.ab”
        var prefab = myLoadedAssetBundle.LoadAsset<GameObject>("MyObject");
        Instantiate(prefab);
    }
}

网络加载

IEnumerator InstantiateObject()
{
    //assetBundleName也是完整的路径,加上后缀
    string url = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;        
    UnityEngine.Networking.UnityWebRequest request 
        = UnityEngine.Networking.UnityWebRequestAssetBundle.GetAssetBundle(url, 0);
    yield return request.Send();
    AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
    GameObject cube = bundle.LoadAsset<GameObject>("Cube");
    GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
    Instantiate(cube);
    Instantiate(sprite);
}

GetAssetBundle(string, int) 前边是路径,后边是版本号,路径要加上后缀名

UnityWebRequest 处理的是 AssetBundles, DownloadHandlerAssetBundle, 处理的是assetbundle的请求 request.

 

打包 AssetBundles的技巧

Logical Entity Grouping:按功能打包

Examples

  • 把UI界面的贴图和一些分层数据打包到一个assetbundle里面
  • 把一个人物的模型和动画打包到一个assetbundle里面
  • 将多个场景中共享的纹理和模型打包到一个assetbundle里面

把资源单独分开打包,可以避免下载多余的assetbundle,比如模型中的材质更改了,就只更改材质,没必要更改其他的

Type Grouping:分组打包

分组打包:比如音频文件,材质,UI啊

根据不同的平台分组打包比较好,比如iOS和windows两种不同的平台,你的材质压缩的格式是不同的,所以你要分别打两种包,这样整体打包比较好,就像shader,ios和window是不同的压缩格式
 

Concurrent Content Grouping:单个整体打包

把一些会同时使用的资源统一打包成整个包. 比如一个关卡,里面有独立的角色,材质,音乐,你可以把它们打包成一个,但这样会增加包依赖的程度,当你更改其中一个的时候,要下载整个完整的包。

最常用这种打包方式就是根据场景来加载资源的,每个场景都有单独的资源

这几种方法可以混合使用

下面时一些打包时的小技巧:

  • 把经常更新和不经常更新的分开打包
  • 把同时加载的对象打成一个包,比如模型和它的材质
  • 如果多个物体使用的是同一个包,可以把这个包单独拿出来打,打到共享包里面
  • 如果只有不到50%的资源包经常在同一时间被加载,那么可以考虑将其拆分
  • 考虑合并较小(小于5到10个资产)但其内容经常同时加载的资产包
  • 如果一组对象只是同一对象的不同版本,那么可以考虑使用后缀名打包

 

Building AssetBundles

Assets/AssetBundles: 这个是打包之后的路径,要确保打包之前创建好

BuildAssetBundleOptions

  • BuildAssetBundleOptions.None: 使用LZMA 压缩格式, 是一个单独的序列化LZMA压缩流 LZMA压缩要求在使用之前对整个包进行解压缩。这可能导致由于解压缩,加载时间会稍微长一些. 值得注意的是,在使用这个BuildAssetBundleOptions时,为了使用包中的任何资产,必须首先解压缩整个包.一旦包已解压缩,它将使用LZ4压缩在磁盘上重新压缩,这并不需要在使用包中的资产之前解压缩整个包,这样从包中使用一个资产就意味着将加载所有资产,打包一个角色或场景的所有资产可能使用这种方式。由于较小的文件大小,只建议在AssetBundle的初始下载时使用LZMA压缩,LZMA 压缩会自动通过nityWebRequestAssetBundle加载且被自动重新通过LZ4压缩,并缓存在本地文件系统上,如果您通过其他方式下载并存储包,则可以使用AssetBundle.RecompressAssetBundleAsync API重新压缩它。

  • BuildAssetBundleOptions.UncompressedAssetBundle: 此包选项以数据完全未压缩的方式构建包. 未压缩的缺点是下载的文件大,但是,下载后的加载时间要快得多。

  • BuildAssetBundleOptions.ChunkBasedCompression: 这个bundle选项使用了一种称为LZ4的压缩方法,这种方法会产生比LZMA更大的压缩文件大小,但与LZMA不同,它不需要在使用前对整个bundle进行解压缩.LZ4使用了一个基于块的算法,它允许以块或“块”的形式加载资源包。 可以直接压需要使用的块使用ChunkBasedCompression的加载时间与未压缩包的加载时间相当,还可以减少磁盘上的大小。

BuildTarget

BuildTarget.Standalone: 可以手动设置构建平台,也可以通过EditorUserBuildSettings.activeBuildTarget 自动获取当前的平台.

打包完之后,你会发现文件并不是你想的那样,只有一个,文件的数量是2*(assetbundle的数量+1)

有的文件包含assetbundle的名字+“.manifest”.这是一个附加的清单,它不和创建的包共享名称,是根据它所在的目录命名的

 AssetBundles 文件,扩展名是 .manifest ,他会在加载包之前加载,它加载就是为了加载包,相当于一个归档文件,这个归档文件的结构可能会根据它是一个AssetBundle还是一个Scene而稍有变化,

 下面是一个正常包的包装清单:

 Scene AssetBundle 可能和普通的 AssetBundles不同,因为它针对场景及其内容的流加载进行了优化。

The Manifest File

对于生成的每个包,都会生成一个关联的清单文件. 清单文件可以用任何文本编辑器打开,其中包含循环冗余检查(CRC)数据和包的依赖项数据等信息。

ManifestFileVersion: 0
CRC: 2422268106
Hashes:
  AssetFileHash:
    serializedVersion: 2
    Hash: 8b6db55a2344f068cf8a9be0a662ba15
  TypeTreeHash:
    serializedVersion: 2
    Hash: 37ad974993dbaa77485dd2a0c38f347a
HashAppended: 0
ClassTypes:
- Class: 91
  Script: {instanceID: 0}
Assets:
  Asset_0: Assets/Mecanim/StateMachine.controller
Dependencies: {}

 

The Manifest Bundle that was generated will have a manifest, but it’ll look more like this:

ManifestFileVersion: 0
AssetBundleManifest:
  AssetBundleInfos:
    Info_0:
      Name: scene1assetbundle
      Dependencies: {}

这将展示资产包之间的关系以及它们的依赖关系。只要理解这个包包含了AssetBundleManifest对象,这个对象对于找出在运行时加载哪个包依赖关系是非常有用的. To learn more about how to use this bundle and the manifest object, see documentation on Using AssetBundles Natively.

 

AssetBundle Dependencies

AssetBundles包是可以相互依赖的,比如一个包里有材质球,它的材质却是另一个包中的,包之间相互依赖的前提是,相互依赖的东西必须都在包内,如果如果依赖的东西不在包内,则会复制一份该物体,打进包内 .如果多个包中的多个对象包含对同一对象的引用,而该对象没有在包内,那么每个依赖于该对象的包都将创建自己的对象副本,并将其打包到构建的AssetBundle中

考虑下面的例子,Bundle 1中的材质引用了Bundle 2中的纹理:

在从Bundle 1加载texture之前,需要将Bundle 2加载到内存中。不管你是按什么顺序加载Bundle 1和Bundle 2,重要的是Bundle 2是在从Bundle 1加载texture之前加载的。在下一节中,我们将讨论如何使用上一节中提到的AssetBundleManifest对象在运行时确定和加载依赖项

 

AssetBundle.LoadFromMemoryAsync

AssetBundle.LoadFromMemoryAsync

这个函数接收一个包含AssetBundle数据的字节数组 ,如果需要,还可以传入CRC值,如果是非零值,则需要在加载之前校验,如果不对,则会报出来。如果包是LZMA压缩的,它将在加载时解压资产包。LZ4压缩包以其压缩状态加载。

using UnityEngine;
using System.Collections;
using System.IO;

public class Example : MonoBehaviour
{
    IEnumerator LoadFromMemoryAsync(string path)
    {
        AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
        yield return createRequest;
        AssetBundle bundle = createRequest.assetBundle;
        var prefab = bundle.LoadAsset<GameObject>("MyObject");
        Instantiate(prefab);
    }
}

AssetBundle.LoadFromFile

AssetBundle.LoadFromFile

这个API在从本地存储加载未压缩包时非常高效.如果包未压缩或块(LZ4)压缩,LoadFromFile将直接从磁盘加载包. 使用此方法加载一个完全压缩(LZMA)的包时,将首先对包进行解压缩,然后再将其加载到内存中。

One example of how to use LoadFromFile:

public class LoadFromFileExample : MonoBehaviour {
    function Start() {
        var myLoadedAssetBundle 
            = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));
        
        if (myLoadedAssetBundle == null) {
            Debug.Log("Failed to load AssetBundle!");
            return;
        }
        var prefab = myLoadedAssetBundle.LoadAsset.<GameObject>("MyObject");
        Instantiate(prefab);
    }
}

适用于Unity 5.4以上版本

 

UnityWebRequestAssetBundle

UnityWebRequestAssetBundle

首先通过UnityWebRequestAssetBundle.GetAssetBundle创建UnityWebRequestAssetBundle. 然后返回 request, 通过 request来下载DownloadHandlerAssetBundle.GetContent(UnityWebRequest)GetContent会返回AssetBundle 对象

 DownloadHandlerAssetBundle.assetbundle在下载了bundle之后,以AssetBundle. loadfromfile的效率加载AssetBundle。

 

IEnumerator InstantiateObject()
{
    string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName; 
    UnityEngine.Networking.UnityWebRequest request 
        = UnityEngine.Networking.UnityWebRequestAssetBundle.GetAssetBundle(uri, 0);
    yield return request.Send();
    AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
    GameObject cube = bundle.LoadAsset<GameObject>("Cube");
    GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
    Instantiate(cube);
    Instantiate(sprite);
}

加载完bundle之后,从里面下载资源

T objectFromBundle = bundleObject.LoadAsset<T>(assetName);

加载资源的方法有:LoadAsset, LoadAllAssets,和他们的异步方式:LoadAssetAsync and LoadAllAssetsAsync

下面是同步的方式:

GameObject gameObject = loadedAssetBundle.LoadAsset<GameObject>(assetName);

To load all Assets:

Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();

异步方法返回一个AssetBundleRequest.在加载资源之前需要等待此操作完成. To load an asset:

AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
yield return request;
var loadedAsset = request.asset;

And

AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync();
yield return request;
var loadedAssets = request.allAssets;

加载完毕后,就可以生成了

Loading AssetBundle Manifests

加载AssetBundle清单非常有用。特别是在处理资源有依赖关系的时候。

要获得一个可用的AssetBundleManifest对象,您需要加载额外的AssetBundle (这个bundle的名字和它的文件夹名字命名一样) 从里面加载一个 AssetBundleManifest类型的对象.

下面是加载方式:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

 你可以通过AssetBundleManifest API 访问其中的对象.

还记得在前面的小节中我们讨论了资源包依赖关系,以及如果一个包依赖于另一个包,那么在从原始包加载任何资产之前,需要如何先加载依赖的包?manifest可以动态查找加载依赖项,比如一个材质球依赖于另一个包里的材质,这样手动找起来可能会很麻烦,如果加载依赖项,就会自动加载

//manifest文件不用加后缀,其他的assetbundle文件需要加后缀
AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("assetBundle"); //Pass the name of the bundle you want the dependencies for.
foreach(string dependency in dependencies)
{
    AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));
}


//个人实验
 public void LoadManifest(string path)
    {
        AssetBundle ab = AssetBundle.LoadFromFile(Application.dataPath + "/AssetsBoundles/AssetsBoundles");//这个地方不要加后缀
        AssetBundleManifest abm = ab.LoadAsset<AssetBundleManifest>("AssetBundleManifest");//固定名
        string[] paths = abm.GetAllDependencies("sss.ab");//你要加载依赖项的asset bundle的名字,是完整路径
        //print(paths.Length);
        foreach (var item in paths)
        {
            //这个地方得到的只是ab包的名字,所以你要加上完整路径
            AssetBundle abs = AssetBundle.LoadFromFile(Application.dataPath + "/AssetsBoundles/"+item);
        }
        LoadABS();//

Managing Loaded AssetBundles

当对象从活动场景中移除时,Unity不会自动卸载它们,. Asset在特定的时间被清除,也可以手动设置清除的时间

知道何时加载和卸载资产包是很重要的。不正确地卸载一个AssetBundle可能导致在内存中复制对象或其他不希望出现的情况,比如丢失纹理。

 最重要的事情是知道什么时候调用AssetBundle management

AssetBundle.UnloadAllAssetBundles(bool);. Unload 是一个非静态方法,它会卸载你的AssetBundle. 这个API卸载被调用的AssetBundle的头信息。该参数表示是否也卸载从该AssetBundle加载的所有对象。比如LoadAsset的资源

AssetBundle.Unload(true) 卸载从AssetBundle中加载的所有gameobject(包括依赖项),这并不包括复制的GameObjects(例如实例化的GameObjects),因为它们不再属于AssetBundle.当这种情况发生时,从AssetBundle中加载的texture(仍然属AssetBundle)将从场景中的GameObjects中消失,Unity将它们视为丢失的纹理。

假设 Material M 从 AssetBundle AB 中加载

如果AB.Unload(true) 被调用. M在活动场景中的任何实例也将被卸载和销毁。

如果改为调用AB. unload (false),它将打破M和AB的当前实例链

 如果为false的话,卸载的是assetbundle,如果这个ab包重新被加载,且M被重新Load,, Unity也不会把现有的M和ab包连接起来,只会重新拷贝一个

 大多数项目应该使用AssetBundle.Unload(true)来避免在内存中复制对象。

   大多数项目应该使用AssetBundle.Unload(true),并采用一种确保对象不重复的方法。两种常见的方法是:

  • 在应用程序的生命周期中具有定义良好的点,在这些点上可以卸载临时的资产包,例如在关卡之间或在加载屏幕期间。.

  • 当一个物体的所有引用都不被使用时,卸载它

如果应用程序必须使用AssetBundle.Unload(false),则只能以两种方式卸载单个对象:

 

 

Patching with AssetBundles

 向UnityWebRequestAssetBundle 传递不同的参数,就能下载不同版本的资源包

补丁系统中比较难解决的问题是检测需要替换哪些资产包。一个补丁系统需要两个信息列表:

  •  一个列表是当前已下载的AssetBundle,和它们的版本信息
  • 一个列表是服务器上的AssetBundles,和他们的版本信息

补丁程序应该下载服务器端的AssetBundle列表并和当前的AssetBundles比较.丢失的AssetBundles,或者版本信息已经更改的AssetBundles,应该重新下载。

 

Troubleshooting

本节描述了在使用资产包的项目中经常出现的几个问题。

Asset Duplication:资源重复

如果两个不同的包共统一用了一个单独的bundle,那么这个bundle会被复制两次,这会增加内存

解决方法:

  1. 确保不同bundle包中的对象不存在共享的bundle包. 任何共享依赖项的对象都可以放在同一个AssetBundle中,而不需要复制它们的依赖项,如果两个bundle包,依赖同一个bundle包,那么只加载一次依赖的包就可以了,,共享的资源一定要单独打包成一个包

    • 此方法通常不适用于具有许多共享依赖项的项目。它会生成大量的资产包,这些资产包必须频繁地重新构建和重新下载,这既不方便也不高效。
  2. 分段资产包,这样就不会同时加载两个共享依赖项的资产包。

    • 这种方法可能适用于某些类型的项目,例如基于关卡的游戏。但是,它仍然不必要地增加了项目的资产包的大小,并且增加了构建时间和加载时间。
  3. 确保所有依赖项资产都构建到它们自己的资产包中.

在Unity 5中,对象依赖关系是通过UnityEditor中的AssetDatabase API来跟踪的。这个API只能在Unity编辑器下使用,而不能在运行时使用,AssetDatabase.GetDependencies可以用来定位特定对象或资产的所有直接依赖项.注意,这些依赖项可能有自己的依赖项。此外,AssetImporter API可用于查询任何指定对象的AssetBundle。

强烈建议通过 AssetDatabase and AssetImporter APIs,编写一个编辑器脚本,来追踪一个bundle包的所有的间接或者直接依赖项, 或者查找两个bundle包的共享bundle,是不是一个单独的包

Sprite Atlas Duplication:图集重复

要确保sprite atlases 没有被复制,检查所有的sprites 标签是不是打包到同一个图集下,并且打包到同一个bundle下

自动生成的 sprite atlases 永远不会被打包成bundle包,如果它里面的sprite被打包成bundle或者有bundle包引用它里面的sprite,它就会 包含在其中

建议使用5.2版本以上的unity

Android Textures

由于Android生态系统中严重的设备碎片化,通常需要将纹理压缩成几种不同的格式. 而所有的Android设备都支持ETC1, ETC1 不支持texture的透明通道. 如果一个设备不需要 OpenGL ES 2 的支持, 最好的方法是使用 ETC2, 所有 Android OpenGL ES 3 的设备都支持

但是很多应用程序需要在不支持ETC2的设备上使用,一个方法是使用 Unity 5’s AssetBundle 的后缀名. (Please see Unity’s Android optimization guide for details on other options.)

使用AssetBundle 的后缀变量名, 所有不能使用ETC1进行压缩的纹理被单独打包到只含有texture的bundle包中,然后使用特定的压缩格式,来支持不支持ETC2的设备,这些压缩格式比如: DXT5, PVRTC, ATITC. 将textures’ TextureImporter设置成根据后缀变量名更改

运行时,可以用 SystemInfo.SupportsTextureFormat API来检测对不同材质压缩格式的支持情况

More information on Android texture compression formats can be found here.

iOS File Handle Overuse

The issue described in the following section was fixed in Unity 5.3.2p2. Current versions of Unity are not affected by this issue.

In versions prior to Unity 5.3.2p2, Unity would hold an open file handle to an AssetBundle the entire time that the AssetBundle is loaded. This is not a problem on most platforms. However, iOS
limits the number of file handles a process may simultaneously have open to 255. If loading an AssetBundle causes this limit to be exceeded, the loading call will fail with a “Too Many Open File Handles” error.

This was a common problem for projects trying to divide their content across many hundreds or thousands of AssetBundles.

For projects unable to upgrade to a patched version of Unity, temporary solutions are:

  • Reducing the number of AssetBundles in use by merging related AssetBundles

Using AssetBundle.Unload(false) to close an AssetBundle’s file handle, and managing the loaded Objects’ lifecycles manually

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TO_ZRG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值