Unity-Assets/Resources and AssetBundles

1.1.Inside Assets and Objects

Distinction betwwen Assets and UnityEngine.Objects.
An Asset is a file on disk,stored in the Assets folder of a Unity project. Texture,3D models , or audio clips are common types of Assets.
A UnityEngine.Object, or Object is a set of serialized data collectively describing a specific instance of a resource.All objects are subclasses of the UnityEngine.Object base class.

Two special types:

  1. A ScriptableObject provides a convenient system for developers to define their own data types. These types can be natively serialized and deserialized by Unity, and manipulated in the Unity Editor's Inspector window.
  2. A MonoBehaviour provides a wrapper that links 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.

1.2.Inter-Object references

All UnityEngine.Objects can have references to other UnityEngine.Obejcts.These other Objects may reside within the same Asset file, or may be imported from other Asset files.
When serialized, these references consist of two separate pieces of data: a File GUID and a Local ID.
The file GUID identifies the Assets file where the target resource is stored.
A locally unique local ID identifies each Object within an Asset file because an Asset file may contain multiple Objects
File GUIDs are stored in .meta files.These .meta files are generated when Unity first imports an Assets, and are stored in the same directory as the Asset.

1.3.Why File GUIDs and Local IDs?

文件 GUID 提供了文件位置的抽象。只要文件 GUID 和一个文件关联上,那文件在磁盘上的位置就变得无关紧要了。这个文件可以随意移动,而不必更新所有引用了该文件的对象。
一个资产文件可能包含多个 UnityEngine.Object,为了清楚的区分它们,需要本地 ID。
Unity 编辑器拥有已知文件 GUID 到文件路径的映射。这个映射实体会把资产的文件路径和文件 GUID 关联起来。如果 Unity 编辑器打开时,一个 .meta 文件丢失而资产的路径并没有改变的资产,编辑器会确保这个资产得到相同的文件 GUID。如果Unity编译器关闭的时候.meta文件丢失或.meta文件没有随着资源文件一起移动,将会导致资源引用的地方出现问题。

1.4.Composite Assets and importers

non-native Asset types must be imported into Unity.This is done via an asset importer.The result of the import process is one or more UnityEngine.Objects. These are visible in the Unity Editor as multiple sub-assets within the parent Asset,such as multiple sprites nested beneath a texture Asset that has been imported as a sprite atlas. Each of these Objects will share a File GUID as their source data is stored within the same Asset file.
导入过程会将源资产文件转换成在 Unity 编辑器中选中的目标平台合适的格式。导入过程也可能会带有比较重的操作,比如纹理压缩。如果每次 Unity 编辑器打开的时候都要执行导入过程的话会是 Unity 编辑器变得特别没有效率。
作为解决方案,Unity 会讲资产导入后的结果缓存到 Libraray 文件夹。导入后的结果会缓存到以资产的文件 GUID 前两个字母命名的文件夹中。这个文件夹在 Library/metadat/ 文件夹内。每个独立的对象都会被序列化为单独的以它们资产文件 GUID 命名的二进制文件。

1.5.Serialization and instances

GUID的比较是比较慢的,这需要一个在运行时更高效的系统,Unity内部维持了一个能把文件GUID和本地ID换成在独立会话内唯一的,简单的数字的缓存。这个数字叫做实例ID。当新的对象注册到缓存时,会给它分配一个严格递增的值。
这个缓存维护了给定的实例ID、对象源文件中定义的文件GUID和本地ID和内存中对象的映射关系。它让UnityEngine.Objects稳定的维护的各个对象间的引用成为可能。通过一个示例ID的引用可以快速的返回这个ID对应的对象。如果这个对象没有加载,Unity可以根据FileID和本地ID来实时加载对象。

At startup, the Instance ID cache is initialized with data for all Objects immediately required by the project (i.e., referenced in built 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 and when Objects are loaded from AssetBundles. Instance ID entries are only removed from the cache when an AssetBundle providing access to a specific File GUID and Local ID is unloaded.When this occurs, the mapping between the Instance ID, its File GUID and Local ID are 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.
On specific platforms, certain events can force Objects out of memory. For example, graphical Assets can be unloaded from graphics memory on iOS when an app is suspended. If these Objects originated in an AssetBundle that has been unloaded, Unity will be unable to reload the source data for the Objects. Any extant references to these Objects will also be invalid. In the preceding example, the scene may appear to have invisible meshes or magenta textures.

1.6.MonoScripts

MonoBehaviour拥有一个MonoScript,MonoScript仅包含简单的用来定位特定脚本的信息比较重要。构建项目的时候,Unity收集所有Assets文件下零散放置的脚本,然后将他们编译成Mono程序集。Unity会为Assets文件夹下的不同语言和Assets/Plugins文件夹下的脚本构建单独的程序集。在Plugins子文件夹外的C#脚本会编译到Assembly-CSharp.dll中,而Plugins子文件夹内的脚本会编译到Assembly-CSharp-firstpass.dll中。
这些程序集会被包含到Unity应用的最终构建里面。他们也是MonoScript引用的程序集。与其他资源不同,所有Unity程序内的程序集会在程序第一次启动时加载。
?因为有 MonoScript 对象,AssetBundle(或者是场景文件,或者是预设)中 MonoBehaviour 组件可以不包含实际运行代码。这使得不同的 MonoBehaviour 可以指向特定的共享的类,即使这些不同的 MonoBehaviour 在不同的 AssetBundle 中。

1.7.Resource的生命周期

UnityEingine.Objects会在具体或者特定的时间从内存中加载/卸载。
有两种方式可以加载UnityEngine.Objects: 自动的和显式的。当一个实例ID映射到一个源数据存在,但是没加载到内存并被间接引用的对象时,对象会被自动创建。
对象可以在Script中显式加载。显示加载方式要可以使直接创建他们,也可以是通过资源加载的API,例如AssetBundle.LoadAsset。
当一个对象被加载,Unity会尝试将所有引用就从文件GUID和本地ID转换成实例ID.

Understand the resource lifecycle of UnityEngine.Objects.Objects are loaded into/unloaded from memory at specific and defined times.
An Object is loaded automatically when:
1.The Instance ID mapped to that Object is dereferenced
2.The Object is currently not loaded into memory
3.The Object's source data can be located

当满足下面两个条件时,一个对象在它的实例ID第一次引用是按需加载:
1.实例ID引用了没有加载的对象
2.实例ID在缓存中有效的、对应文件GUID和本地ID

如果一个文件GUID和本地ID不包含实例ID,或者一个实例ID关联一个引用无效的文件GUID和本地ID的未加载的对象,实例ID引用将会保留但是世纪对象缺少不能加载。这个在Unity编译器里面显示为(Missing)。在程序运行时或者场景视图里,基于(Missing )对象的类型,会有下面几种显示:比如网格不可见,纹理显示成洋红色。

Objects are unloaded in three specific scenarios:
1.Objects are automatically unloaded when Asset cleanup occurs.This process is triggered automatically when scenes are changed destructively (i.e. when SceneManager.LoadScene is invoked non-additively), or when a script invokes the Resources.UnloadUnusedAssets API.This process only unloads unreferenced Objects; an Object will only be unloaded if no Mono variable holds a reference to the Object, and there are no other live Objects holding references to the Object. Furthermore, note that anything marked with HideFlags.DontUnloadUnusedAsset and HideFlags.HideAndDontSave will not be unloaded.
场景变化卸载
2.Objects sourced from the Resources folder can be explicitly unloaded by invoking the Resources.UnloadAsset API. The Instance ID for these Objects remains valid and will still contain a valid File GUID and LocalID entry. If any Mono variable or other Object holds a reference to an Object that is unloaded with Resources.UnloadAsset, then that Object will be reloaded as soon as any of the live references are dereferenced.
Resource卸载
3.Objects sourced from AssetBundles are automatically and immediately unloaded when invoking the AssetBundle.Unload(true) API. This invalidates the File GUID and Local ID of the Object's Instance ID, and any live references to the unloaded Objects will become "(Missing)" references. From C# scripts, attempting to access methods or properties on an unloaded object will produce a NullReferenceException.
AB对象卸载
If AssetBundle.Unload(false) is called, live Objects sourced from the unloaded AssetBundle will not be destroyed, but Unity will invalidate the File GUID and Local ID references of their Instance IDs. It will be impossible for Unity to reload these Objects if they are later unloaded from memory and live references to the unloaded Objects remain.
(Note: The most common case where Objects are removed from memory at runtime without being unloaded occurs when Unity loses control of its graphics context. This may occur when a mobile app is suspended and the app is forced into the background. In this case, the mobile OS usually evicts all graphical resources from GPU memory. When the app returns to the foreground, Unity must reload all needed Textures, Shaders and Meshes to the GPU before scene rendering can resume.)

1.8.Loading large hierarchies

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

  • Reading the source data 读取源数据
  • Setting up the parent-child relationships between the new Transforms 构建新的Transform间父-子关系
  • Instantiating the new GameObjects and Components 实例化新的游戏对象和组件
  • Awakening the new GameObjects and Components on the main thread 在主线程中激活新游戏对象和组件时间
    后三种时间花费一般时不变的,不论是从现成结构中或者从存储中加载。但是读取源数据的时间与层次结构中的组件和游戏对象成线性增加的关系,当然还要乘以读取源数据的速度。
    在当前的全平台中,从内存中读取数据要更快比从硬盘中读取数据。在读取较慢的平台,从硬盘中读取prefab序列化的数据将超过实例化prefab的时间。

As mentioned before, when serializing a monolithic prefab, every GameObject and component's data is serialized separately, which may duplicate data. For example, a UI screen with 30 identical elements will have the identical element serialized 30 times, producing a large blob of binary data. At load time, the data for all of the GameObjects and Components on each one of those 30 duplicate elements must be read from disk before being transferred to the newly-instantiated Object. This file reading time is a significant contributor to the overall cost of instantiating large prefabs. Large hierarchies should be instantiated in modular chunks, and then be stitched together at runtime.

关于实例化的优化:
Unity 5.4 note: Unity 5.4 altered the representation of transforms in memory. Each root transform's entire child hierarchy is stored in compact, contiguous regions of memory. When instantiating new GameObjects that will be instantly reparented into another hierarchy, consider using the new GameObject.Instantiate overloaded variants which accept a parent argument. Using this overload avoids the allocation of a root transform hierarchy for the new GameObject. In tests, this speeds up the time required for an instantiate operation by about 5-10%.

3.The Resources folder

2.1.Best Practices for the Resources System

Don't use it.

  • Resources folder 让内存管理变得更加困难。
  • 不恰当的使用Resources文件增加application的启动时间和包的大小。
  • Resources System将降低项目自定义平台的能力并且消除了增量更新的可能性。

2.2.Proper uses of the Resources system

  • 在项目原型阶段使用Resources 文件进行开发,当项目进入full production阶段,Resources文件应该被消除。
  • 在整个项目周期中经常被使用
  • 占用内存很小
  • 不需要升级更新
  • Used for minimal bootstrapping

2.3.Serialization of Resources

The Assets and Objects in all folders named "Resources" are combined into a single serialized file when a project is built. This file also contains metadata and indexing information, similar to an AssetBundle.
On most platforms, the lookup data structure is a balanced search tree, which has a construction time that grows at an O(n log(n)) rate. This growth also causes the index's loading time to grow more-than-linearly as the number of Objects in Resources folders increases.

AssetBundle fundamentals

一个AssetBundle包括两个部分:a header and data segment
The header contains information about the AssetBundle,such as its identifiers,compression type, and 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. On most platforms, this lookup table is implemented as a balanced search tree.

3.3.Loading AssetBundles

AB can be loaded via four distinct APIs. The behavior of these four APIs is different depending on two criteria:

  • Whether the AB is LZMA compressed,LZ4 compressed or uncompressed
  • The platform on which the AssetBundle is being loaded.

These APIs are:

  • AssetBundle.LoadFromMemory(Async optional)
  • AssetBundle.LoadFromFile(Async optional)
  • UnityWebRequest's DownloadHandlerAssetBundle
  • WWW.LoadFromCacheOrDownload (on Unity 5.6 or older)

3.3.1 AssetBundle.LoadFromMemory(Async)

Unity's recommendation is not to use this API.
AssetBundle.LoadFromMemoryAsync 从托管代码字节数组(C# 中的 Btye[])中加载 AssetBundle。它总是会从本地内存中开辟一段连续内存,然后从托管代码的字节数组中拷贝源数据到这段新分配的内存中。如果 AssetBundle 是 LZMA 压缩格式的,拷贝过程中 AssetBundle 会被解压。而 LZ4 压缩格式的 AssetBundle 会原原本本的拷贝过去。

AssetBundle.LoadFromMemoryAsync 从托管代码字节数组(C# 中的 Btye[])中加载 AssetBundle。它总是会从本地内存中开辟一段连续内存,然后从托管代码的字节数组中拷贝源数据到这段新分配的内存中。如果 AssetBundle 是 LZMA 压缩格式的,拷贝过程中 AssetBundle 会被解压。而 LZ4 压缩格式的 AssetBundle 会原原本本的拷贝过去。

3.3.2. AssetBundle.LoadFromFile

从本地存储中高效的加载未压缩的AssetBundle。如果AssetBundle未压缩或者使用LZ4压缩,这个API有如下表现。
移动设备:API只会加载AssetBundle的Header,其他数据保留在磁盘中。当调用加载的方法或者他们实例ID被间接引用时对象会被按需加载。在这种情况下没有额外的内存开销。

Unity编译器:这个API会将整个AssetBundle加载进内存,而不像从磁盘上读取所有字节,使用AssetBundle.LoadFromMemoryAsync。

3.3.3. AssetBundleDownloadHandler

The UnityWebRequest API allows developers to specify exactly how Unity should handle downloaded data and allows developers to eliminate unnecessary memory usage.
The simplest way to download an AssetBundle using UnityWebRequest is call UnityWebRequest.GetAssetBundle.下载一个AssetBundle

LZMA压缩的AB包将解压然后LZ4再压缩一下,
下载完成后,assetBundle属性提供了对下载数据中AssetBundle的访问。

If caching information is provided to a UnityWebRequest object, and the requested AssetBundle already exists in Unity's cache, then the AssetBundle will become available immediately and this API will operate identically to AssetBundle.LoadFromFile.

3.3.4. WWW.LoadFromCacheOrDownload

WWW.LoadFromCacheOrDownload is an API that allows loading of Objects from both remote servers and local storage. Files can be loaded from local storage via a file:// URL. If the AssetBundle is present in the Unity cache, this API will behave exactly like AssetBundle.LoadFromFile.

3.3.5. Loading Assets From AssetBundles

  • LoadAsset (LoadAssetAsync)
  • LoadAllAssets (LoadAllAssetsAsync)
  • LoadAssetWithSubAssets (LoadAssetWithSubAssetsAsync)
    Asynchronous loads will load multiple Objects per frame, up to their time-slice limits.

3.4.1. 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.Unity中不涉及线程的部分将再worker中被转换。
对象的加载时并行的,多对象可以被序列化,处理和继承多个对象。当对象完成加载,它的Awake对调将会调用,对象再下一帧将可用。

AssetBundle.load方法将暂停主线程直到Object加载完成。它们会将对象加载按时间分片,使对象集成不超过一定毫秒数量的帧时间。
这个毫秒数量靠下面属性设置。
Application.backgroundLoadingPriority

ThreadPriority.High: 每帧最多 50 毫秒
ThreadPriority.Normal: 每帧最多 10 毫秒
ThreadPriority.BelowNormal: 每帧最多 4 毫秒
ThreadPriority.Low: 每帧组多 2 毫秒

3.4.4. Recommendations

In many cases, it is preferable to load as many needed Objects as possible before players enter performance-critical areas of an application, such as the main game level or world. This is particularly critical on mobile platforms, where access to local storage is slow and the memory churn of loading and unloading Objects at play-time can trigger the garbage collector.
For projects that must load and unload Objects while the application is interactive, see the Managing loaded assets section of the AssetBundle usage patterns step for more information on unloading Objects and AssetBundles.

AssetBundle usage patterns

管理加载后的资产

大多数项目应该使用 AssetBundle.Unload(true) 并且使用方法确保对象不会有重复副本。有两种通用的方法:
1.在应用生命周期中,临时 AssetBundle 卸载有明确定义的点,比如两个关卡之间或者加载场景的时候。
2.维护单个物体的引用计数,并当组成 AssetBundle 的对象都未被使用时卸载 AssetBundle。

如果必须用AssetBundle.Unload(false), 则单个对象能通过下面两种方式卸载:
1.在场景和代码中删除不需要对象的所有引用。完成之后调用 Resources.UnloadUnusedAssets
2.非增量方式加载场景。这个行为会卸载当前场景中的所有对象,然后自动的调用 Resources.UnloadUnusedAssets()

如果 Unity 必须重新从已经卸载的 AssetBundle 中加载对象,还有一种问题会出现。这种情况是, 对象加载失败,对象在 Unity 编辑器的 Hierarchy 中显示为(Missing)。

这主要发生在 Unity 丢失和拿回它的图形上下文控制权时,比如一个移动应用被挂起或者用户 PC 端锁屏。在这种情况下,Unity 必须给 GPU 重新上传纹理和 shader。如果上传的资产的源 AssetBundle 不可用,应用就会将场景中的对象显示成丢失 Shader 的洋红色的对象。

4.2.1. 随项目一起打包

AssetBundle 跟随项目一起打包是部署他们的最简单的方式,因为不需要额外的下载管理代码。
Streaming Assets

在 Unity 程序安装时包括各种类型内容的最简单的方法,是在构建项目之前将内容构建入 /Assets/StreamingAssets/ 文件夹内。所有在 StreamingAssets 文件夹内的文件都会在项目构建的时候被拷贝到最终程序包里。StreamingAssets 文件夹可以用来存储最终程序包内的各种内容,而不仅仅是 AssetBundle。
StreamingAssets 文件夹在本地存储中的全路径,可以在运行期时通过属性 Application.streamingAssetsPath 得到。AssetBundle 之后在大多数平台上都可以通过 AssetBundle.LoadFromFile 来加载。

安装之后下载
最简单地交付 AssetBundle 是将它们放到一个网络服务器上,然后通过 WWW.LoadFromCacheOrDownload 或者 UnityWebRequest 来下载它们。Unity 会在本地存储上自动的缓存已下载的 AssetBundle。如果下载的 AssetBundle 是 LZMA 压缩格式,为了之后更快的加载,它会以未压缩格式存储在缓存中。如果下载的 AssetBundle 是 LZ4 压缩格式,则会保持压缩格式存储在缓存中。

不适用 UnityWebRequest 或者 WWW.LoadFromCacheOrDownload 的情况实例:

  • 对AssetBundle cache 细微的控制
  • 实现定制的压缩策略
  • 希望项目使用平台特定API去实现特定需求
  • 当AB必须使用SSL在Unity不完全支持SSL的时候

内置缓存
Unity中有一个可以用来缓存WWW或者UnityWebRequest下载的AB的缓存系统。
这两个 API 都有接收 AssetBundle 版本号为参数的函数重载。这个版本号不是保存在 AssetBundle 里面,也不是由 AssetBundle 系统生成。
缓存系统会一直跟踪传递给 WWW.LoadFromCacheOrDownload 和 UnityWebRequest 的版本号。当带着版本号调用两者其中之一时,缓存系统会检查是否有缓存过的 AssetBundle。缓存系统会比较首次缓存时被传递的版本号和当前传递的版本号。如果两个版本号不匹配,或者没有缓存过的 AssetBundle,Unity 会下载一个新的副本,然后将其与新的版本号关联。
AssetBundles in the caching system are identified only by their file names, and not by the full URL from which they are downloaded. This means that an AssetBundle with the same file name can be stored in multiple different locations, such as a Content Delivery Network. As long as the file names are identical, the caching system will recognize them as the same AssetBundle.

4.2.3. 定制下载器

here are four major considerations when writing a custom downloader:
Download mechanism
Storage location
Compression type
Patching

4.3.Asset Assignment Strategies(资产分配策略)

The key decision:

  • Logical entities
  • Object Types
  • Concurrent content

4.4.Common pitfalls

4.5.1.Asset duplication
没有被显式分配到 AssetBundle 的对象会被打包到拥有一个或者多个没有标签的对象的 AssetBundle 中。

如果两个不同的对象被分配到不同的 AssetBundle ,而它们都引用了共同依赖对象,然后这个共同的对象会被拷贝到每个 AssetBundle 中。重复的依赖对象会被实例化,这意味着这些依赖对象的拷贝会被认为是拥有不同标识不同对象。这会增加应用的 AssetBundle 的总大小。这也让加载这两个不同对象所在的 AssetBundle 时,它们会被加载进内存中。

几种方式应对这种问题:

  • 确保被打包进 AssetBundle 中的不同对象不会有同样的依赖。任何跟其他对象没有共同依赖的对象都会打包到 AssetBundle 中,而不同重复拷贝依赖。

    这种方法对有很多共享依赖的项目不太合适。它会产生的巨大的 AssetBunle,而且这个 AssetBunle 必须频繁地重建和下载,不方便而又低效。

  • AssetBundle 分片,这样就不会同时有两个有共同依赖的 AssetBundle 会被加载

    这个方法可能只对某些项目管用,比如基于关卡的游戏。但是它会给项目增加不必要的 AssetBundle 大小和增加编译与加载时间。

  • 把所有的依赖都打包到依赖他们的 AssetBundle 中。这完全地排除了冗余资产的风险,但是它也引入了复杂性。应用程序必须 AssetBundle 间的依赖,来确保在调用 AssetBundle.LoadAsset API 前加载了正确的 AssetBundle。

Object dependencies are tracked via the AssetDatabase API, located in the UnityEditor namespace. As the namespace implies, this API is only available in the Unity Editor and not at runtime. AssetDatabase.GetDependencies can be used to locate all of the immediate dependencies of a specific Object or Asset.

通过 AssetDatabase 和 AssetImporter API 的组合使用,让编程用来确保一个 AssetBundle 的所有直接或者间接的依赖都指派到了同一个 AssetBundle 上,或者不存在没有指派到 AssetBundle 的依赖被两个 AssetBundle 共享着。出于对重复资源内存消耗考虑,建议所有的项目都有这种的编辑器脚本。

4.5. AssetBundle Variants

A key feature of the AssetBundle system is the introduction of 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. Conceptually, 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.
There are two primary use cases for this system:

  • Variants simplify the loading of AssetBundles appropriate for a given platform.
  • Variants allow an application to load different content on the same platform, but with different hardware.

压缩还是不压缩

是否要压缩 AssetBundle 需要仔细的考虑,重点问题有几个:

* 加载时间 是不是这个 AssetBundle 的主要因数?从磁盘或缓存中加载没有压缩的 AssetBundle 会比压缩的 AssetBundle 快很多。但是通常从服务器上下载一个压缩的文件会比一个未压缩的 AssetBundle 快。
* 编译时间 是不是这个 AssetBundle 的主要因数?LZMA 和 LZ4 在压缩式很慢,并且 Unity 编辑器会序列化 AssetBundle。有很多 AssetBundle 的项目会在压缩上花费很长的时间。
* 程序大小 是不是主要因数?如果 AssetBundle 是跟程序一起发布,压缩他们会减少包体的大小。另外 AssetBundle 可以在程序安装后安装。
* 内存使用 是不是主要因数?在 Unity 5.3 之前,所有 Unity 的解压机制都要求解压前将整个 AssetBundle 都加载到内存中。如果内存使用率比较重要,请使用 LZ4 压缩 AssetBundles 或者不压缩 AssetBundle。
* 下载时间 是不是主要因数?压缩仅在 AssetBundle 比较大或者用户在带宽有限的环境中才需要,比如移动端通过 3G 下载或者在低速连接中。如果只有几十 M 的数据需要传输到 PC 或者在高速连接中,可以把压缩去掉。

参考资料:
http://blog.shuiguzi.com/2017/04/18/AssetBundle_usage_pattern_1/

你可能感兴趣的:(Unity-Assets/Resources and AssetBundles)