Unity学习笔记 AB包(看了几眼)

从官网复制黏贴的 Unity之AB包

AssetBundle 工作流程

要开始使用 AssetBundle,请按照以下步骤操作。有关每个工作流程的更多详细信息,请参阅本文档这一部分的其他页面。

为 AssetBundle 分配资源

  1. 要为 AssetBundle 分配指定资源,请按照下列步骤操作:
  2. 从 Project 视图中选择要为捆绑包分配的资源。
  3. 在 Inspector 中检查对象。
  4. 在 Inspector 底部,有一个用于分配 AssetBundle 和变体的部分。可使用左侧下拉选单分配 AssetBundle,而使用右侧下拉选单分配变量。
  5. 单击左侧下拉选单的 None 以显示当前注册的 AssetBundle 名称。
  6. 单击 New 以创建新的 AssetBundle 1.输入所需的 AssetBundle 名称。**注意:**AssetBundle 名称支持某种类型的文件夹结构,具体取决于您输入的内容。要添加子文件夹,请用 / 分隔文件夹名称。例如,使用 AssetBundle 名称 environment/forestenvironment 子文件夹下创建名为 forest 的捆绑包
  7. 一旦选择或创建了 AssetBundle 名称,便可以重复此过程在右侧下拉选单中分配或创建变体名称(如果需要)。构建 AssetBundle 不需要变体名称

要阅读有关 AssetBundle 分配和相关策略的更多信息,请参阅关于为 AssetBundle 准备资源的文档。

构建 AssetBundle

在 Assets 文件夹中创建一个名为 Editor 的文件夹,并将包含以下内容的脚本放在该文件夹中:

using UnityEditor;
using System.IO;

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

此脚本将在 Assets 菜单底部创建一个名为 Build AssetBundles 的菜单项,该菜单项将执行与该标签关联的函数中的代码。单击 Build AssetBundles 时,将随构建对话框一起显示一个进度条。此过程将会获取带有 AssetBundle 名称标签的所有资源,并将它们放在 assetBundleDirectory 定义的路径中的文件夹中。

如需了解与此相关的更多详细信息,请参阅关于构建 AssetBundle 的文档。

加载 AssetBundle 和资源

如果您想从本地存储中加载,请使用 AssetBundles.LoadFromFile API,如下所示:

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

LoadFromFile 获取捆绑包文件的路径。

如果是您自己托管 AssetBundle 并需要将它们下载到应用程序中,请使用 UnityWebRequest API。下面是一个示例:

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

GetAssetBundle(string, int) 获取 AssetBundle 的位置 URL 以及要下载的捆绑包的版本。此示例仍然指向一个本地文件,但 string url 可以指向托管 AssetBundle 的任何 URL。

UnityWebRequest 有一个特定的句柄来处理 AssetBundle:DownloadHandlerAssetBundle,可根据请求获取 AssetBundle。

无论使用哪种方法,现在都可以访问 AssetBundle 对象了。对该对象需要使用 LoadAsset<T>(string),此函数将获取您尝试加载的资源的类型 T 以及对象的名称(作为捆绑包内部的字符串)。这将返回从 AssetBundle 加载的任何对象。可以像使用 Unity 中的任何对象一样使用这些返回的对象。例如,如果要在场景中创建游戏对象,只需调用 Instantiate(gameObjectFromAssetBundle)

有关用于加载 AssetBundle 的 API 的更多信息,请参阅关于本机使用 AssetBundle 的文档。

为 AssetBundle 准备资源(这个很重要 )

使用 AssetBundle 时,可以任意将任何资源分配给所需的任何捆绑包。但是,在设置捆绑包时需要考虑某些策略。以下分组策略旨在用于您认为适合的具体项目。可以根据需要随意混合和搭配这些策略。

逻辑实体分组

逻辑实体分组是指根据资源所代表的项目功能部分将资源分配给 AssetBundle。这包括各种不同部分,比如用户界面、角色、环境以及在应用程序整个生命周期中可能经常出现的任何其他内容。

示例

  • 捆绑用户界面屏幕的所有纹理和布局数据
  • 捆绑一个/一组角色的所有模型和动画
  • 捆绑在多个关卡之间共享的景物的纹理和模型

逻辑实体分组非常适合于可下载内容 (DLC),因为通过这种方式将所有内容隔离后,可以对单个实体进行更改,而无需下载其他未更改的资源。

为了能够正确实施此策略,最大诀窍在于,负责为各自捆绑包分配资源的开发人员必须熟悉项目使用每个资源的准确时机和场合。

类型分组

根据此策略,可以将相似类型的资源(例如音频轨道或语言本地化文件)分配到单个 AssetBundle。

要构建供多个平台使用的 AssetBundle,类型分组是最佳策略之一。例如,如果音频压缩设置在 Windows 和 Mac 平台上完全相同,则可以自行将所有音频数据打包到 AssetBundle 并重复使用这些捆绑包,而着色器往往使用更多特定于平台的选项进行编译,因此为 Mac 构建的着色器捆绑包可能无法在 Windows 上重复使用。此外,这种方法非常适合让 AssetBundle 与更多 Unity 播放器版本兼容,因为纹理压缩格式和设置的更改频率低于代码脚本或预制件。

并发内容分组

并发内容分组是指将需要同时加载和使用的资源捆绑在一起。可以将这些类型的捆绑包用于基于关卡的游戏(其中每个关卡包含完全独特的角色、纹理、音乐等)。有时可能希望确保其中一个 AssetBundle 中的资源与该捆绑包中的其余资源同时使用。依赖于并发内容分组捆绑包中的单个资源会导致加载时间显著增加。您将被迫下载该单个资源的整个捆绑包。

并发内容分组捆绑包最常见的用例是针对基于场景的捆绑包。在此分配策略中,每个场景捆绑包应包含大部分或全部场景依赖项。

请注意,项目绝对可以也应该根据您的需求混用这些策略。对任何给定情形使用最优资源分配策略可以大大提高项目的效率。

例如,一个项目可能决定将不同平台的用户界面 (UI) 元素分组到各自的 Platform-UI 特定捆绑包中,但按关卡/场景对其交互式内容进行分组。

无论遵循何种策略,下面这些额外提示都有助于掌控全局:

  • 将频繁更新的对象与很少更改的对象拆分到不同的 AssetBundle 中
  • 将可能同时加载的对象分到一组。例如模型及其纹理和动画
  • 如果发现多个 AssetBundle 中的多个对象依赖于另一个完全不同的 AssetBundle 中的单个资源,请将依赖项移动到单独的 AssetBundle。如果多个 AssetBundle 引用其他 AssetBundle 中的同一组资源,一种有价值的做法可能是将这些依赖项拉入一个共享 AssetBundle 来减少重复。
  • 如果不可能同时加载两组对象(例如标清资源和高清资源),请确保它们位于各自的 AssetBundle 中。
  • 如果一个 AssetBundle 中只有不到 50% 的资源经常同时加载,请考虑拆分该捆绑包
  • 考虑将多个小型的(少于 5 到 10 个资源)但经常同时加载内容的 AssetBundle 组合在一起
  • 如果一组对象只是同一对象的不同版本,请考虑使用 AssetBundle 变体

构建 AssetBundle

在有关 AssetBundle 工作流程的文档中,我们有一个代码示例将三个参数传递给 BuildPipeline.BuildAssetBundles 函数。让我们深入了解一下这方面。

Assets/AssetBundles:这是 AssetBundle 要输出到的目录。可以将其更改为所需的任何输出目录,只需确保在尝试构建之前文件夹实际存在。

BuildAssetBundleOptions

可以指定几个具有各种效果的不同 BuildAssetBundleOptions。请参阅关于 BuildAssetBundleOptions 的脚本 API 参考查看所有这些选项的表格。

虽然可以根据需求变化和需求出现而自由组合 BuildAssetBundleOptions,但有三个特定的 BuildAssetBundleOptions 可以处理 AssetBundle 压缩:

  • BuildAssetBundleOptions.None:此捆绑包选项使用 LZMA 格式压缩,这是一个压缩的 LZMA 序列化数据文件流。LZMA 压缩要求在使用捆绑包之前对整个捆绑包进行解压缩。此压缩使文件大小尽可能小,但由于需要解压缩,加载时间略长。值得注意的是,在使用此 BuildAssetBundleOptions 时,为了使用捆绑包中的任何资源,必须首先解压缩整个捆绑包。
    解压缩捆绑包后,将使用 LZ4 压缩技术在磁盘上重新压缩捆绑包,这不需要在使用捆绑包中的资源之前解压缩整个捆绑包。最好在包含资源时使用,这样,使用捆绑包中的一个资源意味着将加载所有资源。这种捆绑包的一些用例是打包角色或场景的所有资源。
    由于文件较小,建议仅从异地主机初次下载 AssetBundle 时才使用 LZMA 压缩。通过 UnityWebRequestAssetBundle 加载的 LZMA 压缩格式 Asset Bundle 会自动重新压缩为 LZ4 压缩格式并缓存在本地文件系统上。如果通过其他方式下载并存储捆绑包,则可以使用 AssetBundle.RecompressAssetBundleAsync API 对其进行重新压缩。
  • BuildAssetBundleOptions.UncompressedAssetBundle: This bundle option builds the bundles in such a way that the data is completely uncompressed. The downside to being uncompressed is the larger file download size. However, the load times once downloaded will be much faster. Uncompressed AssetBundles are 16-byte aligned.
  • BuildAssetBundleOptions.ChunkBasedCompression:此捆绑包选项使用称为 LZ4 的压缩方法,因此压缩文件大小比 LZMA 更大,但不像 LZMA 那样需要解压缩整个包才能使用捆绑包。LZ4 使用基于块的算法,允许按段或“块”加载 AssetBundle。解压缩单个块即可使用包含的资源,即使 AssetBundle 的其他块未解压缩也不影响。

使用 ChunkBasedCompression 时的加载时间与未压缩捆绑包大致相当,额外的优势是减小了占用的磁盘大小。

BuildTarget

BuildTarget.Standalone:这里我们告诉构建管线,我们要将这些 AssetBundle 用于哪些目标平台。可以在关于 BuildTarget 的脚本 API 参考中找到可用显式构建目标的列表。但是,如果不想在构建目标中进行硬编码,请充分利用 EditorUserBuildSettings.activeBuildTarget,它将自动找到当前设置的目标构建平台,并根据该目标构建 AssetBundle。

一旦正确设置构建脚本,最后便可以开始构建资源包了。如果是按照上面的脚本示例进行的操作,请单击 Assets > Build AssetBundles 以开始该过程。

现在已经成功构建了 AssetBundle,您可能会注意到 AssetBundles 目录包含的文件数量超出了最初的预期。确切地说,是多出了 2*(n+1) 个文件。让我们花点时间详细了解一下 BuildPipeline.BuildAssetBundles 产生的结果。

对于在编辑器中指定的每个 AssetBundle,可以看到一个具有 AssetBundle 名称+“.manifest”的文件。

随后会有一个额外捆绑包和清单的名称不同于先前创建的任何 AssetBundle。相反,此包以其所在的目录(构建 AssetBundle 的目录)命名。这就是清单捆绑包。我们以后会对此进行详细讨论并介绍使用方法。

AssetBundle 文件

这是缺少 .manifest 扩展名的文件,其中包含在运行时为了加载资源而需要加载的内容。

AssetBundle 文件是一个存档,在内部包含多个文件。此存档的结构根据它是 AssetBundle 还是场景 AssetBundle 可能会略有不同。以下是普通 AssetBundle 的结构:

img

场景 AssetBundle 与普通 AssetBundle 的不同之处在于,它针对场景及其内容的串流加载进行了优化。

清单文件

对于生成的每个捆绑包(包括附加的清单捆绑包),都会生成关联的清单文件。清单文件可以使用任何文本编辑器打开,并包含诸如循环冗余校验 (CRC) 数据和捆绑包的依赖性数据之类的信息。对于普通 AssetBundle,它们的清单文件将如下所示:

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

其中显示了包含的资源、依赖项和其他信息。

生成的清单捆绑包将有一个清单,但看起来更可能如下所示:

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

这将显示 AssetBundle 之间的关系以及它们的依赖项。就目前而言,只需要了解这个捆绑包中包含 AssetBundleManifest 对象,这对于确定在运行时加载哪些捆绑包依赖项非常有用。要了解有关如何使用此捆绑包和清单对象的更多信息,请参阅有关本机使用 AssetBundle 的文档。

AssetBundle 依赖项(也很重要)

如果一个或多个 UnityEngine.Objects 包含对位于另一个捆绑包中的 UnityEngine.Object 的引用,则 AssetBundle 可以变为依赖于其他 AssetBundle。如果 UnityEngine.Object 包含对任何 AssetBundle 中未包含的 UnityEngine.Object 的引用,则不会发生依赖关系。在这种情况下,在构建 AssetBundle 时,捆绑包所依赖的对象的副本将复制到捆绑包中。如果多个捆绑包中的多个对象包含对未分配给捆绑包的同一对象的引用,则每个对该对象具有依赖关系的捆绑包将创建其自己的对象副本并将其打包到构建的 AssetBundle 中。

如果 AssetBundle 中包含依赖项,则在加载尝试实例化的对象之前,务必加载包含这些依赖项的捆绑包。Unity 不会尝试自动加载依赖项。

参考以下示例,Bundle 1 中的材质引用了 Bundle 2 中的纹理:

在此示例中,在从 Bundle 1 加载材质之前,需要将 Bundle 2 加载到内存中。加载 Bundle 1Bundle 2 的顺序无关紧要,重要的是在从 Bundle 1 加载材质之前应加载 Bundle 2。在下一部分,我们将讨论如何使用我们在上一部分介绍的 AssetBundleManifest 对象在运行时确定并加载依赖项。

AssetBundle 之间的重复信息

默认情况下,Unity 不会优化 AssetBundle 之间的重复信息。这意味着您项目中的多个 AssetBundle 可能包含相同信息,例如多个预制件使用的同一种材质。在多个 AssetBundle 中使用的资源被称为公共资源。这些会影响内存资源和加载时间。

本页介绍 Unity 如何管理 AssetBundle 之间的重复信息,以及您可以如何应用优化措施。

Editor 设置

默认情况下,Unity 不会执行任何优化措施来减少或最小化存储重复信息所需的内存。在创建构建版本期间,Unity 会在 AssetBundle 中对隐式引用的资源构建重复版本。 为避免发生此类重复,请将公共资源(例如材质)分配到它们自身的 AssetBundle。

例如:可能有一个应用程序包含两个预制件,这两个预制件都分配到它们自身的 AssetBundle。两个预制件共享相同材质,而该材质未分配到 AssetBundle。该材质引用一个纹理,而该纹理也未分配到 AssetBundle。

如果您遵循 AssetBundle 工作流程并使用示例类 CreateAssetBundles 来构建 AssetBundle,每个生成的 AssetBundle 都会包含此材质(包括其着色器和引用的纹理)。在以下示例图像中,预制件文件的大小分别为 383 KB 和 377 KB。

img

如果项目包含更多数量的预制件,此行为会影响最终安装程序的大小以及加载这两个 AssetBundle 时的运行时内存占用量。由于 Unity 将同一材质的每个副本视为独特材质,因此 AssetBundle 之间的数据重复问题也会影响批处理。

为了避免发生数据重复,请将材质及其引用的资源分配给其各自的 modulesmaterials AssetBundle。还可以仅标记材质,因为构建过程中,纹理依赖关系也会被自动包含在 AssetBundle 中。

现在,如果重新构建 AssetBundle,则生成的输出包含单独的 modulesmaterials AssetBundle (359 KB),其中包含此材质及其关联的纹理。这会显著减小预制件的其他 AssetBundle 的大小(从上一个迭代的大约 380 KB 减小到大约 20 KB)。

下图说明了此情况。

img

运行时加载

将公共资源提取到单个 AssetBundle 时,请注意依赖关系。特别要注意的是,如果仅使用预制件来实例化模块,则不会加载材质。

一个未加载材质的预制件一个未加载材质的预制件

要解决此问题,请先在内存中加载材质 AssetBundle,然后再加载属于模块的 AssetBundle。在以下示例中,这个过程发生在变量 materialsAB 中。

using System.IO;
using UnityEngine;

public class InstantiateAssetBundles : MonoBehaviour
{
    void Start()
    {
        var materialsAB = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, Path.Combine("AssetBundles", "modulesmaterials")));
        var moduleAB = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, Path.Combine("AssetBundles", "example-prefab")));

        if (moduleAB == null)
        {
            Debug.Log("Failed to load AssetBundle!");
            return;
        }
        var prefab = moduleAB.LoadAsset<GameObject>("example-prefab");
        Instantiate(prefab);
    }
}

c#一个已正确加载材质的预制件一个已正确加载材质的预制件

**注意:**使用 LZ4 压缩和未压缩的 AssetBundle 时,AssetBundle.LoadFromFile 仅在内存中加载其内容目录,而未加载内容本身。要检查是否发生了此情况,请使用内存性能分析器 (Memory Profiler) 包来检查内存使用情况

说人话

C和D分别依赖材质T,如果T不单独打包,C和D每次打包的时候,都会和T一起打包,这样相当于C和D两个AB包中都有T,重复了。如果将T单独打包,那么C和D打包的时候就不用包含T,可是加载的时候,必须先加载T。不然就会没有材质。

下图是T单独打包

image-20210512160157782

下图是不打包T

image-20210512160315579

代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
public class LoadAB : MonoBehaviour
{

    void Start()
    {
        //var materialsAB = AssetBundle.LoadFromFile("./AssetBundles/"+"mt_ab.u3d");
        var moduleAB = AssetBundle.LoadFromFile("./AssetBundles/"+"sphere_ab.u3d");

        if (moduleAB == null)
        {
            Debug.Log("Failed to load AssetBundle!");
            return;
        }
        var prefab = moduleAB.LoadAsset<GameObject>("Sphere.prefab");
        Instantiate(prefab);
    }


}

本机使用 AssetBundle

可以使用四种不同的 API 来加载 AssetBundle。它们的行为根据加载捆绑包的平台和构建 AssetBundle 时使用的压缩方法(未压缩、LZMA 和 LZ4)而有所不同。

我们必须使用的四个 API 是:

AssetBundle.LoadFromMemoryAsync

AssetBundle.LoadFromMemoryAsync

此函数采用包含 AssetBundle 数据的字节数组。也可以根据需要传递 CRC 值。如果捆绑包采用的是 LZMA 压缩方式,将在加载时解压缩 AssetBundle。LZ4 压缩包则会以压缩状态加载。

以下是如何使用此方法的一个示例:

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

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

但是,这不是实现 LoadFromMemoryAsync 的唯一策略。File.ReadAllBytes(path) 可以替换为获得字节数组的任何所需过程。

AssetBundle.LoadFromFile

AssetBundle.LoadFromFile

从本地存储中加载未压缩的捆绑包时,此 API 非常高效。如果捆绑包未压缩或采用了数据块 (LZ4) 压缩方式,LoadFromFile 将直接从磁盘加载捆绑包。使用此方法加载完全压缩的 (LZMA) 捆绑包将首先解压缩捆绑包,然后再将其加载到内存中。

如何使用 LoadFromFile 的一个示例:

using System.IO;
using UnityEngine;

public class LoadFromFileExample : MonoBehaviour
{
    void Start()
    {
        var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));

        if (myLoadedAssetBundle == null)
        {
            Debug.Log("Failed to load AssetBundle!");
            return;
        }
        var prefab = myLoadedAssetBundle.LoadAsset<GameObject>("MyObject");
        Instantiate(prefab);
    }
}

注意:在使用 Unity 5.3 或更早版本的 Android 设备上,尝试从流媒体资源 (Streaming Assets) 路径加载 AssetBundle 时,此 API 将失败。这是因为该路径的内容将驻留在压缩的 .jar 文件中。Unity 5.4 和更高版本则可以将此 API 调用与流媒体资源一起使用。

WWW.LoadFromCacheOrDownload

WWW.LoadFromCacheOrDownload

即将弃用(使用 UnityWebRequest)

此 API 对于从远程服务器下载 AssetBundle 或加载本地 AssetBundle 非常有用。这是一个陈旧且不太理想的 UnityWebRequest API 版本。

从远程位置加载 AssetBundle 将自动缓存 AssetBundle。如果 AssetBundle 被压缩,则将启动工作线程来解压缩捆绑包并将其写入缓存。一旦捆绑包被解压缩并缓存,它就会像 AssetBundle.LoadFromFile 一样加载。

如何使用 LoadFromCacheOrDownload 的一个示例:

using UnityEngine;
using System.Collections;

public class LoadFromCacheOrDownloadExample : MonoBehaviour
{
    IEnumerator Start ()
    {
            while (!Caching.ready)
                    yield return null;

        var www = WWW.LoadFromCacheOrDownload("http://myserver.com/myassetBundle", 5);
        yield return www;
        if(!string.IsNullOrEmpty(www.error))
        {
            Debug.Log(www.error);
            yield return;
        }
        var myLoadedAssetBundle = www.assetBundle;

        var asset = myLoadedAssetBundle.mainAsset;
    }
}

由于在 WWW 对象中缓存 AssetBundle 字节所需的内存开销,建议所有使用 WWW.LoadFromCacheOrDownload 的开发人员都应该确保自己的 AssetBundle 保持较小的大小 - 最多只有几兆字节。此外,还建议在有限内存平台(如移动设备)上运行的开发人员确保其代码一次只下载一个 AssetBundle,以此避免内存峰值。

如果缓存文件夹没有任何空间来缓存其他文件,LoadFromCacheOrDownload 将以迭代方式从缓存中删除最近最少使用的 AssetBundle,直到有足够的空间来存储新的 AssetBundle。如果无法腾出空间(因为硬盘已满,或者缓存中的所有文件当前都处于使用状态),LoadFromCacheOrDownload() 将不会使用缓存,而将文件流式传输到内存中

为了强制执行 LoadFromCacheOrDownload,需要更改版本参数(第二个参数)。仅当传递给函数的版本与当前缓存的 AssetBundle 的版本匹配,才会从缓存加载 AssetBundle。

UnityWebRequest

UnityWebRequest

UnityWebRequest 有一个特定 API 调用来处理 AssetBundle。首先,需要使用 UnityWebRequest.GetAssetBundle 来创建 Web 请求。返回请求后,请将请求对象传递给 DownloadHandlerAssetBundle.GetContent(UnityWebRequest)GetContent 调用将返回 AssetBundle 对象。

下载捆绑包后,还可以在 DownloadHandlerAssetBundle 类上使用 assetBundle 属性,从而以 AssetBundle.LoadFromFile 的效率加载 AssetBundle。

以下示例说明了如何加载包含两个游戏对象的 AssetBundle 并实例化这些游戏对象。要开始这个过程,我们只需要调用 StartCoroutine(InstantiateObject());

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

使用 UnityWebRequest 的优点在于,它允许开发人员以更灵活的方式处理下载的数据,并可能消除不必要的内存使用。这是比 UnityEngine.WWW 类更新和更优的 API。

从 AssetBundle 加载资源

现在已经成功下载 AssetBundle,因此是时候最终加载一些资源了。

通用代码片段:

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

T 是尝试加载的资源类型。

决定如何加载资源时有几个选项。我们有 LoadAssetLoadAllAssets 及其各自的异步对应选项 LoadAssetAsyncLoadAllAssetsAsync

同步从 AssetBundle 加载资源的方法如下:

加载单个游戏对象:

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

加载所有资源:

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

现在,在前面显示的方法返回要加载的对象类型或对象数组的情况下,异步方法返回 AssetBundleRequest。在访问资源之前,需要等待此操作完成。加载资源:

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

以及

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

加载资源后,就可以开始了!可以像使用 Unity 中的任何对象一样使用加载的对象。

加载 AssetBundle 清单

加载 AssetBundle 清单可能非常有用。特别是在处理 AssetBundle 依赖关系时。

要获得可用的 AssetBundleManifest 对象,需要加载另外的 AssetBundle(与其所在的文件夹名称相同的那个)并从中加载 AssetBundleManifest 类型的对象。

加载清单本身的操作方法与 AssetBundle 中的任何其他资源完全相同:

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

现在,可以通过上面示例中的清单对象访问 AssetBundleManifest API 调用。从这里,可以使用清单获取所构建的 AssetBundle 的相关信息。此信息包括 AssetBundle 的依赖项数据、哈希数据和变体数据。

别忘了在前面的部分中,我们讨论过 AssetBundle 依赖项以及如果一个捆绑包对另一个捆绑包有依赖性,那么在从原始捆绑包加载任何资源之前,需要加载哪些捆绑包?清单对象可以动态地查找加载依赖项。假设我们想要为名为“assetBundle”的 AssetBundle 加载所有依赖项。

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("assetBundle"); //传递想要依赖项的捆绑包的名称。
foreach(string dependency in dependencies)
{
    AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));
}

现在已经加载 AssetBundle、AssetBundle 依赖项和资源,因此是时候讨论如何管理所有这些已加载的 AssetBundle 了。

管理已加载的 AssetBundle

另请参阅:Unity 学习教程 - 管理已加载的 AssetBundle (Managing Loaded AssetBundles)

从活动场景中删除对象时,Unity 不会自动卸载对象。资源清理在特定时间触发,也可以手动触发。

了解何时加载和卸载 AssetBundle 非常重要。不正确地卸载 AssetBundle 会导致在内存中复制对象或其他不良情况,例如缺少纹理。

The biggest things to understand about AssetBundle management is when to call AssetBundle.Unload(bool) (or AssetBundle.UnloadAsync(bool)) and if you should pass true or false into the function call. Unload is a non-static function that will unload your AssetBundle. This API unloads the header information of the AssetBundle being called. The argument indicates whether to also unload all Objects instantiated from this AssetBundle.

AssetBundle.Unload(true) 卸载从 AssetBundle 加载的所有游戏对象(及其依赖项)。这不包括复制的游戏对象(例如实例化的游戏对象),因为它们不再属于 AssetBundle。发生这种情况时,从该 AssetBundle 加载的纹理(并且仍然属于它)会从场景中的游戏对象消失,因此 Unity 将它们视为缺少纹理。

假设材质 M 是从 AssetBundle AB 加载的,如下所示。

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

如果改作调用 AB.Unload(false),那么将会中断 M 和 AB 当前实例的链接关系。

img

如果稍后再次加载 AB 并且调用 AB.LoadAsset(),则 Unity 不会将现有 M 副本重新链接到新加载的材质。而是将加载 M 的两个副本。

img

img

Generally, using AssetBundle.Unload(false) does not lead to an ideal situation. Most projects should use AssetBundle.Unload(true) and adopt a method to ensure that Objects are not duplicated. Two common methods are:

  • 在应用程序生命周期中具有明确定义的卸载瞬态 AssetBundle 的时间点,例如在关卡之间或在加载屏幕期间。
  • 维护单个对象的引用计数,仅当未使用所有组成对象时才卸载 AssetBundle。这允许应用程序卸载和重新加载单个对象,而无需复制内存。

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

如果不想自己管理加载资源包、依赖项和资源,可能需要使用 Addressable Assets 包。

重点

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

如果改作调用 AB.Unload(false),那么将会中断 M 和 AB 当前实例的链接关系。

啥玩意?

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

AssetBundle 压缩

AssetBundle 压缩格式

默认情况下,Unity 通过 LZMA 压缩来创建 AssetBundle,然后通过 LZ4 压缩将其缓存。本部分描述以上的两种压缩格式。

Unity 的 AssetBundle 构建管线通过 LZMA 压缩来创建 AssetBundle。此压缩格式是表示整个 AssetBundle 的数据流,这意味着如果您需要从这些存档中读取某个资源,就必须将整个流解压缩。这是从内容分发网络 (CDN) 下载的 AssetBundle 的首选格式,因为文件大小小于使用 LZ4 压缩的文件。

另一方面,LZ4 压缩是一种基于块的压缩算法。如果 Unity 需要从 LZ4 存档中访问资源,只需解压缩并读取包含所请求资源的字节的块。这是 Unity 在其两种 AssetBundle 缓存中使用的压缩方法。在构建 AssetBundle 以强制进行 LZ4(HC) 压缩时,应使用 BuildAssetBundleOptions.ChunkBasedCompression 值。

Uncompressed AssetBundles that Unity builds when you use BuildAssetBundleOptions.UncompressedAssetBundle require no decompression, but occupy more disk space. Uncompressed AssetBundles are 16-byte aligned.

AssetBundle 缓存

为了使用 WWW 或 UnityWebRequest (UWR) 来优化 LZMA AssetBundle 的提取、再压缩和版本控制,Unity 有两种缓存:

  • 内存缓存UncompressedRuntime 格式将 AssetBundle 存储在 RAM 中。
  • 磁盘缓存将提取的 AssetBundle 以下文描述的压缩格式存储在可写介质中。

将 AssetBundle 加载到内存缓存中会耗用大量的内存。除非您特别希望频繁且快速地访问 AssetBundle 的内容,否则内存缓存的性价比可能不高。因此,应改用磁盘缓存。

如果向 UWR API 提供版本参数,Unity 会将 AssetBundle 数据存储在磁盘缓存中。如果没有提供版本参数,Unity 将使用内存缓存。版本参数可以是版本号或哈希。如果 Caching.compressionEnabled 设置为 true,则对于所有后续下载,Unity 会在将 AssetBundle 写入磁盘时应用 LZ4 压缩。它不会压缩缓存中的现有未压缩数据。如果 Caching.compressionEnabled 设置为 false,Unity 在将 AssetBundle 写入磁盘时不会应用压缩。

最初加载缓存的 LZMA AssetBundle 所花费的时间更长,因为 Unity 必须将存档重新压缩为目标格式。随后的加载将使用缓存版本。

AssetBundle.LoadFromFileAssetBundle.LoadFromFileAsync 始终对 LZMA AssetBundle 使用内存缓存,因此您应该使用 UWR API。如果无法使用 UWR API,您可以使用 AssetBundle.RecompressAssetBundleAsync 将 LZMA AssetBundle 重写到磁盘中。

内部测试表明,使用磁盘缓存而不是内存缓存在 RAM 使用率方面至少存在一个数量级的差异。因此,必须在内存影响、增加的磁盘空间要求以及应用程序的资源实例化时间之间进行权衡。

重点

在缓存那里

如果向 UWR API 提供版本参数,Unity 会将 AssetBundle 数据存储在磁盘缓存中。如果没有提供版本参数,Unity 将使用内存缓存。版本参数可以是版本号或哈希。如果 Caching.compressionEnabled 设置为 true,则对于所有后续下载,Unity 会在将 AssetBundle 写入磁盘时应用 LZ4 压缩。它不会压缩缓存中的现有未压缩数据。如果 Caching.compressionEnabled 设置为 false,Unity 在将 AssetBundle 写入磁盘时不会应用压缩。

Unity Asset Bundle Browser 工具

注意:此工具是 Unity 标准功能之外的额外功能。要访问此工具,必须从 GitHub 下载并安装,该过程独立于标准 Unity Editor 的下载和安装。

此工具使用户能够查看和编辑 Unity 项目的资源包的配置。它将阻止会创建无效捆绑包的编辑,并告知现有捆绑包的任何问题。此外还会提供基本的构建功能。

通过使用此工具,无需选择资源并在 Inspector 中手动设置资源包。它可以放入 5.6 或更高版本的任何 Unity 项目中。此工具将在 Window > AssetBundle Browser 中创建一个新菜单项。捆绑包的配置和构建功能在新窗口中拆分为两个选项卡。

img

需要 Unity 5.6+

用法 - 配置 (Configure)

注意:此实用程序处于预发布状态,因此我们建议在使用之前创建项目的备份。

此窗口提供了一个类似资源管理器的界面,用于管理和修改项目中的资源包。首次打开时,该工具将在后台解析所有捆绑包数据,缓慢标记所检测到的警告或错误。它会尽其所能与项目保持同步,但不能始终了解工具之外的活动。要强制快速刷新错误检测,或者使用外部更改来更新工具,请单击左上角的 Refresh 按钮。

该窗口分为四个部分:捆绑包列表、捆绑包详细信息、资源列表和资源详细信息。img

捆绑包列表

左侧面板显示项目中所有捆绑包的列表。可用功能:

  • 选择一个或一组捆绑包在资源列表面板中查看捆绑包中的资源列表。
  • 带变体的捆绑包为深灰色,可以展开来显示变体列表。
  • 右键单击或慢速双击可重命名捆绑包或捆绑包文件夹。
  • 如果捆绑包有任何错误、警告或信息消息,则右侧会出现一个图标。将鼠标悬停在图标上可获取更多信息。
  • 如果一个捆绑包中至少有一个场景(使其成为一个场景捆绑包)和显式包含的非场景资源,它将被标记为有错误。在修复之前不会构建此捆绑包。
  • 具有重复资源的捆绑包将标有警告(下面“资源列表”部分中提供关于重复的更多信息)
  • 空捆绑包将标有信息消息。由于多种原因,空捆绑包不是很稳定,有时可能会从此列表中消失。
  • 捆绑包的文件夹将使用包含的捆绑包中的最高消息进行标记。
  • 要解决捆绑包中包含的重复资源,可以采取以下操作:
    • 右键单击单个捆绑包可将确定为重复的所有资源移动到新捆绑包中。
    • 右键单击多个捆绑包,将资源从所有选定的重复捆绑包移动到新捆绑包中,或仅移动到选定项中共享的捆绑包。
    • 还可以将重复资源从资源列表面板拖到捆绑包列表中,从而将它们显式包含在捆绑包中。如需了解与此相关的更多信息,请参阅下面的资源列表功能集。
  • 右键单击或按 DEL 键可删除捆绑包。
  • 拖动捆绑包可将它们移入和移出文件夹,或合并它们。
  • 将资源从 Project Explorer 拖到捆绑包中可添加资源。
  • 将资源拖到空白区域可创建新捆绑包。
  • 右键单击可创建新捆绑包或捆绑包文件夹。
  • 右键单击“Convert to Variant”可转换为变体
    • 这将为所选捆绑包添加一个变体(最初称为“newvariant”)。
    • 当前处于选定捆绑包中的所有资源都将移至新变体中
    • 即将发布:检测变体之间的不匹配。

图标表示捆绑包是标准捆绑包还是场景捆绑包。

表示标准捆绑包的图标表示标准捆绑包的图标

表示场景捆绑包的图标表示场景捆绑包的图标

捆绑包详细信息

左下方面板显示捆绑包列表面板中的选定捆绑包的详细信息。此面板将显示以下信息(如果有):

  • 捆绑包总大小。这是所有资源占用的磁盘大小总和。
  • 当前捆绑包依赖的捆绑包
  • 与当前捆绑包关联的任何消息(错误/警告/信息)。

资源列表

右上方面板提供捆绑包列表中选择的任何捆绑包中包含的资源列表。可用功能:

  • 查看预计包含在捆绑包中的所有资源。按任何列标题对资源列表排序。
  • 查看显式包含在捆绑包中的资源。这些是已显式分配给捆绑包的资源。Inspector 将反映包含的捆绑包,在此视图中,它们将在资源名称旁边显示捆绑包名称。
  • 查看隐式包含在捆绑包中的资源。这些资源将对资源名称旁边的捆绑包的名称显示 auto。如果在 Inspector 中查看这些资源,它们将对分配的捆绑包显示 None
    • 由于对另一个包含资源的依赖关系,这些资源已经添加到所选捆绑包。只有未显式分配给捆绑包的资源才会隐式包含在捆绑包中。
    • 请注意,此隐式包含列表可能不完整。材质和纹理存在并非始终正确显示的已知问题。
    • 由于多个资源可以共享依赖关系,因此将给定资源隐式包含在多个捆绑包中是很常见的。如果该工具检测到这种情况,将使用警告图标来标记捆绑包和相关资源。
    • 要修复重复包含项的警告,可以手动将资源移动到新捆绑包中,或者右键单击捆绑包并选择“Move duplicate”选项之一。
  • 将资源从 Project Explorer 拖到此视图中可将它们添加到选定的捆绑包中。仅当选择了一个捆绑包并且资源类型可兼容(将场景拖放到场景捆绑包上等)时,此选项才有效。
  • 将资源(显式或隐式)从资源列表拖到捆绑包列表中(将它们添加到不同的捆绑包或新创建的捆绑包)。
  • 右键单击或按 DEL 键可从捆绑包中删除资源(不从项目中删除资源)。
  • 选择或双击资源可在 Project Explorer 中显示它们。

请注意关于在捆绑包中包含文件夹的说明。可以将资源文件夹(从 Project Explorer)分配给捆绑包。在浏览器中查看时,文件夹本身将列为显式,而内容为隐式。这反映了用于将资源分配给捆绑包的优先级系统。例如,假设游戏在 Assets/Prefabs 中有五个预制件,然后将文件夹“Prefabs”标记为一个捆绑包,并将其中一个实际预制件(“PrefabA”)标记为另一个捆绑包。构建后,“PrefabA”将在一个捆绑包中,其他四个预制件将在另一个捆绑包中。

资源详细信息

右下方面板可显示在资源列表面板中选择的资源的详细信息。此面板不具有交互性,但会显示以下信息(如果有):

  • 资源的完整路径。
  • 隐式包含在捆绑包中的原因(如果为隐式)。
  • 警告的原因(如果有警告)。
  • 错误的原因(如果有错误)。

故障排除

  • *无法重命名或删除特定的捆绑包。*首次将此工具添加到现有项目时偶尔会导致此问题。请通过 Unity 菜单系统强制重新导入资源来刷新数据。

外部工具集成

可以将生成资源包数据的其他工具与浏览器集成。目前,主要的例子是资源包图形工具 (Asset Bundle Graph Tool)。如果检测到集成,则会在浏览器顶部附近显示一个选择栏。在此栏中可以选择默认数据源(Unity 的 AssetDatabase)或集成的工具。如果没有检测到,则选择器不显示,但可以通过右键单击选项卡标题并选择“Custom Sources”来添加工具。

用法 - 构建 (Build)

Build 选项卡提供基本构建功能来帮助您开始使用资源包。在大多数专业情况下,用户最后需要更高级的构建设置。如果无法满足需求,任何人都可以使用此工具中的构建代码作为一个起点来编写自己的代码。界面:

  • Build Target - 构建捆绑包的目标平台
  • Output Path - 用于保存构建的捆绑包的路径。默认为 AssetBundles/。可以手动编辑该路径,也可以选择“Browse”。要恢复默认命名约定,请点击“Reset”。
  • Clear Folders - 在构建之前删除构建路径文件夹中的所有数据。
  • Copy to StreamingAssets - 构建完成后,将结果复制到 Assets/StreamingAssets。对测试很有用,但不会用于生产。
  • Advanced Settings
    • Compression - 在无压缩、标准 LZMA 压缩或基于块的 LZ4 压缩之间进行选择。
    • Exclude Type Information - 在资源包中不包括类型信息
    • Force Rebuild - 重新构建需要构建的捆绑包。与“Clear Folders”不同,因为此选项不会删除不再存在的捆绑包。
    • Ignore Type Tree Changes - 在执行增量构建检查时忽略类型树更改。
    • Append Hash - 将哈希附加到资源包名称。
    • Strict Mode - 如果在此期间报告任何错误,则构建无法成功。
    • Dry Run Build - 进行干运行构建。
  • Build - 执行构建。

Addressable Assets

大佬的总结

AssetBundle 打包时的分组(仅供参考)

1、逻辑实体分组:

a、一个UI界面或者所有UI界面一个包(这个界面里面所有的贴图和布局信息一个包);

b、一个角色或者所有角色一个包(这个角色里面的模型和动画一个包);

c、所有场景所共有的部分一个包(包括贴图和模型);

2、按照类型分组

所有的声音资源打成一个包,所有shader打成一个包,所有模型打成一个包,所有材质打成一个包。

3、按使用分组

把在某一时间内使用的所有资源打成一个包,可以按照关卡分,一个关卡所需要的所有资源包,包括角色、

贴图、声音等打成一个包,也可以按照场景分,一个场景所需要的资源一个包。

小结:

1、把经常更新的资源放在一个单独的包里、跟不经常更新的包分离。

2、把需要同时加载的资源放在一个包里面。

3、可以把其他包共享的资源放在一个单独的包里面。(做依赖,这样可以减少很多比较资源占用)

4、把一些需要同时加载的小资源打包成一个包。

5、如果对一个同一个资源有两个版本,可以考虑通过后缀来区分(如:v1 v2 v3 )(如unityAB_V1 、unityAV_V2);

**//=============================================================================================================**

六 : 依赖打包简介

img

小结:如图所示,其主要的特点是把模型预制体所使用的材质贴图进行整合,这样可以避免占用过多的内存资源,

​ **在加载时也可以避免包过大使用的时间太长,同时避免了资源的重复加载。**在需要加载时也可以通过依赖

关系进行逆向加载。

****//=============================================================================================================****

七: Build AssetBundle方法参数详解(BuildPipline.BuildAssetBundle)

1、Build的路径(只要是在硬盘上都可以的)

2、BuildAssetBundleOptions(枚举类型)

a)、BuildAssetBundleOptions.None:使用LZMA算法压缩,压缩的包更小,但加载时间更长,

使用之前需要整体解压 。一旦被解压,这个包会使用LZ4重新压缩。使用资源的时候不需要整体解压

。在下载的时候可以使用LZMA算法。一旦它被下载了之后,它会使用LZ4算法保存到本地上。

b)、BuildAssetBundleOption.UncompressedAssetBundle (不压缩,包大,加载速度快)。

c)、*BuildAssetBundleOption.ChunkBasedCompression:(使用LZ4算法,压缩率没有LZMA高,*

*但我们可以加载指定资源的不用解压全部);*

*注意:使用LZ4算法,可以获得可以跟不压缩相媲美的加载速度,而且比不压缩的文件要小。*

3、BuildTarget:(选择Build出来的AB包需要的平台)

图解:img

****//=============================================================================================================******

八:关于AssetBundle的卸载

首先为什么要把AB 包卸载了?其实也很简单,内存永远是有限的,当你转换一个场景或者关卡时,之前不需要的AB包所占用的内存是需要把它

释放掉的,这样才能让内存永远保持着一个健康的容量。

卸载主要有两个方面:

1、 减少内存使用,可以保证一个良好且健康的运行内存。

2、 有可能导致资源丢失问题。

所以一般什么时候卸载资源呢?

AssetBundle.Unload(true)卸载所有资源,即使有资源使用着(1,在关卡切换或场景切换时 2、资源没被用的时候调用);

AssetBundle.Unload(false)卸载所有没有被使用的资源。(这个适用时间比较多 ,可自行把控)。

**个别资源怎么卸载,通过Resources.UnloadUnusedAssets();

**

当场景切换时,unity会自行执行(Resources.UnloadUnusedAssets()这个方法);

****//=============================================================================================================******

九:关于AssetBundle的文件效验(每个AB包中有一个manifest文件中就有一个CRC)

**CRC、DM5、SHA1

**

相同点:

CRC、DM5、SHA1都是通过对数据进行计算,来生成一个效验值,该效验值用来效验数据的完整性。

不同点:

1、算法的不同。CRC采用多项式除法,MD5和SHA1使用的替换,轮转等方法。

**2、效验值的长度不同。CRC效验值的长度基本跟其多项式有关系,一般为16位或32位,**MD5是16个字节(128位);SHA1是20个字节(160位);

3、效验值的称呼不同。CRC一般叫做CRC值;MD5和SHA1一般叫哈希值(Hash)或散列值。

4、安全性不同。这里的安全性是指效验的能力,即数据的错误能通过效验位检测出来,CRC的安全性跟多项式有很大关系,相对于MD5和SHA1要弱很多;MD5的安全性很高,不过大概在04年的时候被山东大学的王小云破解了;SHA1安全性最高。

(算法的过程一般是不可逆的,破解即是逆向根据效验值推导出算法过程列表)

5、效率不同。CRC的计算效率很高;MD5和SHA1比较慢。

6、用途不同。CRC一般用作通信数据的效验,MD5和SHA1用于安全(Security)领域。比如文件效验、数字签名等。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值