Creator | 资源管理:Asset Bundle 全解析
共 12248字,需浏览 25分钟
·
2021-04-02 12:35
本篇文章主要向大家介绍 Cocos Creator 之 AssetBundle 使用方案分享,包括配置、构建、加载、释放 AB 包这些实用性很强的内容,同时也有对 AB 包的优先级及资源引用关系的讨论,帮助大家更好地理解 AB 包。
Asset Bundle 介绍:
https://docs.cocos.com/creator/manual/zh/asset-manager/bundle.html
配置和加载 Asset Bundle:
https://docs.cocos.com/creator/manual/zh/scripting/asset-bundle.html
本文中 Asset Bundle 简称 AB 包
外部资源 = 不在本 AB 包目录内的资源(包括内置 AB 包和其他 AB 包)
欢迎阅读。
AB 包的定义
AB 包作为资源模块化工具,允许开发者按照项目需求将贴图、脚本、场景等资源划分在多个 AB 包中,然后在游戏运行过程中,按照需求去加载不同的 AB 包,以减少启动时需要加载的资源数量,从而减少首次下载和加载游戏时所需的时间,同时可以减少内存占用。
AB 包可以按需求随意放置,比如可以放在远程服务器、本地、或者小游戏平台的分包,也可以跨项目复用,用于加载子项目中的 AB 包。
配置 AB 包
配置方法:
① 将项目中的场景、资源、代码等内容按照需求划分到不同的文件夹
② 单击该文件夹,属性检查器 中就会出现一个 配置为 Bundle 的选项,勾选后会出现如下图的配置项
配置项 | 功能说明 |
Bundle 名称 | AB 包构建后的名称,默认会使用这个文件夹的名字,可根据需要修改 |
Bundle 优先级 | Creator 开放了 10 个可供配置的优先级,构建时将会按照优先级 从大到小 的顺序对 AB 包依次进行构建 |
目标平台 | 不同平台可使用不同的配置,构建时将根据对应平台的设置来构建 AB 包 |
压缩类型 | 决定 AB 包最后的输出形式,包括 默认、无压缩、合并所有 JSON、小游戏分包、Zip 五种压缩类型 |
配置为远程包 | 是否将 AB 包配置为远程包,不支持 Web 平台 若勾选了该项,则 AB 包在构建后会被放到 remote 文件夹,你需要将整个 remote 文件夹放到远程服务器上 |
配置完成后点击右上方的 应用 按钮,这个文件夹就被配置为 AB 包了,然后在 构建发布 面板选择对应的平台进行构建。
注意:
① Creator 有 4 个 内置 AB 包,包括 resources、internal、main、start-scene,在设置 Bundle 名称 时请不要使用这四个名称
② 小游戏分包只能放在本地,不能配置为远程包,所以当 压缩类型 设置为 小游戏分包 时,配置为远程包 项不可勾选
③ Zip 压缩类型主要是为了降低网络请求数量,如果放在本地,不用网络请求,则没什么必要,所以要求与 配置为远程包 搭配使用
内置 AB 包
内置 AB 包 | 功能说明 | 优先级 |
internal | 存放所有内置资源以及其依赖资源 | 11 |
main | 存放所有在 构建发布 面板的 参与构建场景 中勾选的场景以及其依赖资源 | 7 |
resources | 存放 resources 目录下的所有资源以及其依赖资源 | 8 |
start-scene | 如果在 构建发布 面板中勾选了 初始场景分包,则首场景将会被构建到 start-scene 中 | 9 |
注意:
start-scene 目前仅支持小游戏平台,如果在 构建发布 面板中勾选 初始场景分包,则首场景会被放到内置 AB 包的 start-scene 中,从而实现分离首场景。
构建 AB 包
构建完成后,该文件夹会被打包到对应平台发布包目录下的 assets 文件夹中。
但有以下两种特殊情况:
① 配置 AB 包时,若勾选了 配置为远程包,则这个文件夹会被打包到对应平台发布包目录下的 remote 文件夹中
② 配置 AB 包时,若设置了 压缩类型 为 小游戏分包,则这个文件夹会被打包到对应平台发布包目录下的 subpackages 文件夹中
assets、remote、subpackages 这三个文件夹中包含的每个文件夹都是一个 AB 包。
assets:
remote:
AB 包的构造
代码:文件夹中的所有代码会根据发布平台合并成一个 index.js 或 game.js 的入口脚本文件,并从主包中剔除
资源:文件夹中的所有资源以及文件夹外的相关依赖资源都会放到 import 或 native 目录下
资源配置:所有资源的配置信息包括路径、类型、版本信息都会被合并成一个 config.json 文件
构建后生成的 AB 包目录结构如下图所示:
import: 资源描述 json 的存放目录
native :资源文件的存放目录
config.json:所有资源的配置信息,包括路径、类型、版本信息
index.js:文件夹中的所有代码
AB 包的优先级和资源引用关系
假设 AB 包 A(也可以是内置的 AB 包)中的 asset X 同时被 AB 包 B、C、D 引用, 按照刚才所说,构建后,AB 包 A、B、C、D 中会分别存放一份 asset X,这明显违背减小包体的原则,那么构建后 asset X 究竟该放到哪个 AB 包中呢?
此时就需要通过调整 AB 包的优先级来决定资源的存放位置:
Creator 开放了 10 个可供配置的优先级,编辑器在构建时将会按照优先级 从大到小 的顺序对 AB 包依次进行构建(别忘了 Creator 的内置 AB 包)。
了解了 AB 包的优先级后我们再来讨论下 asset X 的存放情况。
① AB 包 优先级相同 的情况下,引用外部资源时,构建后,该资源会在每个 AB 包中复制一份,此时不同的 AB 包之间没有依赖关系,可按任意顺序加载,但由于该资源会被复制 N 份,这样会引起包体的增大
② AB 包 优先级不同 的情况下,引用外部资源时,构建后,该资源会放在 优先级高 的 AB 包(包括内置 AB 包)中,优先级低 的 AB 包只会存储一条记录信息。
此时优先级低的 AB 包会 依赖 优先级高的 AB 包。如果想在优先级低的 AB 包中加载此资源,必须在加载优先级低的 AB 包 之前 先加载(loadBundle)优先级高的 AB 包。
因此项目中那些频繁被其他 AB 包使用的资源,应该放置在优先级较高的 AB 包中,比如 内置 AB 包 main,或者为了减少首包的大小,可以放到 自定义 AB 包中,然后修改该 AB 包的 Bundle 优先级为较高的值,在合适的时机调用 cc.assetManager.loadBundle
AB 包的脚本
注意:
有些平台不允许加载远程的脚本文件,例如微信小游戏,在这些平台上,Creator 会将 AB 包中的代码拷贝到 src/scripts 目录下,从而保证正常加载
不同 AB 包中的脚本建议最好不要互相引用,否则可能会导致在运行时找不到对应脚本,如果需要引用某些类或变量,可以将该类和变量暴露在一个你自己的全局命名空间中,从而实现共享,类似:cc["MyBundle"] = MyBundle;
注意:
虽然脚本文件也是资源的一种,但是脚本文件只会合并到本 AB 包中的 index.js,并不会像其他资源一样复制到其他 AB 包中,无论优先级是多少
node_modules 中的第三方脚本文件只会合并到 内置 AB 包 main 中的 index.js
注意:
在通过 API(loadBundle)加载 AB 包时,就会加载 AB 包中的 index.js,一旦加载后,就会一直存在内存中,移除 AB 包(removeBundle)也不会释放,再次加载 AB 包时也不会重新加载脚本。
加载 AB 包
1加载AB包
引擎提供了一个统一的 API cc.assetManager.loadBundle 来加载 AB 包,加载时需要传入 AB 包在配置面板中的 Bundle 名称 或者 AB 包的 url
但当你复用其他项目的 AB 包时,则只能通过 url 进行加载。
使用方法如下:
cc.assetManager.loadBundle("MyBundle", (err: Error, bundle: cc.AssetManager.Bundle) => {
bundle.load("xxx");
});
// 当复用其他项目的AB包时
cc.assetManager.loadBundle("https://xxx.com/remote/MyBundle", (err: Error, bundle: cc.AssetManager.Bundle) => {
bundle.load("xxx");
});
通过 url 加载 AB 包,其过程和 cc.assetManager.loadRemote 相同,加载成功后,该 AB 包会以 文件夹 的形式保存在本地缓存目录,如 win32 模拟器:
loadBundle 后,只是将该 AB 包中的 资源清单 和 脚本文件 缓存到本地,只有在 bundle 调用 load 或者 preload 时,才会缓存对应的资源。
cacheList.json 文件中以 { url: object } 的形式记录远程资源信息,以后再次加载该远程资源时则直接使用缓存中的资源文件。
cc.assetManager.loadBundle 还支持传入用户空间中的路径来加载用户空间中的 AB 包。
通过对应平台提供的 下载 接口将 AB 包提前下载到用户空间中,然后再使用 loadBundle 进行加载,开发者就可以完全自己管理 AB 包的下载与缓存过程,更加灵活。
// 提前下载某个 Asset Bundle 到用户空间 pathToBundle 目录下。需要保证用户空间下的 Asset Bundle 和对应原始 Asset Bundle 的结构和内容完全一样
// 原生平台
let dir = cc.path.dirname(storagePath);
if (!jsb.fileUtils.isDirectoryExist(dir)) {
jsb.fileUtils.createDirectory(dir);
}
this._downloader = new jsb.Downloader();
this._downloader.setOnFileTaskSuccess(this.onSucceed.bind(this));
this._downloader.setOnTaskProgress(this.onProgress.bind(this));
this._downloader.setOnTaskError(this.onError.bind(this));
this._downloader.createDownloadFileTask(url, storagePath);
// 只加载下载后的资源使用:
// cc.assetManager.loadRemote
// 通过 Asset Bundle 在用户空间中的路径进行加载
// 原生平台
cc.assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => {
// ...
});
// 微信小游戏平台
cc.assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => {
// ...
});
注意:
在配置 AB 包时,若勾选了 配置为远程包,那么构建时请在 构建发布 面板中填写 资源服务器地址。
通过 cc.assetManager.bundles 可以看到当前内存中已加载 bundle 的集合以及 bundle 的具体信息。
console.log(cc.assetManager.bundles);
2AB 包的版本
AB 包在更新上延续了 Creator 的 MD5 方案。
当你需要更新远程服务器上的 AB 包时,请在 构建发布 面板中勾选 MD5 Cache 选项,此时构建出来的AB包中的 config.json 文件名会附带 Hash 值
如图所示:
在加载 AB 包时 不需要 额外提供对应的 Hash 值,Creator 会在 settings.js 中查询对应的 Hash 值,并自动做出调整。
但如果你想要将相关版本配置信息存储在服务器上,启动时动态获取版本信息以实现热更新,你也可以手动指定一个版本 Hash 值并传入 loadBundle 中,此时将会以传入的 Hash 值为准:
cc.assetManager.loadBundle("MyBundle", { version: "fbc07" }, (err: Error, bundle: cc.AssetManager.Bundle) => {
if (err) {
return console.error(err);
}
console.log("load bundle successfully.");
});
这样就能绕过缓存中的老版本文件,重新下载最新版本的 AB 包。
3加载 AB 包中的资源
在通过 API(loadBundle )加载 AB 包时,引擎并 没有加载 AB 包中的所有资源,而是只 加载 AB 包的 资源清单(config.json),以及包含的 所有脚本(index.js)。
即 AB 包中的脚本会被加载到内存中,但是 AB 包中的资源并不会加载到内存中,如果需要加载其中的资源,还需要 bundle.load("prefab") 。
当 AB 包加载完成后,会返回一个 cc.AssetManager.Bundle 类的实例,这个实例就是 AB 包 API 的主要入口,我们可以通过实例上的 load 方法来加载 AB 包中的资源,此方法的参数与 cc.resources.load 相同,只需要传入资源相对 AB 包的路径即可,但需要注意的是,路径的结尾处 不能 包含文件扩展名。
// 加载 prefab
cc.assetManager.loadBundle(name, (err: Error, bundle: cc.AssetManager.Bundle)=> {
bundle.load("prefab", (error: Error, asset: cc.Prefab) => {
});
});
//加载 spriteFrame
cc.assetManager.loadBundle("MyBundle", (err: Error, bundle: cc.AssetManager.Bundle) => {
bundle.load("HelloWorld", cc.SpriteFrame, (err, assets: cc.SpriteFrame) => {
this.sprite.spriteFrame = assets;
});
});
AB 包还提供了 loadDir 方法来批量加载相同目录下的多个资源,此方法的参数与 cc.resources.loadDir 相似,只需要传入该目录相对 AB 包的路径即可。
// 加载 textures 目录下的所有资源
bundle.loadDir("textures", function (err, assets) {
// ...
});
// 加载 textures 目录下的所有 SpriteFrame资源
bundle.loadDir("textures", cc.SpriteFrame, function (err, assets) {
// ...
});
注意:
cc.resources 和 cc.AssetManager.Bundle 分别提供了 load 和 loadDir 接口,且加载后的资源都需要我们手动管理。
load 后资源的引用计数为 0,而 loadDir 后资源的引用计数要视情况而定。
① loadDir 不指定资源类型时,会加载文件夹内的所有资源
cc.resources.loadDir("dir", (err, assets) => { });
我们以图片资源为例:
API 将 Texture2D 和 SpriteFrame 一起加载出来,因此 Texture2D 的资源会被 SpriteFrame 引用到,其引用计数成为 1,而 SpriteFrame 未被其他资源引用,其引用计数依然是 0。
② loadDir 指定资源类型时,只加载文件夹内该类型的资源
cc.resources.loadDir("dir", cc.SpriteFrame, (err, spriteFrames) => { });
此时加载的资源之间不存在相互引用关系,所以其引用计数都是 0。
在 资源管理器 中,图像资源的左边会显示一个和文件夹类似的三角图标,点击就可以展开看到它的子资源(sub asset),每个图像资源导入后编辑器会自动在它下面创建同名的 SpriteFrame 资源。
SpriteFrame 是核心渲染组件 Sprite 所使用的资源,设置或替换 Sprite 组件中的 spriteFrame 属性,就可以切换显示的图像。
为什么会有 SpriteFrame 这种资源?Texture 是保存在 GPU 缓冲中的一张纹理,是原始的图像资源。
而 SpriteFrame 包含两部分内容:记录了 Texture 及其相关属性的 Texture2D 对象和纹理的矩形区域,对于相同的 Texture 可以进行不同的纹理矩形区域设置,然后根据 Sprite 的填充类型,如 SIMPLE、SLICED、TILED 等进行不同的顶点数据填充,从而满足 Texture 填充图像精灵的多样化需求。
而 SpriteFrame 记录的纹理矩形区域数据又可以在资源的属性检查器中根据需求自由定义,这样的设置让资源的开发更为高效和便利。
除了每个文件会产生一个 SpriteFrame 的图像资源(Texture)之外,我们还有包含多个 SpriteFrame 的图集资源(Atlas)类型。
4预加载资源
为了尽可能缩短下载时间,我们可以使用预加载。
Asset Manager 中的大部分加载接口包括 load、loadDir、loadScene 都有其对应的预加载版本。
cc.assetManager.loadBundle("MyBundle", (err: Error, bundle: cc.AssetManager.Bundle) => {
bundle.preload("HelloWorld", cc.SpriteFrame);
});
加载接口与预加载接口所用的参数是完全一样的,两者的区别在于:
预加载只会下载资源,不会对资源进行解析和初始化操作
预加载在加载过程中会受到更多限制,例如最大下载并发数会更小
预加载的下载优先级更低,当多个资源在等待下载时,预加载的资源会放在最后下载
因为 预加载没有做任何解析操作,所以当所有的预加载完成时,不会返回任何可用资源
以上优化手段充分 降低了预加载的性能损耗,确保了游戏体验顺畅,开发者可以充分利用游戏过程中的网络带宽缩短后续资源的加载时间。
因为预加载没有去解析资源,所以需要在预加载完成后配合加载接口进行资源的解析和初始化,来完成资源加载。
注意:
加载不需要等到预加载完成后再调用,开发者可以在任何时候进行加载。正常加载接口会直接复用预加载过程中已经下载好的内容,缩短加载时间。
预加载只会去 下载 必要的资源,并 不会进行资源的反序列化和初始化工作,也就不会将资源放入内存(cc.assetManager.assets)中,所以性能消耗更小,确保了游戏体验流畅。
5加载场景
AB 包提供了 loadScene 方法用于加载指定 bundle 中的场景,你只需要传入 场景名 即可。
loadScene 与 cc.director.loadScene 不同的地方在于 loadScene 只会加载指定 AB 包中的场景,而不会运行场景,你还需要使用 cc.director.runScene 来运行场景。
// 加载场景
bundle.loadScene("MyBundle", (error: Error, sceneAsset: cc.SceneAsset)=> {
cc.director.runScene(sceneAsset);
});
// 预加载场景
bundle.preloadScene("MyBundle", this.onComplete.bind(this), (error: Error)=> {
});
6获取 AB 包
当 AB 包被加载过之后,会被缓存下来,此时开发者可以使用 AB 包名称来获取该 bundle。
let bundle = cc.assetManager.getBundle("MyBundle");
释放 AB 包
1释放 AB 包中的资源
在资源加载完成后,所有的资源都会被临时缓存到 cc.assetManager 中,以避
免重复加载。当然,缓存中的资源也会占用内存,有些资源如果不再需要用到,可以通过以下三种方式进行释放:
① 使用常规的 cc.assetManager.releaseAsset 方法进行释放
bundle.load("image", cc.SpriteFrame, function (err, spriteFrame) {
cc.assetManager.releaseAsset(spriteFrame);
});
② 使用 AB 包提供的 release 方法,通过传入路径和类型进行释放,只能释放在 AB 包中的单个资源,参数可以与 AB 包的 load 方法中使用的参数一致
bundle.load("image", cc.SpriteFrame, function (err, spriteFrame) {
bundle.release("image", cc.SpriteFrame);
});
③使用 AB 包提供的 releaseAll 方法,此方法与 cc.assetManager.releaseAll 相似,releaseAll 方法会释放所有属于该 bundle 的资源(包括在AB包中的资源以及其外部的相关依赖资源),请慎重使用
bundle.load("image", cc.SpriteFrame, function (err, spriteFrame) {
bundle.releaseAll();
});
注意:
在释放资源时,Creator 会自动处理该资源的依赖资源,开发者不需要对其依赖资源进行管理。
2移除 AB 包
在加载了 AB 包之后,此 bundle 会一直存在整个游戏过程中,除非开发者手动移除。
当手动移除了某个不需要的 bundle,那么此 bundle 的缓存也会被移除,如果需要再次使用,则必须再重新加载一次。
let bundle = cc.assetManager.getBundle("MyBundle");
cc.assetManager.removeBundle(bundle);
注意:
在移除 AB 包时,并不会释放该 bundle 中加载过的资源。
如果需要释放,请先使用 AB 包的 release / releaseAll 方法:
let bundle = cc.assetManager.getBundle("MyBundle");
// 释放在AB包中的单个资源
bundle.release("image", cc.SpriteFrame);
cc.assetManager.removeBundle(bundle);
let bundle = cc.assetManager.getBundle("MyBundle");
// 释放所有属于AB包的资源
bundle.releaseAll();
cc.assetManager.removeBundle(bundle);
往期精彩