Cocos 3.x 资源管理系列二 Asset Bundle

参考
Asset Bundle 介绍
Creator | Asset Bundle 全解析
讨论下2.4.3,内置的AB包能否被loadBundle方法加载出来
新版AssetBundle项目实际运用经验总结

一、内置 Asset Bundle
image.png
1. resources 优先级8

resources 目录下的所有资源以及其依赖资源

2. main主包 优先级7

构建发布 面板的 参与构建场景 中勾选的场景以及其依赖资源

注:这里我做了测试,如果一个场景参与构建,使用的图片会打到main包里。如果把场景和使用的资源划分到Bundle中,则不会打到main包里,而是有个单独的bundle文件夹。


image.png

这里把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 时,若勾选了 配置为远程包,那么构建时请在 构建发布 面板中填写 资源服务器地址。


image.png
二、Asset Bundle配置与构建
1.不同目标平台分别进行配置

自定义 Asset Bundle 是以 文件夹 为单位进行配置的。当我们在 资源管理器 中选中一个文件夹时,属性检查器 中就会出现一个 配置为 Bundle 的选项,勾选后会出现如下图的配置项:


image.png

经过测试,配置bundle后。即使项目中没有使用这个bundle里的资源,构建时也能看到它。

2.构建

构建后生成的 Asset Bundle 目录结构如下图所示:


image.png
  • 代码:文件夹中的所有代码会根据发布平台合并成一个 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。


image.png

注意:有些平台不允许加载远程的脚本文件,例如微信小游戏,在这些平台上,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 值。如图所示:


image.png

在加载 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大厅级别的主体项目


image.png

运行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,稍加等待之后,完成。


image.png

构建完成后,进入到项目目录,找到 \build\web-mobile\assets 下面:


image.png

这里有个 Game1,它就是 Bundle 包了,其他的都不需要,咱们只需要这个部分。

使用anywhere弄个http服务器
npm install anywhere -g
把 Build 下面的 asset 下的 Game1 移动或复制到D:\CocosProject\remoteDir中,然后使用anywhere -p 8090 -s启动服务器。

同时,由于子游戏在 Build 的时候为文件加了 MD5 标记,所以直接打开是不行的,需要借助可选参数的 version 字段来解决这个问题,因此最终的代码如下:


image.png
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
image.png

加载进度条场景时,不加载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

你可能感兴趣的:(Cocos 3.x 资源管理系列二 Asset Bundle)