Unity Resource Manager

来源:互联网 发布:刷关注软件 编辑:程序博客网 时间:2024/04/28 16:42

原文链接:http://tonytang1990.github.io/2016/10/13/Unity-Resource-Manager/

参考文章和网站:
Asset Workflow
Behind the Scenes
A guide to AssetBundles and Resources
Assets, Objects and serialization
The Resources folder
AssetBundle fundamentals 
AssetBundle usage patterns
Unity5 如何做资源管理和增量更新
Unity3D研究院之提取游戏资源的三个工具支持Unity5
Unity3D研究院之Assetbundle的原理(六十一)
Unity3D研究院之Assetbundle的实战(六十三)

前言

在最初学习Unity的时候,对于资源管理没有系统的概念,只知道放到Assets下即可。
本章节主要是对于Unity在资源管理方面的进一步学习了解。

这里简单提及正确的利用Unity资源管理的好处:

  1. 高效的管理资源
  2. 合理的分配内存(避免不必要的内存开销 — 比如同一个Asset被打包到多个AssetBundle里,然后分别被游戏加载)
  3. 做到增量更新(无需下载更新整个游戏程序,通过Patch的形式动态更新部分游戏内容)
  4. 以最少及最合理的方式减少程序大小(避免所有资源一次性打到游戏程序里)
  5. 帮助快速开发(动态和静态的资源方式合理利用,高效开发)

资源管理

资源形式

Asset

在Unity里所有我们使用的资源都是以Asset的形式存在的。
我们要想在Unity里使用特定的资源文件(比如.ogg .png .fbx等(Unity支持的资源格式)),我们需要放到Assets目录下。
见下图:
AssetsDirectoryAssetsDirectory

在进一步了解我们导入Assets的时候,会发生些什么之前,先让我们来学习了解一些相关的概念。
首先,什么是Asset?
Asset — “An Asset is a file on disk, stored in the Assets folder of a Unity Project. e.g. texture files, material files and FBX files……”(Asset代表的所有存储在Assets目录下的文件资源)

Unity如何区分每一个Asset?
Unity通过赋予每一个Asset一个Unique ID来区分每一个Asset。
每一个放在Asset目录下的资源都会对应生成一个同样名字的.meta文件,前面提到的Unique ID就被存储在这里。
下面我已导入一张Projectile.png并设置导入设置为Sprite等相关信息为例。
ImportProjectilePNGImportProjectilePNG
ProjectilePNGImportSettingProjectilePNGImportSetting
Projectile.png.meta

1
2
3
4
5
6
7
fileFormatVersion: 2
guid: 8764571b0416f90488390d0114c49afd
timeCreated: 1476349445
licenseType: Free
TextureImporter:
fileIDToRecycleName: {}
......

可以看到对应生成了一个叫Projectile.png.meta的文件,里面存储了guid(前面提到的那个Unique ID)和资源导入配置的数据。
这样一来无论我们如何移动Asset(在Assets目录下),我们都不会影响其他资源对该Asset的引用(因为Unity通过Unique ID去标识该Asset)
Note:
所以我们一旦在Unity外部移动或改名Asset文件,一定要把对应的Asset.meta文件名也对应移动和改名。(在Unity窗口修改可以不必管这些,因为Unity会对应生成新的.meta文件)
如果script脚本的.meta文件丢失,那么所有挂载了该script的GameObject都会显示unassaigned script(因为找不到该Script Asset的ID标识)

知道了Asset在Unity是如何区分的,那么接下来的问题是,Unity是直接使用这些Asset资源文件作为游戏资源吗?
答案是否定的,所有导入的Asset都会被Unity转化成特定的格式在游戏中使用,被存储在Library目录下,而原有资源保持不变,依然放在原始位置。(这样一来,我们可以通过修改原始文件,快速的在Unity中看到变化。比如.png作为UI,我们在Photoshop里修改源文件,直接就能在Unity看到变化)
UnityAssetInternalFormatInLibraryFolderUnityAssetInternalFormatInLibraryFolder
Note:
因为Library目录是通过动态转换Asset资源成Unity识别的数据,所以我们不会去主动修改该目录文件,同时也不会对该目录做版本控制,我们放心的删除该目录(但会导致所有Asset重新导入声称一次Unity识别的数据)

知道了Unity如何利用原始文件生成最终的可利用的资源数据,那么是不是所有的Asset都是通过直接放置在Assets目录下得到的了?
答案是否定的,Unity支持从一些资源文件里细分出多个Assets。(比如.png文件可以作为Multiple Sprite导入到Unity里,然后通过Sprite Editor细分出多个Sprite,然后每一个Sprite都作为Asset存在于Unity里)

知道了Asset在Unity里的概念以及存储方式,那么Unity支持哪些常见的Asset格式了?

  1. Image File(e.g. .bmp, .tif, .tga, .jpg, .psd……)
  2. 3D Model Files(.fbx)
  3. Meshes & Animations
  4. Audio files
  5. Other Asset Types

遇到Unity不支持的格式,Unity需要通过import process导入资源文件(e.g. PNG, JPG)
“The import process converts source Assets into formats suitable for the target platform selected in the Unity Editor.”(Import process主要是为了将不支持的资源格式转换到Unity对应平台设置的对应格式)

Unity不可能每一次都去重新Import这些资源,那么Unity是如何将import结果存储在哪里了?
为了避免重复的import导入,” the results of Asset importing are cached in the Library folder.”(Asset导入的结果被缓存在了library folder — 我想这也就是为什么每次删掉Library文件会导致一些asset重新导入的原因)

“the results of the import process are stored in a folder named for the first two digits of the Asset’s File GUID. This folder is stored inside the Library/metadata/ folder. The individual Objects are serialized into a single binary file that has a name identical to the Asset’s File GUID.”(可以看出import的结果存储在Library/metadata/文件夹下,并且把File GUID的前两位bit作为文件夹名,以File GUID作为文件名字)
以前面Projectile.png导入为例:
因为Projectile.png.meta的File ID为8764571b0416f90488390d0114c49afd
所以导入的结果就存储在Library\metadata\87\文件夹下,文件名为8764571b0416f90488390d0114c49afd(由于是二进制文件,无法查看具体内容)
AssetImportResultAssetImportResult
Note:
Non-Native asset type需要通过asset importer去导入Unity。(自动调用,也可以通过AssetImporter API去调用)

明白了Asset在Unity里的概念和存储方式,那么我们如何去访问,使用,创建Asset了?
在了解如何访问Asset之前,我们需要明确的是,我们访问的目的是什么。
如果只是在编辑器里单纯访问Asset去创建和删除一些Asset,那么通过AssetDatabase就可以实现。

先来看看什么是AssetDatabase:
“AssetDatabase is an API which allows you to access the assets contained in your project. Among other things, it provides methods to find and load assets and also to create, delete and modify them. The Unity Editor uses the AssetDatabase internally to keep track of asset files and maintain the linkage between assets and objects that reference them.”(可以看出,AssetDatabase在Unity对于Asset管理上起了关键性作用,AssetDatabase里存储了Asset相关的很多信息(e.g. Asset Depedency, Asset Path…..))

所以如果我们想通过代码去实现一些关于Assset的操作,我们应该使用AssetDatabase而非Filesystem(Filesystem只是单纯的删除或移动文件,但对于Asset在Unity里的导入设置,Asset访问等还是得通过AssetDatabase的接口来操作)

这里我以之前导入的Projectile.png为纹理图片,通过程序创建3个颜色分别是Red,Blue,Green的Material和分别使用其作为材质的3个Cude Prefab为例:
先看一下效果图:
CreateCubePrefabsCreateCubePrefabs
CreateCubeMaterialsCreateCubeMaterials

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using UnityEngine;
using System.Collections;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class CreateCubeAssetMenu {
//Only works under editor
#if UNITY_EDITOR
[MenuItem("AssetDatabase/CreateCubeAsset")]
static void CreateCudeAsset()
{

//load Projectile.png as texture
Texture2D texture = (Texture2D)AssetDatabase.LoadAssetAtPath("Assets/Sprites/Projectile.png", typeof(Texture2D));

//create material and cube assets
string matassetname;
string cubename;
string materialfoldername = "Materials";
string cubefoldername = "Prefabs";

Color[] colors = { Color.red, Color.blue, Color.green };
for (int i = 0; i < colors.Length; i++)
{
//Create material first
Material mat = new Material(Shader.Find("Transparent/Diffuse"));
mat.mainTexture = texture;
mat.color = colors[i];

//create a new cube and set material for it
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
MeshRenderer meshrender = obj.GetComponent<MeshRenderer>();
if (meshrender != null)
{
meshrender.material = mat;
}
else
{
Debug.Log("meshrender == null!");
break;
}

matassetname = mat.color.ToString() + "Material.mat";
cubename = mat.color.ToString() + "Cube.prefab";
//create material folder
//check whether material folder exist
if (AssetDatabase.Contains(mat))
{
Debug.Log("Material asset has been created before!");
}
else
{
if (!AssetDatabase.IsValidFolder("Assets/" + materialfoldername))
{
AssetDatabase.CreateFolder("Assets", materialfoldername);
}

if (!AssetDatabase.IsValidFolder("Assets/" + cubefoldername))
{
AssetDatabase.CreateFolder("Assets", cubefoldername);
}

AssetDatabase.CreateAsset(mat, "Assets/" + materialfoldername + "/" + matassetname);

//Create prefab
PrefabUtility.CreatePrefab("Assets/" + cubefoldername + "/" + cubename, obj);

//inform the change
AssetDatabase.Refresh();
}
}
}
#endif
}

Note:
AssetDatabase只适用于Editor,所以上述代码都用#if UNITY_EDITOR #endif判断了平台

那么是不是只需存储Asset的.meta文件(Unique ID和导入配置信息)就足够了了?
答案是否定的,要知道在Unity里很多Asset不只是单纯的通过原始资源导入构成的(比如一个Bullet Prefab,上面会挂载Component,我们不仅要去标识Asset,我们还需要对Asset上的各个Component进行标识和相关信息存储,这样Unity才能正确的找到特定Asset上的特定Component的)。

Object

在进一步了解还应该存储哪些信息之前,这里需要理解一个概念UnityEngine.Object
那么Object在Unity里是什么概念了?
UnityEngine.Object — “Object with a capitalized ‘O’, is a set of serialized data collectively describing a specific instance of a resource. This can be any type of resource which the Unity Engine uses, such as a mesh, a sprite, and AudioClip …..”(Object是指描述了Asset上使用的所有resources的序列化数据。比如制作一个2D Bullet Prefab,上面会挂载Transform,Sprite Renderer,Box Collider 2D,Rigidbody 2D, Bullet Script,Animator等Object,这里我们需要分别标识和记录这些Object的信息)

前面我们提到了Asset是通过Unique ID(File GUID)来标识,那么这里的Object用什么来标识了?并且又存储在哪里了?
答案是使用Local ID来标识并存储在Asset文件里(需要设置Asset Serialization到Force Text才能查看(默认是Mixed(Text和Binary)))
Local ID — identifies each Object within an Asset file because an Asset file may contain multiple Objects.

那么Asset和Object之间是什么样的关系了?
“There is a one-to-many relationship between Assets and Objects: that is, any given Asset file contains one or more Objects.”(Asset file可以包含一个或多个Objects)

那么如何查看Object的具体信息了?
通过设置Edit -> Project Setting -> Editor -> Asset Serialization -> Force Text
我们可以去查看所有Object索引的相关信息。
这里我们以一个创建一个Bullet Prefab的Asest file为例(Bullet Prefab包含很多Component):
当创建一个Bullet Prefab的时候,我在上面挂载了Transform,Sprite Renderer,Box Collider 2D,Rigidbody 2D, Bullet Script,Animator等Object。
BulletInspectorBulletInspector
这里的Bullet.prefab文件就是我们说的Asset File。
而上述挂载的所有Components就是之前说的Object。(这就印证了Asset File和Object一对多的关系)
下面让我们看看在包含多个Obejct的Prefab里是如何通过File GUID和Local ID来定位各个Object的。
Bullet.prefab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1001 &100100000
Prefab:
......
--- !u!1 &1000012041017010
GameObject:
......
--- !u!4 &4000011268538122
Transform:
......
--- !u!50 &50000011296845006
Rigidbody2D:
......
--- !u!61 &61000010120606286
BoxCollider2D:
......
--- !u!95 &95000013277359834
Animator:
......
--- !u!114 &114000011185120874
MonoBehaviour:
......
--- !u!212 &212000011673545760
SpriteRenderer:
......

Bullet.prefab.meta

1
2
3
4
5
6
7
8
fileFormatVersion: 2
guid: 9c4834a7b611ce848b2d182d5057bcbb
timeCreated: 1476367984
licenseType: Free
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:

从Bullet.prefab里可以看出每一个Object都有定义对应的Local ID
而在Bullet.prefab.meta里定义了Bullet Prefab这个Asset File的File GUID
结合File GUID和Local ID我们就能定位到Bullet Prefab Asset File里的某某Object

那么为什么要采用File GUID和Local ID了?
“The File GUID provides an abstraction of a file’s specific location. As long as a specific File GUID can be associated with a specific file, that file’s location on disk becomes irrelevant. The file can be freely moved without having to update all Objects referring to the file.”(通过指定唯一个File GUID和Local ID,使文件的位置变的无关紧要,我们可以随意的移动文件位置而无需更新所有的Object reference信息)

所以一旦File GUID丢失,那么所有引用该文件里的Object的引用都会丢失,因为无法定位位于哪一个Asset File里。(所以不要随意乱改.meta文件)

File GUID和Local ID虽然好,但是一味的比较File GUID和Local ID会导致slow performance,所以Unity为了快速访问标识的各个Asset,内部维护了一个Instance ID的映射缓存(通过File GUID和Local ID计算得出的唯一的integer)来标识各个Asset。
通过Instance ID Unity可以快速的访问到被加载了的对应的Object。(如果target object还没被加载,那么通过File GUID和Local ID,Unity会去把Object加载进来)

那么Instance ID是如何运作的了?
“At startup, the Instance ID cache is initialized with data for all Objects that are built-in to the project (i.e. referenced in Scenes), as well as all Objects contained in the Resources folder. Additional entries are added to the cache when new assets are imported at runtime(3) and when Objects are loaded from AssetBundles. Instance ID entries are only removed from the cache when they become stale. This happens when an AssetBundle providing access to a specific File GUID and Local ID is unloaded.”(当游戏启动的时候,Instance ID的cache开始初始化所有在项目场景里引用到的Object。额外的Instance ID Cache只有在通过运行时导入或则AssetBundle动态加载Object的时候添加。Instance ID只有在Instance ID标识的Object被Unloaded的时候才会被removed from cache)

那么什么时候Object才会被Unloaded了?Object的Instance ID与AssetBundle之间又是如何关联起来的了?
“When the unloading of an AssetBundle causes an Instance ID to become stale, the mapping between the Instance ID and its File GUID and Local ID is deleted to conserve memory. If the AssetBundle is re-loaded, a new Instance ID will be created for each Object loaded from the re-loaded AssetBundle.”(当AssetBundle(后面会详细讲到)被unload后,通过AssetBundle加载的Object的Instance ID会被删除以节约内存。当AssetBundle再次加载进来后,当AssetBundel里的Obejct被再次加载时,会为该Object生成新的Instance ID到cache里)

上面算是提到了Resource(Asset里的Object)的lifecycle。关于Resource Lifecycle和Instance ID相关的学习在了解了Unity里与资源加载相关的Resource和AssetBundle后会再次讨论,见后面。

Note:
Implementation note: At runtime, the above control flow is not literally accurate. Comparing File GUIDs and Local IDs at runtime would not be sufficiently performant during heavy loading operations. When building a Unity project, the File GUIDs and Local IDs are deterministically mapped into a simpler format. However, the concept remains identical, and thinking in terms of File GUIDs and Local IDs remains a useful analogy during runtime.

接下来看看两种特殊的Object类型:

  1. ScriptableObject
    “Provides a convenient system for developers to define their own data types.”(用于定义自定义类型数据)
  2. MonoBehaviour
    “Provides a wrapper that lins to a MonoScript. A MonoScript is an internal data type that Unity uses to hold a reference to a specific scripting class within a specific assembly and namespace. The MonoScript does not contain any actual executable code.”

Monoscripts:
“a MonoBehaviour has a reference to a MonoScript, and MonoScripts simply contain the information needed to locate a specific script class. Neither type of Object contains the executable code of script class.”
“A MonoScript contains three strings: an assembly name, a class name, and a namespace.”(正如前面MonoBehavior提到的,MonoScript包含了定位script class需要的信息。 e.g. assembly name, class name, namspace……)

我们编写的scripts最终会被Unity编译到Assembly-CSharp.dll里。
在Plugins目录下的插件会被编译到Assembly-CSharp-firstpass.dll里。
UnityAssemblyUnityAssembly

那么我们的Asset资源被打包到哪里去了了?
接下来我们设置场景如下:

  1. 放置一个Bullet.prefab实例(上面挂载了Bullet script和一系列Component,使用Assets/Sprites/Projectile.png作为sprite(设置了打包到图集1里))
  2. 创建一个Cude的3D GameObject(Unity Primitive Cube)
  3. 创建UI Image使用Background.png作为Sprie(/Assets/Resources/Textures/UI/Background.png)
  4. 放置一张未使用的AddImage.png在Assets/Sprites下,并设置打包到图集2里。
  5. 放置一张未使用的Coin.png在Assets/Resources/Textures/UI下,并设置打包到图集3里。

然后通过Unity提取查看IOS打包后的各个资源分别存储在哪里。
首先看看我们在场景里创建的GameObject情况:
ResourceManagerStudySceneResourceManagerStudyScene
接下来查看下打包的IOS的资源文件夹下的.asset文件:
ResourceManagerStudyIOSResourceResourceManagerStudyIOSResource
可以看到Data下有三个.assets文件(globalgamemanager.assets和sharedassets0.assets和resources.assets)
然后通过UnityStudio我们分别打开这三个文件进行查看:
globalgamemanager.assets
globalgamemanagerassetsglobalgamemanagerassets
sharedassets0assetssharedassets0assets
resourcesassetsresourcesassets
通过查看里面的资源可以发现,我们放在Assets/Sprites下的Projectile.png被打包到了SpriteAtlasTexture-1-32x32-fmt33(Texture2D)图集里,而作为UI背景放置在Assets/Resoures/Textures/UI下的backgroudn.png被单独打包在了名为backgroudn(Texture2D)里
而没有在游戏里使用且放置在Assets/Resources/Textures/UI下的Coin.png被单独打包到了名为resources.assets的资源文件里。

从上面可以看出Resources下的资源文件并没有被打包到图集里,而是作为单独的Texture2D资源存在。同时没有放置在Resources目录下的资源,如果没有被游戏使用,最终是不会被打包到游戏里(反之放在Resources目录下即使未被使用也会被打包到游戏里)。

如果我们使用UnityStudio加载整个Data文件夹,我们还能查看到场景Level里的树状结构:
SeneHierarchySeneHierarchy

说了这么多Asset和Object相关的知识和概念,下面提一个与之相关却又常常遇到的问题。
Unity Store可以下载很多Asset Package资源,那么这里的问题就是,Asset Package是个什么概念?为什么通过导入Asset Package我们就能导入别人做好的Asset资源?如何制作自己的Asset Package?
Asset Package概念:
“Packages are collections of files and data from Unity projects, or elements of projects, which are compressed and stored in one file, similar to Zip files.”(可以看出Asset Package只是相当于Unity对于一系列Assets的打包,单记录了Assets原始的目录结构和Asset信息,好比压缩包)

正如我们前面学习理解的,要想使Asset能够使用,我们需要把Asset源文件和Asset.meta文件一起保存下来(为了确保原始的Asset和Object引用正确)。那么Asset Package是否保存了Asset源文件和Asset.meta文件了?接下来通过自制Asset Package我们来验证这个问题。

如何制作自己的Asset Package:
这里以导出前面制作的Bullet.prefab(Bullet.prefab以Projectile.png作为Sprite,同时挂载了Rigidbox 2D,Boxcollider 2D,Bullet script……)为例:
Asset -> Export Package -> 只勾选Bullet.prefab -> Export
不勾选Include Dependencies:
ExportPackageWithoutDependencyExportPackageWithoutDependency
勾选Include Dependencies:
ExportPackageWithDependencyExportPackageWithDependency
这里不知道为什么CreateCubeAssetMenu.cs会作为Bullet.prefab的dependencies!
接下来在新的项目里导入该Asset Package:
Assets -> Import Package -> custom package
这样一来就得到了我们所导出的Assets Package
AssetPackageCompareAssetPackageCompare
可以看出Bullet.prefab和Bullet.meta原封不动的以原始的形式保留了下来。
Note:
当导出Asset Package的时候,勾选Include dependencies,那么所有Asset依赖的Asset都会被导出到最终的Package

资源来源

Unity自动打包

Unity自动打包资源是指在Unity场景中直接使用到的资源会随着场景被自动打包到游戏中,这些资源会在场景加载的时候由unity自动加载。这些资源只要放置在Unity工程目录的Assets文件夹下即可,程序不需要关心他们的打包和加载,这也意味着这些资源都是静态加载的。但在实际的游戏开发中我们一般都是会动态创建GameObject,资源是动态加载的,因此这种资源其实不多

Resources

所有放在Assets/Resources目录下的资源都当做Resources。无论游戏是否使用,都会被打包到最终的程序里。(这也就说明为什么前面的例子在Resources下没有被使用的Coin.png最终被打包到了resources.assets里)
那么如何判断Resources下的资源是被打包到resources.assets里还是其他的assets里了?
答案取决于我们是否在Unity对Resources目录下的资源进行了索引引用。
正如前面我们测试的结果一样,同样是放在Resources目录下的backgroudn.png和Coin.png,前者因为在游戏里有引用,所以被打包到了sharedassets0.assets里,后者因为无人使用,而打包到了resources.assets里。(Resources下被引用的资源是存储在.sharedAsseets file里,而没被引用的是存储在resources.assets里)

那么为什么我们需要把资源放置在Resources下了?放在Assets下单独去引用使用不就可以了吗?
Resources主要是为了帮助我们去动态加载一些资源去创建Asset。(通过Resource API可以动态加载Resource里的资源)

但Unity官网讲解提到的我们应该尽量去避免使用Resources。
原因如下:

  1. Use of the Resources folder makes fine-grained memory management more difficult.(使用Resources folder会使内存管理更困难)
  2. Improper use of Resources folders will increase application startup time and the length of builds.(不合理的使用Resources folders会使程序启动和编译时间变长)
    As the number of Resources folders increases, management of the Assets within those folders becomes very difficult.(随着Resources folders数量的增加,Assets管理越来越困难)
  3. The Resources system degrades a project’s ability to deliver custom content to specific platforms and eliminates the possibility of incremental content upgrades.(Resources System降低了项目对于各平台和资源的动态更新能力,因为Resources目录下的资源无论如何都会被打包到游戏程序里)
    AssetBundle Variants are Unity’s primary tool for adjusting content on a per-device basis.(AssetBundle是Unity针对设备动态更新的主要工具)

正确的使用Resources system就显得尤为重要:
以下两种情况比较适合使用Resource System:

  1. Resources is an excellent system for during rapid prototyping and experimentation because it is simple and easy to use. However, when a project moves into full production, it is strongly recommended to eliminate uses of the Resources folder.(快速开发,但到了真正发布还是应该减少Resources Folder的使用)
  2. The Resources folder is also useful in trivial cases, when all of the following conditions are met(当下列情况都满足的时候,Resource folder比较有用):
    1. The content stored in the Resources folder is not memory-intense
    2. The content is generally required throughout a project’s lifetime(该资源在项目生命周期里都需要)
    3. The content rarely requires patching(很少需要改动patch)
    4. The content does not vary across platforms or devices.(在各个平台设备都一致)
      比如一些第三方配置文件等asset。

那么接下来让我们了解下Resources是如何被保存到Unity里的:
Serialization of resources:
“The Assets and Objects in all folders named “Resources” are combined into a single serialized file when a project is built.”(当项目编译的时候,所有放到Resources目录下的Assets和Object最终会被序列化到一个单独的文件,根据前面的测试应该是resources.assets)

“This file also contains metadata and indexing information, similar to an AssetBundle. This indexing information includes a serialized lookup tree that is used to resolve a given Object’s name into its appropriate File GUID and Local ID. It is also used to locate the Object at a specific byte offset in the serialized file’s body.”(Resource会去维护一个映射表,用于查询特定Object

“As the lookup data structure is (on most platforms) a balanced search tree(1), its construction time grows at an O(N log(N)) rate.”(Lookup是通过平衡二叉树来查找,所以时间复杂度为N x Log(N))

“This operation is unskippable and occurs at application startup time while the initial non-interactive splash screen is displayed.”(在程序启动的时候会去初始化index info(Lookup data)的时候,Resources里assets数量过多的话会导致花费大量时间)

AssetBundle

接下来让我们前面一直提到的一个很重要的点AssetBundle。
什么是AssetBundle?
The AssetBundle system provides a method for storing one or more files in an archival format that Unity can index. The purpose of the system is to provide a data delivery method compatible with Unity’s serialization system. AssetBundles are Unity’s primary tool for the delivery and updating of non-code content after installation.(AssetBundle system提供了一个被Unity支持索引的格式文件(被Unity serialization system支持)。主要用于非代码资源的动态更新。)

为什么需要AssetBundle?
This permits developers to reduce shipped asset size, minimize runtime memory pressure, and selectively load content that is optimized for the end-user’s device.(AssetBundle的好处是减少了发布的Asset大小,降低了运行时内存压力,动态更新非代码资源)

AssetBundle包含些什么信息?
主要包含两部分信息:

  1. A header
    The header is generated by Unity when the AssetBundle is built.(header是在Unity编译AssetBundle的时候生成)主要包含下列内容:
    1. The AssetBundle’s identifier
    2. Whether the AssetBundle is compressed or uncompressed
    3. A manifest(“The manifest is a lookup table keyed by an Object’s name. Each entry provides a byte index that indicates where a given Object can be found within the AssetBundle’s data segment.”(manifest把Object的名字作为key,用于查询特定object是否存在于AssetBundle的数据字段里))
      (manifest里通过std::multimap实现,不同平台multimap的实现有些许差别,Windows和OSX采用red-black tree,所以在构造manifest的时候,时间复杂度是N x Log(N))
  2. A data segment.
    “Contains the raw data generated by serializing the Assets in the AssetBundle.”(data segment包含了序列化Assets的原始数据)
    data segment最后还会通过LZMA algorithm压缩。
    “Prior to Unity 5.3, Objects could not be compressed individually inside an AssetBundle. “(Unity 5.3之前Object不支持被单独压缩到AssetBundle里,所以在去访问一个被包含在压缩了的AssetBundle里的Object时,Unity需要去解压整个AssetBundle)
    “Unity 5.3 added a LZ4 compression option. AssetBundles built with the LZ4 compression option will compress individual Objects within the AssetBundle, allowing Unity to store compressed AssetBundles on disk.”(Unity 5.3加入了LZ4压缩选项,支持单独的Object压缩到AssetBundle里,这样一来就可以通过单独解压特定的Object来实现访问该Object)

AssetBundle能包含哪些Assets?
Models,Materials,textures and secenes.AssetBundle can not contain scripts.

如何去加载AssetBundles?
下列四种方式主要是根据AssetBundle的压缩算法和平台支持来划分。
API加载AssetBundles:

  1. AssetBundle.LoadFromMemoryAsync(Unity’s recommendation is not use this API)
    AssetBundle.LoadFromMemoryAsync详情
  2. AssetBundle.LoadFromFile
    “A highly-efficient API intended for loading uncompressed AssetBundle from local storage, such as a hard disk or an SD card.”(可以高效的加载本地未压缩的AssetBundle,也支持加载LZ4压缩的AssetBundle,但不支持LZMA压缩的AssetBundle)
    Mobile和Editor表现不一样,详情参见
  3. WWW.LoadFromCacheOrDownload
    “A useful API for loading Objects both from remote servers and from local storage.”(主要用于加载远程服务器端和本地的Object)
    使用建议:
    “Due to the memory overhead of caching an AssetBundle’s bytes in the WWW object, it is recommended that all developers using WWW.LoadFromCacheOrDownload ensure that their AssetBundles remain small”(尽量保证AssetBundle很小,避免内存消耗过大)
    “Each call to this API will spawn a new worker thread. Be careful of creating an excessive number of threads when calling this API multiple times.”(避免同时调用多次,导致大量的Thread执行,确保同一时间很少的thread执行)
  4. UnityWebRequest’s DonwloadHandleAssetBundle(on Unity 5.3 or newer)
    “UnityWebRequest allows developers to specify exactly how Unity should handle downloaded data and allows developers to eliminate unnecessary memory usage.”(UnityWebRequest支持更细致的AssetBundle加载的内存使用,可以通过配置UnityWebRequest达到使用最少内存的目的得到我们想要加载的Obejct)
    “Note: Unlike WWW, the UnityWebRequest system has an internal pool of worker threads and an internal job system to ensure that developers cannot start an excessive number of simultaneous downloads. The size of the thread pool is not currently configurable.”(UnituWebRequest system内部有自身的线程管理,避免同一时间大量的线程同时加载)

使用建议:
尽可能的使用AssetBundle.LoadFromFile(使用异步版本LoadFromFileAsync)
当项目需要下载和patch AssetBundle时,尽量使用UnityWebRequest(Unity 5.3),老版本的话使用WWW.LoadFromCacheOrDownload.
可能的话最好在项目安装的时候,预先缓存AssetBundle

Loading Assets from AssetBundles:
Synchronous API:

  1. LoadAsset
  2. LoadAllAssets
  3. LoadAssetWithSubAsset

Asynchronous API:

  1. LoadAssetAsync
  2. LoadAllAssetsAsync
  3. LoadAssetWithSubAssetAsync

使用建议:
“LoadAllAssets should be used when loading multiple independent UnityEngine.Objects.”(当需要加载大量独立的Objects的时候,使用LoadAllAssets。当需要加载的Object数量很多,又少于AssetBundle里的2/3的时候,我们可以采用制作多个小的AssetBundle,然后再通过LoadAllAssets加载)
“LoadAssetWithSubAssets should be used when loading a composite Asset which contains multiple embedded Objects. If the Objects that need to be loaded all come from the same Asset, but are stored in an AssetBundle with many other unrelated Objects.”(当加载由多个obejct构成的Object的时候,建议使用LoadAssetWithSubAssets。当加载的Objects都来之同一个Asset,但存储的AssetBundle里包含很多其他无关的Obejcts时,采用LoadAssetWithSubAssets)
“For any other case, use LoadAsset or LoadAssetAsync.”(其他情况都是用LoadAsset和LoadAssetAsync)

Low-Level Loading details:
“UnityEngine.Object loading is performed off the main thread: an Object’s data is read from storage on a worker thread. Anything which does not touch thread-sensitive parts of the Unity system (scripting, graphics) will be converted on the worker thread.”(Object的加载是在main thread上,而object data的数据读取是在worker thread。所有线程不敏感的数据都是在worker thread进行。)

加载AssetBundle里的Object需要注意些什么?
“An Object is assigned a valid Instance ID when its AssetBundle is loaded, the order in which AssetBundles are loaded is not important. Instead, it is important to load all AssetBundles that contain dependencies of an Object before loading the Object itself. Unity will not attempt to automatically load any child AssetBundles when a parent AssetBundle is loaded.”(当AssetBundle被加载的时候,Object会被assigned一个valide instance ID,因为这个instance ID是唯一的,所以AssetBundle的加载顺序并不重要,重要的是我们要确保所有Object依赖的Objects都被加载(Unity不会自动加载所有Child AssetBundles当Parent AssetBundle被加载的时候))
下面以Material A引用Texture B为例。Material A被Packaged到了AssetBundle1,而Texture B被packaged到了AssetBundle2。
AssetBundleDependenciesAssetBundleDependencies
所以我们要使用Material A,我们不仅要加载AssetBundle1,还得确保在此之前我们加载了AssetBundle2里的Texture B

AssetBundle的dependencies信息存储在哪里?
AssetBundleManifest存储了AssetBundle’s dependency information(AssetBundle里的依赖关系信息)

AssetBundleManifest存放在哪里?
This Asset will be stored in an AssetBundle with the same name as the parent directory where the AssetBundles are being built.(AssetBundleManifest存放在AssetBundle同级目录,并且包含一样的名字)

Note:
The AssetBundle containing the manifest can be loaded, cached and unloaded just like any other AssetBundle.(AssetBundleManifest可以像AssetBundle一样被加载,缓存,释放)

如何查询AssetBundle里的Dependecy信息?
Depending on the runtime environmen:

  1. Editor
    AssetDatabase API(Query AssetBundle dependencies)
    AssetImporter API(Access and change AssetBundle assignments and dependencies)
  2. Runtime
    AssetBundleManifest API(load the dependency information of AssetBundle)
    AssetBundleManifest.GetAllDependencies
    AssetBundleManifest.GetDirectDependencies
    Note:
    “Both of these APIs allocate arrays of strings. Use them sparingly, and preferably not during performance-sensitive portions of an application’s lifetime.”(因为上述API会分配大量的string字符串,所以要尽量少用并且避开性能敏感的时期)

接下来通过制作2个UI prefab:
一个包含全屏显示的Background image(打到uitextures AssetBundle里)
一个包含Backgroundimage但大小只有背景的一半(打到backgroundimage AssetBundle里)
同时设置background图片都打包到uitextures AssetBundle里
通过AssetBundleManifest API查询两个AssetBundle里的Dependencies信息。
首先如何制作AssetBundle?

  1. 选择需要制作成AssetBundle的资源(Texture,Prefab),设置相应的AssetBundle名字
    AssetBundleUIBackground1AssetBundleUIBackground1
    BackgroundImagePrefabAssetBundleBackgroundImagePrefabAssetBundle
    UIBackgroundPrefabAssetBundleUIBackgroundPrefabAssetBundle
  2. 调用BuildPipeline.BuildAssetBundles()打包AssetBundle
    Unity5.4官网给出了两个方法:

    1
    2
    3
    public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform); 

    public static AssetBundleManifest BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

    前者是以UnityEditor设置的AssetBundle name为准进行所有AssetBundle打包,后者是根据自定义的打包规则打包特定AssetBundle。
    这里我尝试使用前者针对PC进行测试。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    using UnityEngine;
    using System.Collections;
    using UnityEditor;

    public class AssetBundleMenu {
    [MenuItem("Assets/Build AssetsBundle")]
    static void BuildAllAssetBundles()
    {

    if (!AssetDatabase.IsValidFolder("Assets/StreamingAssets"))
    {
    AssetDatabase.CreateFolder("Assets", "StreamingAssets");
    }

    Caching.CleanCache();
    BuildPipeline.BuildAssetBundles("Assets/StreamingAssets", BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
    }
    }

    第一个参数是输出目录
    第二参数控制AssetBundle打包设定,比如是否压缩等
    第三个参数可设置打包平台
    打包AssetBundle之后的目录结构:
    AssetBundleFolderAssetBundleFolder
    .manifest文件里存储了dependencies信息和AssetBundle里所打包的Assets相关信息
    StreamingAssets.manifest

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ManifestFileVersion: 0
    CRC: 931964529
    AssetBundleManifest:
    AssetBundleInfos:
    Info_0:
    Name: uitextures
    Dependencies: {}
    Info_1:
    Name: backgroundimage
    Dependencies:
    Dependency_0: uitextures

    uibackground.manifest

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    ManifestFileVersion: 0
    CRC: 1667709317
    Hashes:
    AssetFileHash:
    serializedVersion: 2
    Hash: 452c307ebc11a6155a4bdc61d8a0e39f
    TypeTreeHash:
    serializedVersion: 2
    Hash: a13e216067ae14bd74f1f5dcc7c211d7
    HashAppended: 0
    ClassTypes:
    - Class: 1
    Script: {instanceID: 0}
    ......
    Assets:
    - Assets/Resources/Textures/UI/backgroudn.png
    - Assets/Prefabs/UIBackground.prefab
    Dependencies: []

    backgroundimage.manifest

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    ManifestFileVersion: 0
    CRC: 4279971007
    Hashes:
    AssetFileHash:
    serializedVersion: 2
    Hash: c15d1d4fd0fc89ad672fd6a94e16d981
    TypeTreeHash:
    serializedVersion: 2
    Hash: 4583000a582d4aadcaf8400b20641bd6
    HashAppended: 0
    ClassTypes:
    - Class: 1
    Script: {instanceID: 0}
    ......
    Assets:
    - Assets/Prefabs/BackgroundImage.prefab
    Dependencies:
    - Assets/ABs/uitextures

    从StreamingAssets.manifest可以看出,我们总共制作了两个AssetBundle,名字分别为uitextures和backgroundimage,并且backgroundimage AssetBundle依赖于uitextures。
    从uibackground.manifest和backgroundimage.manifest中可以看出,uibackground AssetBundle里包含了UIBackground.prefab和backgroudn.png,而backgroundimage AsssetBundle只包含BackgroundImage.prefab。
    由于BackgroundImage.prefab使用background.png作为背景,但backgroundimage被打包到了uitextures AssetBundle里,所以backgroundimage AssetBundle是依赖于uitextures AssetBundle的。

  3. 通过AssetBundle API下载并加载主AssetBundle,然后通过AssetBundleManifest API查看所有AssetBundle的依赖信息并加载依赖的AssetBundle,最后通过AssetBundle API加载AssetBundle里的特定资源并实例化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    using UnityEngine;
    using System.Collections;
    #if UNITY_EDITOR
    using UnityEditor;
    #endif
    using System.IO;

    public class AssetBundleLoad : MonoBehaviour {

    // Use this for initialization
    void Start () {
    StartCoroutine(LoadAssetBundles());
    }

    IEnumerator LoadAssetBundles()
    {

    #if UNITY_EDITOR
    var names = AssetDatabase.GetAllAssetBundleNames();

    foreach (string name in names)
    {
    Debug.Log("Current Alive Asset Bundle name: " + name);
    }
    #endif

    string bundlename = "StreamingAssets";
    var assetbundlerequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, bundlename));
    yield return assetbundlerequest;

    var assetbundle = assetbundlerequest.assetBundle;
    if (assetbundle == null)
    {
    Debug.Log("Failed to load " + bundlename);
    yield break;
    }

    AssetBundleManifest manifest = assetbundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

    if (manifest == null)
    {
    Debug.Log(string.Format("Failed to load {0}.manifest!", bundlename));
    yield break;
    }

    //try to instantiate backgroundimage assetbundle
    //but need to load dependencies assetbundle first
    Debug.Log("Instantiate BackgroundImage.prefab");

    var backgroundbundlename = "backgroundimage";
    var backgroundimagerequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, backgroundbundlename));

    yield return backgroundimagerequest;

    if (backgroundimagerequest == null)
    {
    Debug.Log(string.Format("Load {0} falied!", "backgroundimagerequest"));
    }

    var backgroundimageassetbundle = backgroundimagerequest.assetBundle;
    if(backgroundimageassetbundle == null)
    {
    Debug.Log(string.Format("Load {0} falied!", backgroundimageassetbundle));
    yield break;
    }

    var backgrounddependencies = manifest.GetAllDependencies(backgroundbundlename);
    if (backgrounddependencies.Length == 0)
    {
    Debug.Log("dependencies.length == 0");
    }

    //Load dependencies assetbundle first
    foreach (string dependency in backgrounddependencies)
    {
    Debug.Log("Dependency : " + dependency);
    var dependencyabrequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, dependency));
    yield return dependencyabrequest;
    AssetBundle dependencyab = dependencyabrequest.assetBundle;
    if (dependencyab == null)
    {
    Debug.Log(string.Format("Load {0} failed!", dependency));
    yield break;
    }
    }

    //Once load all dependencies assetbundle, we can instantiate the gameobject in assetbundle
    var backgroundprefabrequest = backgroundimageassetbundle.LoadAssetAsync("BackgroundImage.prefab");
    yield return backgroundprefabrequest;
    if(backgroundprefabrequest == null)
    {
    Debug.Log(string.Format("Load {0} faled!", "BackgroundImage.prefab"));
    }

    GameObject backgroundimage = backgroundprefabrequest.asset as GameObject;
    if(backgroundimage == null)
    {
    Debug.Log("backgroundimage == null");
    }
    else
    {
    GameObject bggo = Instantiate(backgroundimage);
    bggo.transform.SetParent(gameObject.transform, false);
    }

    //After complete using AssetBundle, always remember unload it,
    //otherwise you can not load it again due to it has exists in memory
    assetbundle.Unload(false);
    }
    }

从上面可以看出,通过主的StreamingAssets,我们获取到了里面所包含的所有AssetBundle信息。然后通过加载指定AssetBundle以及dependencies AssetBundle后,我们就能成功初始化出AsestBundle里的资源。
上述代码实例化了Background.prefab(使用的backgroudn.png存储在uitextures AssetBundle里)
效果图:
BackgroundImageAssetBundleLoadBackgroundImageAssetBundleLoad

上面使用的CreateFromFile后续Unity更新成了LoadFromFile,这个方法只支持uncompressed asset bundles,这里主要是因为利用了streaming assets,所以直接用这个方法可以加载本地的AssetBundle。
官方的建议在正式的时候是使用UnityWebRequest。
Note:
Note that bundles built for standalone platforms are not compatible with those built for mobiles and so you may need to produce different versions of a given bundle. (针对不同平台打包的AssetBundle不通用,需要各自打包对应平台的版本)

上面仅仅是以本地读取作为事例,了解了如何去访问AssetBundle以及manifest里的信息以及如何实例化AssetBundle里的资源。
那么如何判断AssetBundle是否需要更新了?
这里我们可以利用Built-in caching去实现版本更新判断。
“AssetBundle caching system that can be used to cache AssetBundles downloaded via the WWW.LoadFromCacheOrDownload or UnityWebRequest APIs.”(AssetBundle caching system可以帮助我们缓存加载了的AssetBundle而无需每次都重新加载)
而AssetBundle caching system是根据AssetBundle的version number来决定时是否下载新的AssetBundle。(AssetBundleManifest API支持通过MD5算法去计算出AssetBundle的version
number,这样一来每次AssetBundle有变化都会得到一个新的Hash version number)

“AssetBundles in the caching system are identified only by their file names, and not by the full URL from which they are downloaded.”(AssetBundles在Caching system里是通过文件名来标识的跟URL无关,所以无论AssetBundle放在服务器哪里都没有关系)

Cach相关的API控制:

  1. Caching.expirationDelay
    “The minimum number of seconds that must elapse before an AssetBundle is automatically deleted. If an AssetBundle is not accessed during this time, it will be deleted automatically.”(AssetBundle可允许被删除的未使用时间,只有未被使用的时间达到了才能才被删除)
  2. Caching.maximumAvailableDiskSpace
    “The amount of space on local storage that the cache may use before it begins deleting AssetBundles that have been used less recently than the expirationDelay. It is counted in bytes.”(Caching内存使用上限)

Note:
“As of Unity 5.3, control over the built-in Unity cache is very rough. It is not possible to remove specific AssetBundles from the cache. They will only be removed due to expiration, excess disk space usage, or a call to Caching.CleanCache. (Caching.CleanCache will delete all AssetBundles currently in the cache.) “(Caching System还不完善,所以还不允许删除特定AssetBundle,而只能通过Caching.CleanCache去删除所有的AssetBundle)

Cache Priming:
Steps:

  1. Store the initial or base version of each AssetBundle in /Assets/StreamingAssets/
  2. Loading AssetBundles from Application.streamingAssetsPath the first time the application is run
  3. Call WWW.LoadFromCacheOrDownload or UnityWebRequest normally.

Custom downloaders:
Custom downloaders More
……

一般AssetBundle都是通过WWW.LoadFromCacheOrDownload(老版本)或者UnityWebRequest指定url和版本号信息去决定是否下载更新到本地。
然后我们利用AssetBundle.CreateFromFile()去读取AssetBundle,从而实现动态更新AssetBundle。

那么在使用AssetBundle的过程中,我们应该遵循后续讲到的内容(大部分翻译至官网,翻译不太对的地方欢迎指出):
Managing Loaded Assets:
“If an AssetBundle is unloaded improperly, it can cause Object duplication in memory. Improperly unloading AssetBundles can also result in undesirable behavior in certain circumstances.”(不合理的释放Object会导致在内存中重复创建Object。同时也可能导致非预期的问题(比如texture丢失))

对于AssetBundle里Assets管理,这里需要强调的一个API是AssetBundle.Unload(bool);
Unloads all assets in the bundle.
Unload frees all the memory associated with the objects inside the bundle.
当传递true的时候所有从AssetBundle里实例化的Object都会被unload。传递false则只释放AssetBundle资源。

那么这里释放的AssetBundle资源是哪些了?
还记得之前提到的AssetBundle的组成吗(header & data segment)

下面通过AssetBundleAB里的Material Object M实例化的M为例:
AssetBundleUnloadAssetBundleUnload
AssetBundleAfterUnloadFalseAssetBundleAfterUnloadFalse
AssetBundleReloadAssetBundleReload
AssetBundleLoadObjectAgainAssetBundleLoadObjectAgain
如果AB.Unload(true),那么实例化的M会被destroyed。
如果AB.Unload(false),那么AB里的信息会被unloaded,但M还存在于Scene里,但M和AB之间关联就断开了。
当我们重新加载AB后,我们只是再次加载了AB里的信息,但M和AB还是没有关联。
当我们通过重新加载的AB再去实例化Material Object M的时候,我们是创建了一个关联到当前AB的新的M而非把就的关联到AB(Scene里当前存在两个M)

当我们想unloaded旧的M的时候,只能通过下列方式:

  1. Eliminate all references to an unwanted Object, both in the scene and in code. After this is done, call Resources.UnloadUnusedAssets.(取消所有引用,并调用Resources.UnloadUnusedAssets)
  2. Load a scene non-additively. This will destroy all Objects in the current scene and invoke Resources.UnloadUnusedAssets automatically.(切换Scene,触发Resources.UnloadUnusedAssets)

“Another problem can arise if Unity must reload an Object from its AssetBundle after the AssetBundle has been unloaded. In this case, the reload will fail and the Object will appear in the Unity Editor’s hierarchy as a (Missing) Object.”(当AssetBundle被释放后,如果再从该AssetBundle里加载Object会加载失败,出现missing object)

Distribution:
Two basic ways to distribute a project’s AssetBundles to clients:

  1. Installing them simultaneously with the project
  2. Downloading them after installation.
    使用哪一种方式,主要取决于平台需求。
    “Mobile projects usually opt for post-install downloads to reduce initial install size and remain below over-the-air download size limits. Console and PC projects generally ship AssetBundles with their initial install.”(手机上为了减少安装程序大小,通常选择post-installation。而PC不担心硬盘不够,所以通常选择initial install)

Shipped with Project:
“To reduce project build times and permit simpler iterative development. If these AssetBundles do not need to be updated separately from the application itself, then the AssetBundles can be included with the application by storing the AssetBundles in Streaming Assets.”(和程序一起更新的AssetBundle可以放在streaming assets伴随程序一起打包发布)

Streaming Assets:
“The easiest way to include any type of content within a Unity application at install time is to build the content into the /Assets/StreamingAssets/ folder, prior to building the project. Anything contained in the StreamingAssets folder at build time will be copied into the final application. This folder can be used to store any type of content within the final application, not just AssetBundles.”(可以看出Streaming Assets被存放在/Assets/StreamingAssets/目录下,最终会被打包到应用程序里)

Note:
“Android Developers: On Android, Application.streamingAssetsPath will point to a compressed .jar file, even if the AssetBundles are compressed. In this case, WWW.LoadFromCacheOrDownload must be used to load each AssetBundle.”(在Android上,streamingAssetsPath指向的是压缩后的.jar文件,所以我们需要采用LoadFromCacheOrDonwload去解压读取(5.3及以后可以采用UnityWebRequest’s DonwloadHandleAssetBundle))

“Streaming Assets is not a writable location on some platforms. If a project’s AssetBundles need to be updated after installation, either use WWW.LoadFromCacheOrDownload or write a custom downloader. “(因为Streaming Assets在一些平台上是一个不可写的位置,所以我们如果还需要更新该AssetBundle,我们而已通过WWW.LoadFromCacheOrDonwload或则自己编写custom downloader)

Donwloaded post-install:
手机上出于程序安装大小考虑多采用这个方案。
同时AssetBundle通过WWW.LoadFromCacheOrDownload or UnityWebRequest的更新可以快速方便的更新一些经常变化的资源。
更多内容参见

Asset Assignment Strategies:
The key decision is how to group Objects into AssetBundles. The primary strategies are:

  1. Logical entities
  2. Object Types
  3. Concurrent content
    详情参见

Guidelines to follow:

  1. Split frequently-updated Objects into different AssetBundles than Objects that usually remain unchanged
  2. Group together Objects that are likely to be loaded simultaneously

Patching with AssetBundles:
“Patching AssetBundles is as simple as downloading a new AssetBundle and replacing the existing one.”(Patching AssetBundles用于实现动态替换一些资源很方便)

AssetBundle Variants:
什么是AssetBundle Variants?
AssetBundle Variants可以指定AssetBundle里Asset的别名。(两个不同的名字可以指代同一个Asset)

AssetBundle Variants可以用来做什么?
[The purpose of Variants is to allow an application to adjust its content to better suit its runtime environment. Variants permit different UnityEngine.Objects in different AssetBundle files to appear as being the “same” Object when loading Objects and resolving Instance ID references.It permits two UnityEngine.Objects to appear to share the same File GUID & Local ID, and identifies the actual UnityEngine.Object to load by a string Variant ID.(AssetBundle Variants的主要目的是用于动态适应一些运行时的设置。AssetBundle Variants允许两个Asset Object拥有同样的File GUID …& Local ID,但可以通过string Variant ID加载特定的Asest Object)

什么情况下适合使用AssetBundle Variants?

  1. Variants simplify the loading of AssetBundles appropriate for a given platform(用于加载特定平台的对应AssetBundle)
  2. Variants allow an application to load different content on the same platform, but with different hardware.(针对不同硬件相同平台加载对应的content资源)

那么AssetBundle Variants有哪些限制?
A key limitation of the AssetBundle Variant system is that it requires Variants to be built from distinct Assets.(最大的限制就是AssetBundle Variant要求不同的Variants必须编译到不同的Asset。这样一来会导致重复的资源打包(e.g比如两个不同的Texture Variant只是import设置不一样也必须编译两份))

另一个AssetBundle需要关注的点就是Compressed or Uncompressed?
那么如何在Compressed和Uncompressed之间抉择了?主要关注以下几点:

  1. 加载速度。
    压缩与否影响AssetBundle的资源大小,同时也影响加载的时候的加载速度。
  2. AssetBundle编译时间。
    同时压缩的话也会导致Build AssetBundle的时间变长。
  3. 程序大小
    一些AssetBundle是伴随Application打包发布,会影响程序初始大小
  4. 内存使用
    不同的压缩算法对加载时对内存的影响也不一样,LZ4压缩算法和未压缩的方式允许AssetBundle无需解压缩就能访问使用(节约内存)。
  5. AssetBundle下载时间
    AssetBundle资源的大小也同时影响AssetBundle的下载时间。

更多学习参考
AssetBundles

具体现有的完美AssetBundle使用方案,AssetBundle Manager on Bitbucket
接下来以学习使用AssetBundle Manager来理解AssetBundle里的一些相关知识和概念。
首先来看看什么是AssetBundle Manager?
The AssetBundle Manager is a downloadable package that can be installed in any current Unity project and will provide a High-level API and improved workflow for managing AssetBundles.(可以看出AssetBundle Manager为我们提供了更高层的AssetBundle管理的抽象,更方便使用和管理AssetBundle,作为免费的第三方插件在Unity Asset Store可以下载使用)
AssetBundle Manager Download

那么AssetBundle Manager能做到什么?
The AssetBundle Manager helps manage the key steps in building and testing AssetBundles. The key features provided by the AssetBundle Manager are a Simulation Mode, a Local AssetBundle Server and a quick menu item to Build AssetBundles to work seamlessly with the Local AssetBundle Server.(在AssetBundle里,最令人头疼的是编译和测试(需要不断编译然后上传然后测试)。AsestBundle Manager为我们提供了本地AssetBundle Server模拟的方案,还有快速编译打包AssetBundle的菜单,让我们可以快速的编译测试AssetBundle)
AssetBundleManagerQuickMenuAssetBundleManagerQuickMenu
Simulation Mode:
When enabled, allows the editor to simulate AssetBundles without having to actually build them. The editor looks to see which Assets are assigned to AssetBundles and uses these Assets directly from the Project’s hierarchy as if they were in an AssetBundl.(当模拟模式开启的时候,editor可以通过不编译AssetBundle就能模拟AssetBundles的使用(直接使用指定了AssetBundle name的Assets),这样一来在Editor下就能快速的修改测试,无需每次编译AssetBundle)

Local Asset Server:
作为AssetBundle里重要的功能之一: AsssetBundle Variant
AssetBundle Manager也支持了快速方便的AssetBundle Variant测试。
通过Local Asset Server的本地模拟方式测试。(同时Local Asset Server还支持真机测试)
Note:
When Local Asset Server is enabled, AssetBundles must be built and placed in a folder explicitly called “AssetBundles” in the root of the Project, which is on the same level as the “Assets” folder.(当Local Asest Server开启的时候,AssetBundles必须编译放置在Assets/AssetBundles目录下)

Build AssetBundles:
快速编译打包AssetBundles。

实战学习使用AssetBundle Manager:
首先粗略的了解下AssetBundle Manager提供的一些API:
Initialize() — Initializes the AssetBundle manifest object.(初始化AssetBundle Manifest Object)
LoadAssetAsync() — Loads a given asset from a given AssetBundle and handles all the dependencies.(加载特定Asset,并负责处理器所有的dependencies)
LoadLevelAsync() — Loads a given scene from a given AssetBundle and handles all the dependencies.(加载特定scene,并负责处理所有的dependencies)
LoadDependencies() — Loads all the dependent AssetBundles for a given AssetBundle.(加载AssetBundle所依赖的所有dependencies)
BaseDownloadingURL — Sets the base downloading url which is used for automatic downloading dependencies.(设置dependencies下载url)
SimulateAssetBundleInEditor — Sets Simulation Mode in the Editor.(设置editor的模拟模式)
Variants — Sets the active variant.(设置激活的variants)
RemapVariantName() — Resolves the correct AssetBundle according to the active variant.

Loading Assets(AssetLoader.unity):
待续……

Resource LifeCycle

在得出如何利用Resources和AsetBundle高效管理资源方案之前,我们需要了解Resource的Lifecycle。
还记得前面提到的Asset和Obejct是如何被Unity记录下来的吗?
通过File GUID进行Asset标识(存储在.meta文件里,还存储了导入配置信息),通过Local ID对Object标识(存储在Asset文件自身,还存储了Object具体的配置信息)。
然后Unity通过Instance ID cache system管理着Instance ID到File GUID和Local ID(用于标识Asset的Obejct)的映射去查询访问每一个Object。

那么Resources Lifecycle(UnityEngine.Object)具体是怎样的了?
程序启动时会去加载所有场景里引用的Object的Instance ID,后续程序动态加载或则通过AssetBundle加载资源的时候会去更新新的Instance ID。

Two ways to load UnityEngine.Objects(加载Object):

  1. Automatically — An Object is loaded automatically whenever the instance ID mapped to that Object is dereferenced(间接引用)
  2. Explicitly — Resource-loading API(e.g. AssetBundle.LoadAsset)

那么Object什么情况下才会被加载到游戏里了?
An Object will be loaded on-demand the first time its Instance ID is dereferenced if two criteria are true:(当Instance ID被间接引用同时满足以下两个条件的时候,Object会被加载)

  1. The Instance ID references an Object that is not currently loaded(Instance ID引用的Object还没加载)
  2. The Instance ID has a valid File GUID and Local ID registered in the cache(Instance ID拥有的File GUID和Local ID已经存在于cache里)

什么情况下,Object会被unloaded了?
Objects are unloaded in three specific scenarios(Object被Unloaded的三种情况):

  1. Objects are automatically unloaded when unused Asset cleanup occurs.(比如Application.LoadLevel() Rersources.UnloadUnusedAssets()调用的时候,Object会被自动unloaded)
  2. Objects sourced from the Resources folder can be explicitly unloaded by invoking the Resource.UnloadAsset API.(主动调用Resource API去unload resoures下的object)
  3. Objects source from Asset Bundles are automatically and immediately unloaded when invoking the AssetBundle.Unload(true) API.(这样会导致AssetBundle里的Objects InstanceID的引用无效)
    具体Resource API如何影响Object的的生命周期,还需进一步学习,参考文档AssetBundle

知道了Resources的生命周期和如何被映射缓存的,那么如何才能以高效的方式存储resources了?
Loading Large Hierarchies(当我们制作一个复杂的Resources时):
“When serializing hierarchies of Unity GameObjects (such as when serializing prefabs), it is important to remember that the entire hierarchy will be fully serialized.”(当序列化Unity GameObject的时候,所有存在于hierarchy下的GameObject都会被一一序列化。)

When creating any GameObject hierarchy, CPU time is spent in several different ways:

  1. Time to read the source data (from storage, from another GameObject, etc.)
  2. Time to set up the parent-child relationships between the new Transforms
  3. Time to instantiate the new GameObjects and Components
  4. Time to awaken the new GameObjects and Components
    我们的关注点放到第一点上,数据的读写方式对后面三点影响不大,第一点跟数据的读写方式和数据的大小紧密相关。
    “On all current platforms, it is considerably faster to read data from elsewhere in memory rather than loading it from a storage device. “(所有平台上,从内存中读取都比从存储设备去读取快,当然不同的平台的读取速度会有一些差别)

我们前面提到当由复杂结构的GameObject的时候,所有对象都会被单独序列化(无论是否重复),这样一来会导致数据量很大,在加载的时候很慢。为了提高速度,我们可以通过把复杂的GameObject划分为多个单独的小的Prefab,然后通过实例化多个Prefab来构建我们的GameObject而非完全依赖于Unity的Serialization和prefab system。(减少了数据量。同时一旦Prefab被加载后,从内存中读取就比从硬件设备读取快多了)

未完,待续…..

0 0