关于AssetBundle
AssetBundle可用来将多个资源打包为一个文件,实现动态下载和更新。需要注意的是Unity3D5.x以后对打包方式进行了升级,不用再在依赖关系上伤透脑筋,但是和4.x的版本不再兼容,不过我的这篇笔记是基于4.x的。
打包资源
Unity对AssetBundle仅提供了代码方面的支持,并没有一个菜单或窗口可以直接进行操作,不过这样却给了我们最大的开放度来进行资源打包。
我们看看打包的代码:
static function BuildAssetBundle (mainAsset : Object, assets : Object[], pathName : string, options : BuildAssetBundleOptions = BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, targetPlatform : BuildTarget = BuildTarget.WebPlayer) : bool
mainAsset:主资源,可以通过assetBundle.mainAsset直接获取,如果没有填null;
assets:打包的所有资源,可以通过assetBundle.Load获取,如果没有填null;
pathName:打包后的资源文件路径;
options:打包选项
- BuildAssetBundleOptions.CollectDependencies:搜集所有依赖项,将依赖项打包到资源中;
- BuildAssetBundleOptions.CompleteAssets:完全打包资源,如果传递网格它还将包括游戏物体和任意动画剪辑;
- BuildAssetBundleOptions.DeterministicAssetBundle:编译资源包使用一个哈希表储存对象ID在资源包中;
- BuildAssetBundleOptions.DisableWriteTypeTree:在资源包不包含类型信息;
- BuildAssetBundleOptions.UncompressedAssetBundle:不压缩AssetBundle,默认会进行压缩;
targetPlatform:打包资源的目标平台,不同平台的资源是不通用的,需要额外打包。
其它的打包方法
static function BuildAssetBundleExplicitAssetNames (assets : Object[], assetNames : string[],pathName : string, options : BuildAssetBundleOptions = BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, targetPlatform : BuildTarget = BuildTarget.WebPlayer) : bool
可以为每个资源指定特殊的名称。
static function BuildPlayer (levels : string[], locationPathName : string, target : BuildTarget, options : BuildOptions) : string
生成一个流unity3d文件。这包含一个场景,可以按需下载和加载,一旦它的资源包已被加载。
static function BuildStreamedSceneAssetBundle (levels : string[], locationPath : String, target : BuildTarget) : String
编译一个或多个场景和所有它依赖的压缩资源包。
打包脚本
这里提供一个可以进行简单打包的脚本:
1 using UnityEditor; 2 using UnityEngine; 3 4 /// <summary> 5 /// 6 /// 简单的 AssetBundle 创建类. 7 /// 8 /// 解决问题: 9 /// 实现简单的资源打包. 10 /// 11 /// 使用方法及步骤: 12 /// 1.修改要打包到的目标平台枚举; 13 /// 2.选中要打包的文件在菜单栏点击对应的功能菜单. 14 /// 15 /// </summary> 16 public class SimpleCreateAssetBundle 17 { 18 /// <summary> 19 /// 打包的目标平台. 20 /// </summary> 21 private const BuildTarget BUILD_TARGET = BuildTarget.StandaloneWindows64; 22 23 /// <summary> 24 /// 将选定的一个对象进行打包, 同时包含依赖项, 可通过 AssetBundle 的 main 属性获取. 25 /// </summary> 26 [MenuItem("Hammerc/AssetBundle/CreateSingleAssetBundle")] 27 private static void CreateSingleAssetBundle() 28 { 29 if(Selection.activeObject != null) 30 { 31 //显示保存窗口 32 string path = EditorUtility.SaveFilePanel("Create Single AssetBundle:", "", "New AssetBundle", "unity3d"); 33 34 if(path.Length > 0) 35 { 36 //打包 37 BuildPipeline.BuildAssetBundle(Selection.activeObject, null, path, BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BUILD_TARGET); 38 } 39 } 40 } 41 42 /// <summary> 43 /// 将选定的多个对象进行打包, 同时包含依赖项, 不指定 AssetBundle 的 main 属性获取. 44 /// </summary> 45 [MenuItem("Hammerc/AssetBundle/CreateMultipleAssetBundle")] 46 private static void CreateMultipleAssetBundle() 47 { 48 if(Selection.objects.Length > 0) 49 { 50 //显示保存窗口 51 string path = EditorUtility.SaveFilePanel("Create Multiple AssetBundle:", "", "New AssetBundle", "unity3d"); 52 53 if(path.Length > 0) 54 { 55 //打包 56 BuildPipeline.BuildAssetBundle(null, Selection.objects, path, BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BUILD_TARGET); 57 } 58 } 59 } 60 }
加载和测试AssetBundle
新建一个工程,我添加了一个Cube的预制件(MyBox),Cube添加了一个材质对象(MyBoxMaterial)和一个简单的用来移动的脚本对象(MyBoxScript)。
下面选中MyBox后使用上面脚本的CreateSingleAssetBundle打包为StreamingAssets文件夹下的SingleAssetBundle.unity3d文件。
我们添加一个脚本SceneScript到场景的摄像机上。
1 using UnityEngine; 2 using System.Collections; 3 4 public class SceneScript : MonoBehaviour 5 { 6 void Start() 7 { 8 this.StartCoroutine(LoadAssetBundle()); 9 } 10 11 IEnumerator LoadAssetBundle() 12 { 13 //格式化路径 14 string url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "SingleAssetBundle.unity3d"); 15 16 //开始加载 17 WWW www = new WWW(url); 18 yield return www; 19 20 //加载失败 21 if(www.error != null) 22 { 23 Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error); 24 yield break; 25 } 26 27 AssetBundle bundle = www.assetBundle; 28 29 //这里打上断点可以查看 bundle 中的所有对象 30 Object[] objs = bundle.LoadAll(); 31 32 //获取资源, 注意该资源不能直接使用 33 GameObject go = bundle.mainAsset as GameObject; 34 //创建实例并添加到场景 35 Instantiate(go); 36 37 //销毁资源但保留已经实例化的资源 38 bundle.Unload(false); 39 } 40 }
运行可以发现小盒子已经被添加到场景中了,我们打个断点看看AssetBundle中都有啥:
我们发现和MyBox关联的对象都一道被打包进去了。
下面我添加了一个球型的预制件(MySphere),同时将已有的材质(MyBoxMaterial)和脚本(MyBoxScript)都添加到该预制件中。
选中MyBox和MySphere后使用CreateMultipleAssetBundle打包为StreamingAssets文件夹下的MultipleAssetBundle.unity3d文件。
我们修改一下SceneScript脚本,分别加载一下这两个对象。
1 using UnityEngine; 2 using System.Collections; 3 4 public class SceneScript : MonoBehaviour 5 { 6 void Start() 7 { 8 this.StartCoroutine(LoadAssetBundle()); 9 } 10 11 IEnumerator LoadAssetBundle() 12 { 13 //格式化路径 14 string url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "MultipleAssetBundle.unity3d"); 15 16 //开始加载 17 WWW www = new WWW(url); 18 yield return www; 19 20 //加载失败 21 if(www.error != null) 22 { 23 Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error); 24 yield break; 25 } 26 27 AssetBundle bundle = www.assetBundle; 28 29 //这里打上断点可以查看 bundle 中的所有对象 30 Object[] objs = bundle.LoadAll(); 31 32 //获取资源, 注意该资源不能直接使用 33 GameObject go1 = bundle.Load("MyBox", typeof(GameObject)) as GameObject; 34 //创建实例并添加到场景 35 Instantiate(go1, new Vector3(3, 0, 0), Quaternion.identity); 36 37 //获取资源, 注意该资源不能直接使用 38 GameObject go2 = bundle.Load("MySphere", typeof(GameObject)) as GameObject; 39 //创建实例并添加到场景 40 Instantiate(go2, new Vector3(-3, 0, 0), Quaternion.identity); 41 42 //销毁资源但保留已经实例化的资源 43 bundle.Unload(false); 44 } 45 }
我们接着打个断点看看究竟打包了啥到AssetBundle中:
总结一下:
- 打包到AssetBundle的资源仅保留名称,路径和后缀都会被去掉;
- 每个AssetBundle中可能会出现多个同名对象(但类型会不同),所以使用Load加载时务必加上类型以减少不必要的麻烦;
- 空字符串一般都是一个引用类型(ReferenceData),之所以搞成空字符串是为了不让我们可以取到?如下图所示:
通过名称可以发现是脚本(MyBoxScript)的一个引用类型,难道脚本不一起打包而只是用引用引用一下?我们做一个测试一看便知。
关于AssetBundle中的脚本
我们保留之前打包出的AssetBundle文件,将Asset文件夹中的MyBoxScript文件删除,再次运行,会发现脚本提供的功能没有了,同时会报错,如下:
说明脚本文件不会打包到AssetBundle文件中,仅仅是保留一个引用而已。
但是,即便AssetBundle中使用到的脚本在别的地方没有被引用到,运行和打包后任然可以正常运行,所以我的揣测如下:
Unity中的脚本貌似除了Editor文件夹中的不会被打包,其它文件夹的脚本都会被打包,所以AssetBundle中的脚本虽然只是一个引用,但只要程序中存在同名的脚本即可。
依赖关系
一般在实际开发中,我们会将资源进行分类打包,下面引用下文章Unity AssetBundle爬坑手记的内容。
在打包的时候,我们需要对包的大小和数量进行一个平衡,所有资源打成一个包,一个资源打一个包,都是比较极端的做法,他们的问题也很明显,更多情况下我们需要灵活地将他们组合起来打成一个包的缺点是加载了这个包,我们不需要的东西也会被加载进来,占用额外内存,而且不利于热更新打成多个包的缺点是,容易造成冗余,首先影响包的读取速度,然后包之间的内容可能会有重复,且太多的包不利于资源管理哪些模块打成一个包,哪些模块打成多个包,需要根据实际情况来,例如游戏中每个怪物都需要打成一个包,因为每个怪物之间是独立的,例如游戏的基础UI,可以打成一个包,因为他们在各个界面都会出现PS.想打包进AssetBundle中的二进制文件,文件名的后缀必须为“.bytes”
那么打成多个包就会出现数据冗余的问题,比如按我们上面的示例来看,如果我要将MyBox和MySphere分别打成两个包,那么相同的数据MyBoxMaterial和MyBoxScript在两个包中都会存在,出现了多余不必要的数据。
解决这个问题的办法就是将公共部分打包成一个资源,然后让MyBox和MySphere分别都依赖这个公共包,这样就不会出现数据冗余的问题
我们使用BuildPipeline.PushAssetDependencies()和BuildPipeline.PopAssetDependencies()来开启Bundle之间的依赖关系,当我们调用PushAssetDependencies之后,会开启依赖模式,当我们依次打包 A B C时,如果A包含了B的资源,B就不会再包含这个资源,而是直接依赖A的,如果A和B包含了C的资源,那么C的这个资源旧不会被打包进去,而是依赖A和B。这时候只要有同样的资源,就会向前依赖,当我们希望,B和C依赖A,但B和C之间不互相依赖,就需要嵌套Push Pop了,当我们调用PopAssetDependencies就会结束依赖
打包的代码如下(有详尽的注释):
1 using UnityEditor; 2 using UnityEngine; 3 4 /// <summary> 5 /// 数据打包脚本. 6 /// </summary> 7 public class CreateAssetBundle 8 { 9 /// <summary> 10 /// 打包的目标平台. 11 /// </summary> 12 private const BuildTarget BUILD_TARGET = BuildTarget.StandaloneWindows64; 13 14 /// <summary> 15 /// 使用依赖分别打包出公共数据资源, 小盒资源和小球资源. 16 /// </summary> 17 [MenuItem("Hammerc/AssetBundle/CreateAssetBundleFile")] 18 private static void CreateAssetBundleFile() 19 { 20 //压入依赖项 21 BuildPipeline.PushAssetDependencies(); 22 23 //先打包共同数据, 第一个打包的数据会全部进行打包 24 Object[] assets = new[] 25 { 26 AssetDatabase.LoadMainAssetAtPath("Assets/Res/Material/MyBoxMaterial.mat"), 27 AssetDatabase.LoadMainAssetAtPath("Assets/Res/Script/MyBoxScript.cs"), 28 }; 29 BuildPipeline.BuildAssetBundle(null, assets, Application.streamingAssetsPath + "/Common.unity3d", BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets | BuildAssetBundleOptions.DeterministicAssetBundle, BUILD_TARGET); 30 31 //再次压入依赖项, 仍然依赖上一层的依赖项 32 BuildPipeline.PushAssetDependencies(); 33 34 //打包小盒资源, 该资源会依赖于 Common.unity3d 35 assets = new[] 36 { 37 AssetDatabase.LoadMainAssetAtPath("Assets/Res/MyBox.prefab"), 38 }; 39 BuildPipeline.BuildAssetBundle(null, assets, Application.streamingAssetsPath + "/MyBox.unity3d", BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets | BuildAssetBundleOptions.DeterministicAssetBundle, BUILD_TARGET); 40 41 //弹出依赖项, 表明下面的打包不会依赖 MyBox.unity3d 但会继续依赖 Common.unity3d 42 BuildPipeline.PopAssetDependencies(); 43 44 //打包小球资源, 该资源会依赖于 Common.unity3d 45 assets = new[] 46 { 47 AssetDatabase.LoadMainAssetAtPath("Assets/Res/MySphere.prefab"), 48 }; 49 BuildPipeline.BuildAssetBundle(null, assets, Application.streamingAssetsPath + "/MySphere.unity3d", BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets | BuildAssetBundleOptions.DeterministicAssetBundle, BUILD_TARGET); 50 51 //弹出依赖项, 所有依赖都弹出, 如果接下来继续打包资源则不会依赖任何已经打包的资源 52 BuildPipeline.PopAssetDependencies(); 53 } 54 }
我们只要点击菜单栏对应的菜单项就能生成3个打包后的资源文件,分别是Common.unity3d、MyBox.unity3d和MySphere.unity3d。
加载资源
当我们的Bundle之间有了依赖之后,就不能像前面那样简单地直接Load对应的Bundle了,我们需要把Bundle所依赖的Bundle先加载进来,这个加载只是WWW或者LoadFromCacheOrDownload,并不需要对这个Bundle进行Load,如果BundleB依赖BundleA,当我们要加载BundleB的资源时,假设BundleA没有被加载进来,或者已经被Unload了,那么BundleB依赖BundleA的部分就会丢失,例如每个正方体上都挂着一个脚本,当我们不嵌套Push Pop时,单个正方体的Bundle没有被加载或者已经被卸载,我们加载的那组正方体上的脚本就会丢失,脚本也是一种资源,当一个脚本已经被打包了,依赖这个包的资源,就不会被再打进去
下面我们创建一个SceneScript2的脚本挂到场景的摄像机上,脚本如下:
1 using UnityEngine; 2 using System.Collections; 3 4 public class SceneScript2 : MonoBehaviour 5 { 6 void Start() 7 { 8 this.StartCoroutine(LoadAssetBundle()); 9 } 10 11 IEnumerator LoadAssetBundle() 12 { 13 // ----- 公共数据 ----- 14 15 //格式化路径 16 string url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "Common.unity3d"); 17 18 //开始加载 19 WWW www = new WWW(url); 20 yield return www; 21 22 //加载失败 23 if (www.error != null) 24 { 25 Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error); 26 yield break; 27 } 28 29 AssetBundle bundle = www.assetBundle; 30 AssetBundle commonBundle = bundle; 31 32 //这里打上断点可以查看 Common 中的所有对象 33 Object[] objs = bundle.LoadAll(); 34 35 // ----- MyBox ----- 36 37 //格式化路径 38 url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "MyBox.unity3d"); 39 40 //开始加载 41 www = new WWW(url); 42 yield return www; 43 44 //加载失败 45 if (www.error != null) 46 { 47 Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error); 48 yield break; 49 } 50 51 bundle = www.assetBundle; 52 53 //这里打上断点可以查看 MyBox 中的所有对象 54 objs = bundle.LoadAll(); 55 56 //获取资源, 注意该资源不能直接使用 57 GameObject go1 = bundle.Load("MyBox", typeof(GameObject)) as GameObject; 58 //创建实例并添加到场景 59 Instantiate(go1, new Vector3(3, 0, 0), Quaternion.identity); 60 61 //销毁资源但保留已经实例化的资源 62 bundle.Unload(false); 63 64 // ----- MySphere ----- 65 66 //格式化路径 67 url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "MySphere.unity3d"); 68 69 //开始加载 70 www = new WWW(url); 71 yield return www; 72 73 //加载失败 74 if (www.error != null) 75 { 76 Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error); 77 yield break; 78 } 79 80 bundle = www.assetBundle; 81 82 //这里打上断点可以查看 MyBox 中的所有对象 83 objs = bundle.LoadAll(); 84 85 //获取资源, 注意该资源不能直接使用 86 GameObject go2 = bundle.Load("MySphere", typeof(GameObject)) as GameObject; 87 //创建实例并添加到场景 88 Instantiate(go2, new Vector3(-3, 0, 0), Quaternion.identity); 89 90 //销毁资源但保留已经实例化的资源 91 bundle.Unload(false); 92 93 //销毁公共资源但保留已经实例化的资源 94 commonBundle.Unload(false); 95 } 96 }
运行一下会看见两个对象都出现并正常运行了,我们接下来看看每个资源的数据情况:
公共资源:
MyBox:
MySphere:
看起来好像公共数据也被打包到MyBox和MySphere资源中了,其实没有,如果我们把公共数据的加载去掉,在运行时会出现丢失数据的报错。
更新依赖
在打包的时候我们需要指定BuildAssetBundleOptions.DeterministicAssetBundle选项,这个选项会为每个资源生成一个唯一的ID,当这个资源被重新打包的时候,确定这个ID不会改变,包的依赖是根据这个ID来的,使用这个选项的好处是,当资源需要更新时,依赖于该资源的其他资源,不需要重新打包A -> B -> C当A依赖B依赖C时,B更新,需要重新打包C,B,而A不需要动,打包C的原因是,因为B依赖于C,如果不打包C,直接打包B,那么C的资源就会被重复打包,而且B和C的依赖关系也会断掉
内存及其它
写不出新花样了,大家直接点这里吧:Unity AssetBundle爬坑手记
最后提供一下工程文件:http://pan.baidu.com/s/1jGDzshS