Cocos Creator 新资源管理系统剖析【一:概述】

【本文参与征文活动】
v2.4开始,Creator使用AssetBundle完全重构了资源底层,提供了更加灵活强大的资源管理方式,也解决了之前版本资源管理的痛点(资源依赖与引用),本文将带你深入了解Creator的新资源底层。

  • 资源与构建
  • 使用AssetBundle
  • 新资源框架剖析
  • 资源加载与释放流程剖析

资源与构建

creator资源文件基础

在了解引擎如何解析、加载资源之前,我们先来了解一下这些资源文件的规则,在creator项目目录下有几个与资源相关的目录:

  • assets 所有资源的总目录,对应creator编辑器的资源管理器
  • library 本地资源库,预览项目时使用的目录
  • build 构建后的项目默认目录

在assets目录下,creator会为每个资源文件和目录生成一个同名的.meta文件,meta文件是一个json文件,记录了资源的版本、uuid以及各种自定义的信息(在编辑器的属性检查器中设置),比如prefab的meta文件,就记录了我们可以在编辑器修改的optimizationPolicy和asyncLoadAssets等属性。

image

{
  "ver": "1.2.7",
  "uuid": "a8accd2e-6622-4c31-8a1e-4db5f2b568b5",
  "optimizationPolicy": "AUTO",     // prefab创建优化策略
  "asyncLoadAssets": false,         // 是否延迟加载
  "readonly": false,
  "subMetas": {}
}

在library目录下的imports目录,资源文件名会被转换成uuid,并取uuid前2个字符进行目录分组存放,creator会将所有资源的uuid到assets目录的映射关系,以及资源和meta的最后更新时间戳放到一个名为uuid-to-mtime.json的文件中,如下所示。

{
  "9836134e-b892-4283-b6b2-78b5acf3ed45": {
    "asset": 1594351233259,
    "meta": 1594351616611,
    "relativePath": "effects"
  },
  "430eccbf-bf2c-4e6e-8c0c-884bbb487f32": {
    "asset": 1594351233254,
    "meta": 1594351616643,
    "relativePath": "effects\\__builtin-editor-gizmo-line.effect"
  },
  ...
}

与assets目录下的资源相比,library目录下的资源合并了meta文件的信息。文件目录则只在uuid-to-mtime.json中记录,library目录并没有为目录生成任何东西。

资源构建

在项目构建之后,资源会从library目录下移动到构建输出的build目录中,基本只会导出参与构建的场景和resources目录下的资源,及其引用到的资源。脚本资源会由多个js脚本合并为一个js,各种json文件也会按照特定的规则进行打包。我们可以在Bundle的配置界面和项目的构建界面为Bundle和项目设置

image

图片、图集、自动图集

导入编辑器的每张图片都会对应生成一个json文件,用于描述Texture的信息,如下所示,默认情况下项目中所有的Texture2D的json文件会被压缩成一个,如果选择无压缩,则每个图片都会生成一个Texture2D的json文件。

{
  "__type__": "cc.Texture2D",
  "content": "0,9729,9729,33071,33071,0,0,1"
}

如果将纹理的Type属性设置为Sprite,Creator还会自动生成了SpriteFrame类型的json文件。
图集资源除了图片外,还对应一个图集json,这个json包含了cc.SpriteAtlas信息,以及每个碎图的SpriteFrame信息
自动图集在默认情况下只包含了cc.SpriteAtlas信息,在勾选内联所有SpriteFrame的情况下,会合并所有SpriteFrame

Prefab与场景

场景资源与Prefab资源非常类似,都是一个描述了所有节点、组件等信息的json文件,在勾选内联所有SpriteFrame的情况下,Prefab引用到的SpriteFrame会被合并到prefab所在的json文件中,如果一个SpriteFrame被多个prefab引用,那么每个prefab的json文件都会包含该SpriteFrame的信息。而在没有勾选内联所有SpriteFrame的情况下,SpriteFrame会是单独的json文件。

小结

当Creator将多个资源合并到一个json文件中,我们可以在config.json中的packs字段找到被打包的资源信息,一个资源有可能被重复打包到多个json中。下面举一个例子,展示在不同的选项下,creator的构建规则:

  • a.png 一个单独的Sprite类型图片
  • dir/b.png、c.png、AutoAtlas dir目录下包含2张图片,以及一个AutoAtlas
  • d.png、d.plist 普通图集
  • e.prefab 引用了SpriteFrame a和b的prefab
  • f.prefab 引用了SpriteFrame b的prefab

默认选项在绝大多数情况下都是一个不错的选择,如果是web平台,建议勾选内联所有SpriteFrame这可以减少网络io,提高性能,而原生平台不建议勾选,这可能会增加包体大小以及热更时要下载的内容。对于一些紧凑的Bundle(比如加载该Bundle就需要用到里面所有的资源),我们可以配置为合并所有的json。

理解与使用 Asset Bundle

创建Bundle

Asset Bundle是creator 2.4之后的资源管理方案,简单地说就是通过目录来对资源进行规划,按照项目的需求将各种资源放到不同的目录下,并将目录配置成Asset Bundle。能够起到以下作用:

  • 加快游戏启动时间
  • 减小首包体积
  • 跨项目复用资源
  • 方便实现子游戏
  • 以Bundle为单位的热更新

Asset Bundle的创建非常简单,只要在目录的属性检查器中勾选配置为bundle即可,其中的选项官方文档都有比较详细的介绍。

其中关于压缩的理解,文档并没有详细的描述,这里的压缩指的并不是zip之类的压缩,而是通过packAssets的方式,把多个资源的json文件合并到一个,达到减少io的目的。

image

在选项上打勾非常简单,真正的关键在于如何规划Bundle,规划的原则在于减少包体、加速启动以及资源复用。根据游戏的模块来规划资源是比较不错的选择,比如按子游戏、关卡副本、或者系统功能来规划。

Bundle会自动将文件夹下的资源,以及文件夹中引用到的其它文件夹下的资源打包(如果这些资源不是在其它Bundle中),如果我们按照模块来规划资源,很容易出现多个Bundle共用了某个资源的情况。可以将公共资源提取到一个Bundle中,或者设置某个Bundle有较高的优先级,构建Bundle的依赖关系,否则这些资源会同时放到多个Bundle中(如果是本地Bundle,这会导致包体变大)。

使用Bundle

Bundle的使用也非常简单,如果是resources目录下的资源,可以直接使用cc.resources.load来加载

cc.resources.load("test assets/prefab", function (err, prefab) {
    var newNode = cc.instantiate(prefab);
    cc.director.getScene().addChild(newNode);
});

如果是其它自定义Bundle(本地Bundle或远程Bundle都可以用Bundle名加载),可以使用cc.assetManager.loadBundle来加载Bundle,然后使用加载后的Bundle对象,来加载Bundle中的资源。对于原生平台,如果Bundle被配置为远程包,在构建时需要在构建发布面板中填写资源服务器地址。

cc.assetManager.loadBundle('01_graphics', (err, bundle) => {
    bundle.load('xxx');
});

原生或小游戏平台下,我们还可以这样使用Bundle:

  • 如果要加载其它项目的远程Bundle,则需要使用url的方式加载(其它项目指另一个cocos工程)
  • 如果希望自己管理Bundle的下载和缓存,可以放到本地可写路径,并传入路径来加载这些Bundle
// 当复用其他项目的 Asset Bundle 时
cc.assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => {
    bundle.load('xxx');
});

// 原生平台
cc.assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => {
    // ...
});

// 微信小游戏平台
cc.assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => {
    // ...
});

其它注意项:

  • 加载Bundle仅仅只是加载了Bundle的配置和脚本而已,Bundle中的其它资源还需要另外加载
  • 目前原生的Bundle并不支持zip打包,远程包下载方式为逐文件下载,好处是操作简单,更新方便,坏处是io多,流量消耗大
  • 不同Bundle下的脚本文件不要重名
  • 一个Bundle A依赖另一个Bundle B,如果B没有被加载,加载A时并不会自动加载B,而是在加载A中依赖B的那个资源时报错

新资源框架

v2.4重构后的新框架代码更加简洁清晰,我们可以先从宏观角度了解一下整个资源框架,资源管线是整个框架最核心的部分,它规范了整个资源加载的流程,并支持对管线进行自定义。

  • 公共文件
    • helper.js 定义了一堆公共函数,如decodeUuid、getUuidFromURL、getUrlWithUuid等等
    • utilities.js 定义了一堆公共函数,如getDepends、forEach、parseLoadResArgs等等
    • deserialize.js 定义了deserialize方法,将json对象反序列化为Asset对象,并设置其__depends__属性
    • depend-util.js 控制资源的依赖列表,每个资源的所有依赖都放在_depends成员变量中
    • cache.js 通用缓存类,封装了一个简易的键值对容器
    • shared.js 定义了一些全局对象,主要是Cache和Pipeline对象,如加载好的assets、下载完的files以及bundles等
  • Bundle部分
    • config.js bundle的配置对象,负责解析bundle的config文件
    • bundle.js bundle类,封装了config以及加载卸载bundle内资源的相关接口
    • builtins.js 内建bundle资源的封装,可以通过 cc.assetManager.builtins 访问
  • 管线部分
    • CCAssetManager.js 管理管线,提供统一的加载卸载接口
    • 管线框架
      • pipeline.js 实现了管线的管道组合以及流转等基本功能
      • task.js 定义了一个任务的基本属性,并提供了简单的任务池功能
      • request-item.js 定义了一个资源下载项的基本属性,一个任务可能会生成多个下载项
    • 预处理管线
      • urlTransformer.js parse将请求参数转换成RequestItem对象(并查询相关的资源配置),combine负责转换真正的url
      • preprocess.js 过滤出需要进行url转换的资源,并调用transformPipeline
    • 下载管线
      • download-dom-audio.js 提供下载音效的方法,使用audio标签进行下载
      • download-dom-image.js 提供下载图片的方法,使用Image标签进行下载
      • download-file.js 提供下载文件的方法,使用XMLHttpRequest进行下载
      • download-script.js 提供下载脚本的方法,使用script标签进行下载
      • downloader.js 支持下载所有格式的下载器,支持并发控制、失败重试、
    • 解析管线
      • factory.js 创建Bundle、Asset、Texture2D等对象的工厂
      • fetch.js 调用packManager下载资源,并解析依赖
      • parser.js 对下载完成的文件进行解析
  • 其它
    • releaseManager.js 提供资源释放接口、负责释放依赖资源以及场景切换时的资源释放
    • cache-manager.d.ts 在非WEB平台上,用于管理所有从服务器上下载下来的缓存
    • pack-manager.js 处理打包资源,包括拆包,加载,缓存等等
11赞

:clap::+1:

期待下册111

图挂了吗。啊?

mark!