参考
Asset Bundle 介绍
Creator | Asset Bundle 全解析
讨论下2.4.3,内置的AB包能否被loadBundle方法加载出来
新版AssetBundle项目实际运用经验总结
一、内置 Asset Bundle
1. resources 优先级8
resources 目录下的所有资源以及其依赖资源
2. main主包 优先级7
构建发布 面板的 参与构建场景 中勾选的场景以及其依赖资源
注:这里我做了测试,如果一个场景参与构建,使用的图片会打到main包里。如果把场景和使用的资源划分到Bundle中,则不会打到main包里,而是有个单独的bundle文件夹。
这里把atlas-compress场景并未参与构建,但是所在文件夹勾选了bundle,仍然会打包出来,所以使用Bundle进行加载仍然可以正常运行。
3. start-scene 优先级20
如果在 构建发布 面板中勾选了 初始场景分包,则首场景将会被构建到 start-scene 中。
这里会把首场景依赖的资源提取出来,全部放到
build\wechatgame\assets\start-scene
路径下。同时,这部分资源之前打包在main中,现在main包里会自动去掉。所以这个选项推荐勾上。
4.优先级
Creator 开放了 10 个可供配置的优先级,编辑器在构建时将会按照优先级 从大到小 的顺序对 Asset Bundle 依次进行构建。
- 当同个资源被 相同优先级 的多个 Asset Bundle 引用时,资源会在每个 Asset Bundle 中都复制一份。此时不同的 Asset Bundle 之间没有依赖关系,可按任意顺序加载。
- 当同个资源被 不同优先级 的多个 Asset Bundle 引用时,资源会优先放在优先级高的 Asset Bundle 中,低优先级的 Asset Bundle 只会存储一条记录信息。此时低优先级的 Asset Bundle 会依赖高优先级的 Asset Bundle。
结论:
- 请尽量确保共享的资源(例如 Texture、SpriteFrame、Audio 等)所在的 Asset Bundle 优先级更高,以便让更多低优先级的 Asset Bundle 共享资源,从而最小化包体。
5.压缩类型,默认使用 合并依赖 压缩类型
- 合并依赖 构建 Asset Bundle 时会将相互依赖的资源的 JSON 文件合并在一起,从而减少运行时的加载请求次数
- 无压缩 构建 Asset Bundle 时没有任何压缩操作
- 合并所有 JSON 构建 Asset Bundle 时会将所有资源的 JSON 文件合并为一个,从而最大化减少请求数量,但可能会增加单个资源的加载时间
- 小游戏分包 在提供了分包功能的小游戏平台,会将 Asset Bundle 设置为对应平台上的分包。
- Zip 在部分小游戏平台,构建 Asset Bundle 时会将资源文件压缩成一个 Zip 文件,从而减少运行时的加载请求数量
注:
- 压缩类型选小游戏分包,只能放在本地,不能配置为远程包。所以当压缩类型设置为小游戏分包时,配置为远程包项不可勾选。(小游戏分包放到相应平台的subpackages文件夹)
- Zip 压缩类型主要是为了降低网络请求数量,如果放在本地,不用网络请求,则没什么必要。所以要求与 配置为远程包 搭配使用。
配置为远程包
是否将 Asset Bundle 配置为远程包,不支持 Web 平台。若勾选了该项,则 Asset Bundle 在构建后会被放到 remote 文件夹,你需要将整个 remote 文件夹放到远程服务器上。构建 OPPO、vivo、华为等小游戏平台时,若勾选了该项,则不会将 Asset Bundle 打包到 rpk 中。
注意:在配置 Asset Bundle 时,若勾选了 配置为远程包,那么构建时请在 构建发布 面板中填写 资源服务器地址。
二、Asset Bundle配置与构建
1.不同目标平台分别进行配置
自定义 Asset Bundle 是以 文件夹 为单位进行配置的。当我们在 资源管理器 中选中一个文件夹时,属性检查器 中就会出现一个 配置为 Bundle 的选项,勾选后会出现如下图的配置项:
经过测试,配置bundle后。即使项目中没有使用这个bundle里的资源,构建时也能看到它。
2.构建
构建后生成的 Asset Bundle 目录结构如下图所示:
- 代码:文件夹中的所有代码会根据发布平台合并成一个 index.js 或 game.js 的入口脚本文件。
- 资源:文件夹中的所有资源以及文件夹外的相关依赖资源都会放到 import 或 native 目录下。
- 资源配置:所有资源的配置信息包括路径、类型、版本信息都会被合并成一个 config.json 文件。
构建完成后,这个 Asset Bundle 文件夹会被打包到对应平台发布包目录下的 assets 文件夹中。但有以下两种特殊情况:
- 配置 Asset Bundle 时,若勾选了 配置为远程包,则这个 Asset Bundle 文件夹会被打包到对应平台发布包目录下的 remote 文件夹中。
- 配置 Asset Bundle 时,若设置了 压缩类型 为 小游戏分包,则这个 Asset Bundle 文件夹会被打包到对应平台发布包目录下的 subpackages 文件夹中。
assets、remote、subpackages 这三个文件夹中包含的每个文件夹都是一个 Asset Bundle。
例如:将 example 工程中的 cases/01_graphics 文件夹在 Web Mobile 平台配置为 Asset Bundle,那么项目构建后将会在发布包目录下的 assets 中生成 01_graphics 文件夹,01_graphics 文件夹就是一个 Asset Bundle。
注意:有些平台不允许加载远程的脚本文件,例如微信小游戏,在这些平台上,Creator 会将 Asset Bundle 的代码拷贝到 src/bundle-scripts 目录下,从而保证正常加载。
三、加载 Asset Bundle和其中的资源
assetManager.loadBundle('01_graphics', (err, bundle) => {
bundle.load('xxx');
});
在通过 API 加载 Asset Bundle 时,引擎并没有加载 Asset Bundle 中的所有资源,而是加载 Asset Bundle 的 资源清单,以及包含的 所有脚本。当 Asset Bundle 加载完成后,会触发回调并返回错误信息和 AssetManager.Bundle 类的实例,这个实例就是 Asset Bundle API 的主要入口,开发者可以使用它去加载 Asset Bundle 中的各类资源。
1.加载 Asset Bundle 中的资源
在 Asset Bundle 加载完成后,返回了一个 AssetManager.Bundle 类的实例。我们可以通过实例上的 load 方法来加载 Asset Bundle 中的资源,此方法的参数与 resources.load 相同,只需要传入资源相对 Asset Bundle 的路径即可。但需要注意的是,路径的结尾处 不能 包含文件扩展名。
// 加载 Prefab
bundle.load(`prefab`, Prefab, function (err, prefab) {
let newNode = instantiate(prefab);
director.getScene().addChild(newNode);
});
// 加载 Texture
bundle.load(`image`, Texture2D, function (err, texture) {
console.log(texture)
});
与 resources.load 相同,load 方法也提供了一个类型参数,这在加载同名资源或者加载 SpriteFrame 时十分有效。
// 加载 SpriteFrame
bundle.load(`image`, SpriteFrame, function (err, spriteFrame) {
console.log(spriteFrame);
});
2.批量加载资源
Asset Bundle 提供了 loadDir 方法来批量加载相同目录下的多个资源。此方法的参数与 resources.loadDir 相似,只需要传入该目录相对 Asset Bundle 的路径即可。
// 加载 textures 目录下的所有资源
bundle.loadDir("textures", function (err, assets) {
// ...
});
// 加载 textures 目录下的所有 Texture 资源
bundle.loadDir("textures", Texture2D, function (err, assets) {
// ...
});
如果根目录下没有文件夹,想加载整个bundle目录怎么办呢?参考如何加载一个asset bundle包根目录下的所有资源
bundle.loadDir("/", cc.AudioClip, function(err, assets){ //... });
3.加载场景
Asset Bundle 提供了 loadScene 方法用于加载指定 bundle 中的场景,你只需要传入 场景名 即可。loadScene 与 director.loadScene 不同的地方在于 loadScene 只会加载指定 bundle 中的场景,而不会运行场景,你还需要使用 director.runScene 来运行场景。
bundle.loadScene('test', function (err, scene) {
director.runScene(scene);
});
4.预加载
除了场景,其他资源也可以进行预加载。预加载的加载参数和正常加载时一样,不过因为预加载只会去下载必要的资源,并不会进行资源的反序列化和初始化工作,所以性能消耗更小,更适合在游戏过程中使用。
Asset Bundle 中提供了 preload 和 preloadDir 接口用于预加载 Asset Bundle 中的资源。具体的使用方式和 assetManager 一致
四、Asset Bundle 的版本
Asset Bundle 在更新上延续了 Creator 的 MD5 方案。当你需要更新远程服务器上的 Asset Bundle 时,请在 构建发布 面板中勾选 MD5 Cache 选项,此时构建出来的 Asset Bundle 中的 config.json 文件名会附带 Hash 值。如图所示:
在加载 Asset Bundle 时 不需要 额外提供对应的 Hash 值,Creator 会在 settings.js 中查询对应的 Hash 值,并自动做出调整。
五、释放和移除
1.获取 Asset Bundle
当 Asset Bundle 被加载过之后,会被缓存下来,此时开发者可以使用 Asset Bundle 名称来获取该 bundle。例如:
let bundle = assetManager.getBundle('01_graphics');
2.释放
缓存中的资源也会占用内存,有些资源如果不再需要用到,可以通过以下三种方式进行释放:
- assetManager.releaseAsset
- bundle.release(
image
, SpriteFrame); - bundle.releaseAll();
3.移除
在加载了 Asset Bundle 之后,此 bundle 会一直存在整个游戏过程中,除非开发者手动移除。当手动移除了某个不需要的 bundle,那么此 bundle 的缓存也会被移除,如果需要再次使用,则必须再重新加载一次。
注意:在移除 Asset Bundle 时,并不会释放该 bundle 中被加载过的资源。如果需要释放,请先使用 Asset Bundle 的 release / releaseAll 方法
let bundle = assetManager.getBundle('bundle1');
// 释放在 Asset Bundle 中的单个资源
bundle.release(`image`, SpriteFrame);
assetManager.removeBundle(bundle);
let bundle = assetManager.getBundle('bundle1');
// 释放所有属于 Asset Bundle 的资源
bundle.releaseAll();
assetManager.removeBundle(bundle);
六、Cocos Creator 大厅+子游戏,从入门到进阶!
在github.com/Nowpaper/CreatorBundleTest下载代码后,可以对照原文进行操作。
1.同项目bundle
导入BundleLobby大厅级别的主体项目
运行Main1.scene,如果点击跳转场景按钮会报错,只有先加载aaa这个bundle才可以跳转。这种划分思路值得学习,相当于把aaa.scene和相关的资源全部划分到一个Bundle中。
onClickLoad(){
cc.assetManager.loadBundle('aaa',(err,bundle)=>{
if(!err){
this._bundle = bundle;
this.progressBar.progress = 1;
this.target1.active = this.target2.active = true;
}
});
}
运行Main2.scene,这个需要
onClickLoad(){
const options = {
version:"08f26",
onFileProgress:(n,t)=>{
this.progressBar.progress = n / t;
}
}
cc.assetManager.loadBundle('http://127.0.0.1:8080/Game1',
options,
(err,bundle)=>{
if(!err){
this._bundle = bundle;
this.target1.active = true;
}
});
}
这个Game1是跨项目的Bundle
2.跨项目bundle
导入BundleGames 子游戏级别的项目
这里双击Game1.scene就能直接运行,现在直接 Build 一下,点击主菜单中的“项目 -> 构建发布”进行构建,目标平台为了方便测试先选择 Web Mobile,稍加等待之后,完成。
构建完成后,进入到项目目录,找到 \build\web-mobile\assets 下面:
这里有个 Game1,它就是 Bundle 包了,其他的都不需要,咱们只需要这个部分。
使用anywhere弄个http服务器
npm install anywhere -g
把 Build 下面的 asset 下的 Game1 移动或复制到D:\CocosProject\remoteDir
中,然后使用anywhere -p 8090 -s
启动服务器。
同时,由于子游戏在 Build 的时候为文件加了 MD5 标记,所以直接打开是不行的,需要借助可选参数的 version 字段来解决这个问题,因此最终的代码如下:
onClickSceneTo(e: cc.Event.EventTouch) {
// cc.director.loadScene('Game1');
e.currentTarget.active = false;
this._bundle.load("prefab/Game1Stage", cc.Prefab, (err, asset: cc.Prefab) => {
if (!err) {
this.target1.addChild(cc.instantiate(asset));
}
});
}
到目前为之,第二个例子已经结束了,虽然已经完成了远程包体的载入流程,但是真正实现一个大厅加子游戏,或者动态功能模块的话,似乎差了一些什么。这种项目需求是要求子包和大厅之间的代码调用,或者互相通讯,下面我们开始尝试用第三个例子来解决这个问题。
3.跨项目 Bundle,代码互调
原文有点复杂,我们先把案例跑起来,看看功能。和上面类似,先把Build出来的Game2文件夹放到服务器上,然后改一下MainScript3.ts中要加载的md5码,就能运行起来了。
onClickLoad() {
const options = {
version: "22860",
onFileProgress: (n, t) => {
this.progressBar.progress = n / t;
}
}
cc.assetManager.loadBundle('http://127.0.0.1:8090/Game2',
options,
(err, bundle) => {
if (!err) {
this._bundle = bundle;
this.target1.active = true;
}
});
}
onClickSceneTo(e: cc.Event.EventTouch) {
// cc.director.loadScene('Game1');
e.currentTarget.active = false;
this._bundle.load("prefab/Game2Stage", cc.Prefab, (err, asset: cc.Prefab) => {
if (!err) {
this.target1.addChild(this._gameStage = cc.instantiate(asset));
}
});
}
private _gameStage: cc.Node;
onClickActonWalk() {
this._gameStage.emit("ActorAnimationPlay", "walk");
}
onClickActonStand() {
this._gameStage.emit("ActorAnimationPlay", "stand");
}
加载后,使用按钮可以控制小熊的stand和walk动作,但是小熊模型下方的label,并没有在切换动作时,做出相应改变。接着看一下BundleGames项目中的GameLogic2.ts才明白,是点击小熊模型,输出label的。
这里结合原文,也能看出两个工程是有双向交互的。
GameLogic2.ts:
start () {
this.node.on(cc.Node.EventType.TOUCH_END,this.onTouchEnd,this);
this.node.on("ActorAnimationPlay",this.onActorAnimationPlay,this);
}
onDestroy(){
this.node.off(cc.Node.EventType.TOUCH_END,this.onTouchEnd,this);
this.node.off("ActorAnimationPlay",this.onActorAnimationPlay,this);
}
private onActorAnimationPlay(aniname:string){
this.actor.playAnimation(aniname,-1);
}
private index = 0;
private onTouchEnd(){
const arr = this.actor.getAnimationNames("ubbie");
const aniName = arr[this.index % arr.length];
this.node.emit("ActorAnimationPlay",aniName);
// this.actor.playAnimation(aniName,-1);
this.index += 1;
let mainCtrl:IMainController = cc.director.getScene().getComponentInChildren('MainControllerScript');
if(!mainCtrl){
mainCtrl = window.debugMainCtrl;
}
mainCtrl.outString(aniName);
}
这里可以看到使用cc.director.getScene().getComponentInChildren('MainControllerScript')
拿到了Main3.scene中的控制逻辑,而onActorAnimationPlay
侦听了Main3.scene的点击事件,从而实现了双向交互。
下面参考一下原文:
在这之前,我们可能需要了解和梳理 Bundle 的机制,在官方文档中描述 Asset Bundle 的构造提到,内容分为代码和资源两个部分,资源的入口是 config.json,代码入口为 index.js。按照我的测试结果来看,Bundle 在下载成功后,会立即将 index.js 中的代码加入到主包中,打开这个文件看看就能猜到个大概。因此,我们只需要设计大厅接口,在子游戏中实现同样的接口,最后不把它们 Build 到 Bundle 即可。设计思路大致为主包大厅和 Bundle 子游戏内创建的控制组件,并开发通用接口,互相之间通过这种方法调用,为了开发的便捷性,可以为子游戏中创建虚拟的接口类,实现独立开发的能力。在大厅项目中,我们新建一个场景 Main3 和 MainScript3 组件脚本,并且按照之前 Main2 样子搭建,有一些部分还得需要结合子游戏修改,先放在这里,现在用 VS Code 在 src 目录中,实现一个接口文件,就叫 IMainController 吧,我这里就简单实现一个输出文本接口:
export interface IMainController { outString(str: string): void;}
在实际项目中,接口可能要比这个复杂的多,主要看你的项目需求,现在我们再建立一个 MainController 的组件脚本。为了区分,我加上了 Script 为后缀,实现基础的组件类代码,并且实现 IMainController 的接口
用事件是一个很好的办法,因此我上面加入了 ActorAnimationPlay 这个事件名的监听,用这个事件来实现控制子游戏的小熊动画,具体代码请参看后续代码。不明白的,可以看代码以及官方文档当中有关事件的部分,子游戏也可以用事件的方式来处理向大厅通讯。但是按照我的经验来看,写接口调用的方式会更加严谨,也比较容易排查错误,有时候甚至还得用上 Promise 异步,如果真的是需要用上事件,也最好封装一下。
注意事项
- 第一是各个 Bundle 中的代码中不要有一样的类名,或者全局变量名,这样的代码会在读取 Bundle 后直接报重名错误。
- 第二是 Bundle 包代码尽量不要互相引用。如果你的业务需求必须这样做,应该用设置载入优先级解决。但只能解决在同一个项目中的 Bundle 读取,跨项目使用还是得自己控制先后顺序。建议可以把通用代码整合成一个包,在开始的时候读下来。
- 第三是跨 Bundle 的资源尽量互相保持独立,对象管理只是一方面,关键是有一些不可预期的奇怪错误,往往会从缓存和释放的地方出问题。
七、使用中的问题
1.场景和相关资源可以全部放入一个bundle
加载进度条场景时,不加载cases这个文件夹的bundle,直接跳转场景director.loadScene("atlas-compress");
会报错:
debug.ts:99 loadScene: Can not load the scene 'atlas-compress'
because it was not in the build settings before playing.
这时候使用assetManager.loadBundle
加载一下bundle就行,并且不需要在加载bundle后去加载scene,而是在用的时候再loadScene。当然也可以使用preloadScene进行预加载。
2.bundle文件夹中未使用的资源也会被打包,不会自动剔除
3.resource bundle规划
当CocosCreator 2.4.4中resources文件夹内容超过4M,达到了15M时
resource作为内置bundle,本身就是为了兼容之前的项目升级才保留的,引擎会在main.js 中去加载,其实跟普通的bundle差别点就是一个加载先后的问题
resources bundle改成普通bundle还是比较好改的.
其实做资源加载功能时, 就不应该区分resources bundle和普通bundle, 这样切换就只是换个名字.
resources是一个引擎内置的 bundle,游戏开始之前就会由引擎自己下载的bundle,过大的话第一次就会黑屏很久,resources里面不能再分 bundle,那你就别放resources里,里面的文件都挪到别的文件夹下,放到新的 bundle 文件夹下,新的bundle 是你自己管理的 bundle,需要你在合适的时机去 cc.assetManager.load(‘bundle_name’,(err,new_bundle)=>{}),然后之前cc.resources.load替换成new_bundle.load就好了
第一步bundle分类,尽量保证bundle大小在2m以内,类似资源尽量放在一个bundle。
第二步制定一个bundle预加载队列,在空闲时间预加载bundle。
第三步也就是最麻烦的一点,如何平衡弱网环境下,预加载的bundle和需要立即加载的bundle,达到一个稍微还可以的体验。
ps:使用了bundle机制之后,总体流畅度感觉没有之前整包高。另,2.4.4使用体验如何,我最近考虑是否升上去
4.其它
手撸三个有关Bundle详细教程,大厅+子游戏模式从入门到进阶,版本Creator 2.4.x
https://docs.cocos.com/creator/3.0/manual/zh/editor/publish/subpackage.html
https://forum.cocos.org/t/creator-asset-bundle/99886
https://github.com/Nowpaper/CreatorBundleTest