关于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

编译一个或多个场景和所有它依赖的压缩资源包。

打包脚本

这里提供一个可以进行简单打包的脚本:

 using UnityEditor;
using UnityEngine; /// <summary>
///
/// 简单的 AssetBundle 创建类.
///
/// 解决问题:
/// 实现简单的资源打包.
///
/// 使用方法及步骤:
/// 1.修改要打包到的目标平台枚举;
/// 2.选中要打包的文件在菜单栏点击对应的功能菜单.
///
/// </summary>
public class SimpleCreateAssetBundle
{
/// <summary>
/// 打包的目标平台.
/// </summary>
private const BuildTarget BUILD_TARGET = BuildTarget.StandaloneWindows64; /// <summary>
/// 将选定的一个对象进行打包, 同时包含依赖项, 可通过 AssetBundle 的 main 属性获取.
/// </summary>
[MenuItem("Hammerc/AssetBundle/CreateSingleAssetBundle")]
private static void CreateSingleAssetBundle()
{
if(Selection.activeObject != null)
{
//显示保存窗口
string path = EditorUtility.SaveFilePanel("Create Single AssetBundle:", "", "New AssetBundle", "unity3d"); if(path.Length > )
{
//打包
BuildPipeline.BuildAssetBundle(Selection.activeObject, null, path, BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BUILD_TARGET);
}
}
} /// <summary>
/// 将选定的多个对象进行打包, 同时包含依赖项, 不指定 AssetBundle 的 main 属性获取.
/// </summary>
[MenuItem("Hammerc/AssetBundle/CreateMultipleAssetBundle")]
private static void CreateMultipleAssetBundle()
{
if(Selection.objects.Length > )
{
//显示保存窗口
string path = EditorUtility.SaveFilePanel("Create Multiple AssetBundle:", "", "New AssetBundle", "unity3d"); if(path.Length > )
{
//打包
BuildPipeline.BuildAssetBundle(null, Selection.objects, path, BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BUILD_TARGET);
}
}
}
}

加载和测试AssetBundle

新建一个工程,我添加了一个Cube的预制件(MyBox),Cube添加了一个材质对象(MyBoxMaterial)和一个简单的用来移动的脚本对象(MyBoxScript)。

下面选中MyBox后使用上面脚本的CreateSingleAssetBundle打包为StreamingAssets文件夹下的SingleAssetBundle.unity3d文件。

我们添加一个脚本SceneScript到场景的摄像机上。

 using UnityEngine;
using System.Collections; public class SceneScript : MonoBehaviour
{
void Start()
{
this.StartCoroutine(LoadAssetBundle());
} IEnumerator LoadAssetBundle()
{
//格式化路径
string url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "SingleAssetBundle.unity3d"); //开始加载
WWW www = new WWW(url);
yield return www; //加载失败
if(www.error != null)
{
Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error);
yield break;
} AssetBundle bundle = www.assetBundle; //这里打上断点可以查看 bundle 中的所有对象
Object[] objs = bundle.LoadAll(); //获取资源, 注意该资源不能直接使用
GameObject go = bundle.mainAsset as GameObject;
//创建实例并添加到场景
Instantiate(go); //销毁资源但保留已经实例化的资源
bundle.Unload(false);
}
}

运行可以发现小盒子已经被添加到场景中了,我们打个断点看看AssetBundle中都有啥:

我们发现和MyBox关联的对象都一道被打包进去了。

下面我添加了一个球型的预制件(MySphere),同时将已有的材质(MyBoxMaterial)和脚本(MyBoxScript)都添加到该预制件中。

选中MyBox和MySphere后使用CreateMultipleAssetBundle打包为StreamingAssets文件夹下的MultipleAssetBundle.unity3d文件。

我们修改一下SceneScript脚本,分别加载一下这两个对象。

 using UnityEngine;
using System.Collections; public class SceneScript : MonoBehaviour
{
void Start()
{
this.StartCoroutine(LoadAssetBundle());
} IEnumerator LoadAssetBundle()
{
//格式化路径
string url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "MultipleAssetBundle.unity3d"); //开始加载
WWW www = new WWW(url);
yield return www; //加载失败
if(www.error != null)
{
Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error);
yield break;
} AssetBundle bundle = www.assetBundle; //这里打上断点可以查看 bundle 中的所有对象
Object[] objs = bundle.LoadAll(); //获取资源, 注意该资源不能直接使用
GameObject go1 = bundle.Load("MyBox", typeof(GameObject)) as GameObject;
//创建实例并添加到场景
Instantiate(go1, new Vector3(, , ), Quaternion.identity); //获取资源, 注意该资源不能直接使用
GameObject go2 = bundle.Load("MySphere", typeof(GameObject)) as GameObject;
//创建实例并添加到场景
Instantiate(go2, new Vector3(-, , ), Quaternion.identity); //销毁资源但保留已经实例化的资源
bundle.Unload(false);
}
}

我们接着打个断点看看究竟打包了啥到AssetBundle中:

总结一下:

  1. 打包到AssetBundle的资源仅保留名称,路径和后缀都会被去掉;
  2. 每个AssetBundle中可能会出现多个同名对象(但类型会不同),所以使用Load加载时务必加上类型以减少不必要的麻烦;
  3. 空字符串一般都是一个引用类型(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就会结束依赖

打包的代码如下(有详尽的注释):

 using UnityEditor;
using UnityEngine; /// <summary>
/// 数据打包脚本.
/// </summary>
public class CreateAssetBundle
{
/// <summary>
/// 打包的目标平台.
/// </summary>
private const BuildTarget BUILD_TARGET = BuildTarget.StandaloneWindows64; /// <summary>
/// 使用依赖分别打包出公共数据资源, 小盒资源和小球资源.
/// </summary>
[MenuItem("Hammerc/AssetBundle/CreateAssetBundleFile")]
private static void CreateAssetBundleFile()
{
//压入依赖项
BuildPipeline.PushAssetDependencies(); //先打包共同数据, 第一个打包的数据会全部进行打包
Object[] assets = new[]
{
AssetDatabase.LoadMainAssetAtPath("Assets/Res/Material/MyBoxMaterial.mat"),
AssetDatabase.LoadMainAssetAtPath("Assets/Res/Script/MyBoxScript.cs"),
};
BuildPipeline.BuildAssetBundle(null, assets, Application.streamingAssetsPath + "/Common.unity3d", BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets | BuildAssetBundleOptions.DeterministicAssetBundle, BUILD_TARGET); //再次压入依赖项, 仍然依赖上一层的依赖项
BuildPipeline.PushAssetDependencies(); //打包小盒资源, 该资源会依赖于 Common.unity3d
assets = new[]
{
AssetDatabase.LoadMainAssetAtPath("Assets/Res/MyBox.prefab"),
};
BuildPipeline.BuildAssetBundle(null, assets, Application.streamingAssetsPath + "/MyBox.unity3d", BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets | BuildAssetBundleOptions.DeterministicAssetBundle, BUILD_TARGET); //弹出依赖项, 表明下面的打包不会依赖 MyBox.unity3d 但会继续依赖 Common.unity3d
BuildPipeline.PopAssetDependencies(); //打包小球资源, 该资源会依赖于 Common.unity3d
assets = new[]
{
AssetDatabase.LoadMainAssetAtPath("Assets/Res/MySphere.prefab"),
};
BuildPipeline.BuildAssetBundle(null, assets, Application.streamingAssetsPath + "/MySphere.unity3d", BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets | BuildAssetBundleOptions.DeterministicAssetBundle, BUILD_TARGET); //弹出依赖项, 所有依赖都弹出, 如果接下来继续打包资源则不会依赖任何已经打包的资源
BuildPipeline.PopAssetDependencies();
}
}

我们只要点击菜单栏对应的菜单项就能生成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的脚本挂到场景的摄像机上,脚本如下:

 using UnityEngine;
using System.Collections; public class SceneScript2 : MonoBehaviour
{
void Start()
{
this.StartCoroutine(LoadAssetBundle());
} IEnumerator LoadAssetBundle()
{
// ----- 公共数据 ----- //格式化路径
string url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "Common.unity3d"); //开始加载
WWW www = new WWW(url);
yield return www; //加载失败
if (www.error != null)
{
Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error);
yield break;
} AssetBundle bundle = www.assetBundle;
AssetBundle commonBundle = bundle; //这里打上断点可以查看 Common 中的所有对象
Object[] objs = bundle.LoadAll(); // ----- MyBox ----- //格式化路径
url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "MyBox.unity3d"); //开始加载
www = new WWW(url);
yield return www; //加载失败
if (www.error != null)
{
Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error);
yield break;
} bundle = www.assetBundle; //这里打上断点可以查看 MyBox 中的所有对象
objs = bundle.LoadAll(); //获取资源, 注意该资源不能直接使用
GameObject go1 = bundle.Load("MyBox", typeof(GameObject)) as GameObject;
//创建实例并添加到场景
Instantiate(go1, new Vector3(, , ), Quaternion.identity); //销毁资源但保留已经实例化的资源
bundle.Unload(false); // ----- MySphere ----- //格式化路径
url = string.Format("file:///{0}/{1}", Application.streamingAssetsPath, "MySphere.unity3d"); //开始加载
www = new WWW(url);
yield return www; //加载失败
if (www.error != null)
{
Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error);
yield break;
} bundle = www.assetBundle; //这里打上断点可以查看 MyBox 中的所有对象
objs = bundle.LoadAll(); //获取资源, 注意该资源不能直接使用
GameObject go2 = bundle.Load("MySphere", typeof(GameObject)) as GameObject;
//创建实例并添加到场景
Instantiate(go2, new Vector3(-, , ), Quaternion.identity); //销毁资源但保留已经实例化的资源
bundle.Unload(false); //销毁公共资源但保留已经实例化的资源
commonBundle.Unload(false);
}
}

运行一下会看见两个对象都出现并正常运行了,我们接下来看看每个资源的数据情况:

公共资源:

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

Unity3D4.x之AssetBundle学习笔记的更多相关文章

  1. 热更新基础--AssetBundle学习笔记

    一.简介 AssetBundle简称AB包,特定平台的资产压缩包(包括模型.贴图.预设体.音效.材质球等资产). 作用:Resources下的资源只读且打包后不可修改,而AB包存储位置自定,后期可以动 ...

  2. 热更新解决方案--xlua学习笔记

    一.热更新方案简介 在Unity游戏工程中,C#代码(编译型语言)资源和Resources文件夹下的资源打包后都不可以更改,因此这部分内容不能进行热更新,而lua代码(解释型语言)逻辑不需要进行预编译 ...

  3. 热更新解决方案--tolua学习笔记

    一.tolua使用准备工作:从GitHub上下载tolua(说明:这篇笔记使用的Unity版本是2019.4.18f1c1,使用的tolua是2021年4月9日从GitHub上Clone的tolua工 ...

  4. Unity3D之UGUI学习笔记(一):UGUI介绍以及Canvas

    UGUI是Unity3D4.6官方提供的UI系统,支持2D和3D UI的开发. Unity3D UI史 OnGUI 在Unity4.6之前,官方提供的是OnGUI函数来开发UI界面,当然问题也比较多, ...

  5. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  6. PHP-自定义模板-学习笔记

    1.  开始 这几天,看了李炎恢老师的<PHP第二季度视频>中的“章节7:创建TPL自定义模板”,做一个学习笔记,通过绘制架构图.UML类图和思维导图,来对加深理解. 2.  整体架构图 ...

  7. PHP-会员登录与注册例子解析-学习笔记

    1.开始 最近开始学习李炎恢老师的<PHP第二季度视频>中的“章节5:使用OOP注册会员”,做一个学习笔记,通过绘制基本页面流程和UML类图,来对加深理解. 2.基本页面流程 3.通过UM ...

  8. 2014年暑假c#学习笔记目录

    2014年暑假c#学习笔记 一.C#编程基础 1. c#编程基础之枚举 2. c#编程基础之函数可变参数 3. c#编程基础之字符串基础 4. c#编程基础之字符串函数 5.c#编程基础之ref.ou ...

  9. JAVA GUI编程学习笔记目录

    2014年暑假JAVA GUI编程学习笔记目录 1.JAVA之GUI编程概述 2.JAVA之GUI编程布局 3.JAVA之GUI编程Frame窗口 4.JAVA之GUI编程事件监听机制 5.JAVA之 ...

随机推荐

  1. java:比较对象

    对象内容相等条件:1.对象类型相同(可用instanceof操作符比较)2.对象的成员变量的值完全相同 instanceof 判断对象类型 //a是否为Child对象类型 boolean b = a ...

  2. JVM运行机制

    JVM启动流程

  3. Git教程(4)常用小技巧之标签

    Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated). 一个轻量标签很像一个不会改变的分支 - 它只是一个特定提交的引用. 然而,附注标签是存储在 Git ...

  4. Android开发之parseSdkContent failed Could not initialize class android.graphics.Typeface

    在进行android开发过程中,忽然发现经常弹出来parseSdkContent failed 这个错误,然后google了下解决办法 方法1: 删除.android文件 重启eclipse. 该方法 ...

  5. struts2的action中获得request response session 对象

    在struts2中有两种方式可以得到这些对象 1.非IoC方式 要获得上述对象,关键Struts 2中com.opensymphony.xwork2.ActionContext类.我们可以通过它的静态 ...

  6. FormsAuthentication实现单点登录

    原文地址:http://www.wlm.so/Article/Detail/lmb48bk9f690n00000 单点登录,这种在网络非常常见,在这里讨论的是实现同一主域下的子站间的单点登录,同样也适 ...

  7. SQL、LINQ、Lambda 三种用法

    SQL   LinqToSql   Lambda 1. 查询Student表中的所有记录的Sname.Ssex和Class列.select sname,ssex,class from studentL ...

  8. JDK版本1.6和6.0到底指什么

    Both version numbers (1.6.0 and 6) are used to identify this release of the Java Platform. Version 6 ...

  9. 学习面试题Day05

    1.如何理解数组在Java中作为一个类? Java的数组本质上是一个类,该类还保存了数据类型的信息.该类通过成员变量的形式来保存数据,并且通过[]符号,使用下标来访问这些数据.在处理基本类型数据时,数 ...

  10. 进程通信之一 使用WM_COPYDATA C++及C#实现 z

    原文地址:http://blog.csdn.net/morewindows/article/details/6804157 进程间通信最简单的方式就是发送WM_COPYDATA消息.本文提供C++及C ...