V7投稿 | CocosCreator开源框架oops-framework 之 资源管理(五)

引擎: CocosCreator 3.8.0

环境: Mac

Gitee: oops-game-kit


引言


oops-framework是由作者dgflash编写,基于CocosCreator 3.x而实现的开源框架。

该框架以插件形式存在,主要目的是为了降低与项目的耦合,并且通过插件内部的命令快速的获取最新版本。

该框架的特性有:

  • 提供游戏常用的功能库,提高开发效率
  • 提供业务模块代码模版,降低程序设计难度
  • 内置模块低耦合,可根据需要自行删减,以适应不同的类型
  • 提供了常用的插件工具,支持Excel表转Json、支持热更新、AB包
  • 增加了ECS、MVVM框架相关,以及常用的屏幕适配,UI管理,多语言等等

为了方便大家更好的学习和使用该框架,作者很贴心的准备了一些学习资料:

dgflash-哔哩视频

dgflash CSDN博客

dgflash-cocos论坛

Gitee dgflash项目仓库

注:oops-framework框架QQ群: 628575875

在CocosCreator官方商店,可以通过 oops 搜索更多的框架项目Demo进行学习。

1_2


资源管理


在CocosCreator中,资源可以分为两大类:

  1. 静态引用资源,主要放在res目录中,引擎会对这些资源序列化,自动管理资源的加载和释放相关
  2. 动态引用资源,包含动态加载和远程下载资源,使用灵活,但需要开发者手动释放资源相关

这些资源的动态加载均通过 BundleBundle 可存在多个,主要有两类:

  • 内置Bundle, 主要是resources、start-scene、main等
  • 自定义Bundle, 由开发者设定文件夹自定义的,用于对资源进行更好的管理和使用

Bundle 的使用主要通过CocosCreator引擎封装的 AssetManager 进行资源的远程下载、加载和释放等。简单的说框架的资源管理的底层实现就是对 AssetManager的封装。


ResLoader


oops-framework 中的资源管理主要被 ResLoader 管理,用于管理各种不同类型资源的加载和释放。在 Oops.ts中的定义如下:

// ../oops-plugin-framework/assets/core/Oop.ts
export class oops {
    /** 资源管理 */
    static res = new ResLoader();
}

提供的主要参数或接口有:

参数或接口 说明
defaultBundleName 全局默认加载的资源包名,默认为resources
load() 加载单一任意类型资源
loadAsync() 异步加载单一任意类型资源
loadDir() 加载文件夹中资源
loadRemote() 加载远程资源
loadBundle() 加载资源包
get() 根据路径,资源类型获取资源
dump() 打印缓存中所有资源信息
release() 通过相对路径释放资源
releaseDir() 通过文件夹路径释放所有文件夹中资源

使用的简单方式是:

// 获取指定目录下的图片资源,用于替换图片
let url = `game/textures/image/spriteFrame`;
oops.res.load(url, SpriteFrame, (err, spriteframe) => {
  if (err) {
    return console.error(err.message);
  }
  this._sprite.spriteFrame = spriteframe;
});

本地加载原理

资源的加载接口主要有:

  • load(...)/loadAsync(...) 加载/异步加载单一资源
  • loadDir(...) 加载目录资源

从本质来说,走的流程同 assetManager 是类似的。我们以 load 方法看下逻辑相关:

  1. 调用 load() 方法,支持传入参数: bundle名、目录名、资源类型、加载进度回调和完成回调
load<T extends Asset>(
  bundleName: string,
  paths?: string | string[] | AssetType<T> | ProgressCallback | CompleteCallback | null,
  type?: AssetType<T> | ProgressCallback | CompleteCallback | null,
  onProgress?: ProgressCallback | CompleteCallback | null,
  onComplete?: CompleteCallback | null,
  ) {
    let args: ILoadResArgs<T> | null = null;
    if (typeof paths === "string" || paths instanceof Array) {
      args = this.parseLoadResArgs(paths, type, onProgress, onComplete);
      args.bundle = bundleName;
    }
    else {
      args = this.parseLoadResArgs(bundleName, paths, type, onProgress);
      args.bundle = this.defaultBundleName;
    }
    this.loadByArgs(args);
  }
  • parseLoadResArgs 主要分析参数相关
  • args.bundle 如果设置了Bundle名字则获取,如果没有设置则走获取默认的Bundle名
  1. 调用 loadArgs() 方法,用于通过参数加载指定Bundle下的特定资源
private loadByArgs<T extends Asset>(args: ILoadResArgs<T>) {
  if (args.bundle) {
    // 调用assetManager检测bundle是否存在
    if (assetManager.bundles.has(args.bundle)) {
      // 获取指定名称的bundle
      let bundle = assetManager.bundles.get(args.bundle);
      this.loadByBundleAndArgs(bundle!, args);
    }
    else {
      // bundle不存在,则加载指定参数的bundle
      assetManager.loadBundle(args.bundle, (err, bundle) => {
        if (!err) {
          this.loadByBundleAndArgs(bundle, args);
        }
      })
    }
  }
  else {
    this.loadByBundleAndArgs(resources, args);
  }
}
  1. 通过 loadByBundleAndArgs() 加载Bundle路径下的特定资源
private loadByBundleAndArgs<T extends Asset>(bundle: AssetManager.Bundle, args: ILoadResArgs<T>): void {
    if (args.dir) {
      bundle.loadDir(args.paths as string, args.type, args.onProgress, args.onComplete);
    }
    else {
    if (typeof args.paths == 'string') {
      bundle.load(args.paths, args.type, args.onProgress, args.onComplete);
    }
  else {
    bundle.load(args.paths, args.type, args.onProgress, args.onComplete);
    }
  }
}

注: 这个加载的流程本质就是对assetManager下bundle下资源的加载。

关于默认的Bundle名字改变,在项目启动InitRes.ts中加载必备资源是,可以看到默认Bundle名称的设置:

private loadBundle(queue: AsyncQueue) {
  queue.push(async (next: NextFunction, params: any, args: any) => {
    // 设置默认Bundle名
    oops.res.defaultBundleName = oops.config.game.bundleName;
   	// ...
  });
}

远程加载原理

远程加载资源的主要接口是:

  • loadRemote(...) 支持输入:url地址、资源后缀参数、完成回调

看下代码实现:

loadRemote<T extends Asset>(url: string, ...args: any): void {
  var options: IRemoteOptions | null = null;
  var onComplete: CompleteCallback<T> | null = null;
  if (args.length == 2) {
    options = args[0];
    onComplete = args[1];
  }
  else {
    onComplete = args[0];
  }
  assetManager.loadRemote<T>(url, options, onComplete);
}

注: 资源远程加载的本质是调用 assetManager.loadRemote 方法


释放原理

资源的释放主要接口有:

  • release(path: string, bundleName?: string) 释放单一资源
  • releaseDir(path: string, bundleName?: string) 释放指定目录资源

看下释放单一资源的核心逻辑:

release(path: string, bundleName?: string) {
  // 没有设置bundle名字,则走默认bundle名
  if (bundleName == null) bundleName = this.defaultBundleName;
	// 从assetManager中获取指定bundle名
  var bundle = assetManager.getBundle(bundleName);
  if (bundle) {
    // 根据路径获取bundle中指定的资源
    var asset = bundle.get(path);
    if (asset) {
      this.releasePrefabtDepsRecursively(asset._uuid);
    }
  }
}

private releasePrefabtDepsRecursively(uuid: string) {
  // 通过assetManager释放指定asset资源引用
  var asset = assetManager.assets.get(uuid)!;
  assetManager.releaseAsset(asset);
}

释放目录资源与之类似,但要注意bundle的释放:

releaseDir(path: string, bundleName?: string) {
  if (bundleName == null) bundleName = this.defaultBundleName;

  var bundle: AssetManager.Bundle | null = assetManager.getBundle(bundleName);
  if (bundle) {
    var infos = bundle.getDirWithPath(path);
    if (infos) {
      infos.map((info) => {
        this.releasePrefabtDepsRecursively(info.uuid);
      });
    }

    // 如果设置的bundle名字,非resources则进行释放
    if (path == "" && bundleName != "resources") {
      assetManager.removeBundle(bundle);
    }
  }
}

注: 在CocosCreator的AssetManager资源管理千万记住:释放Bundle和释放资源是不同的,一般先释放资源,再释放对应的bundle。

示例


资源的动态加载和释放

private load(name: string) {
  this.node.active = false;
  var path = GameResPath.getRolePath(name);
  oops.res.load(path, sp.SkeletonData, (err:, sd: sp.SkeletonData) => {
    if (err) {
      console.error(err.message);
      return;
    }

    this.spine.skeletonData = sd;
    this.spine.skeletonData.addRef();
    this.node.active = true;
  });
}

onDestroy() {
  if (this.spine.skeletonData) this.spine.skeletonData.decRef();
}

注意:

  • 资源的动态加载是异步过程,因此在加载前建议将 node.active 设置为false, 在加载完成后再设置为true
  • 资源的动态加载要考虑引用计数

资源远程加载

let url = "https://oops-1255342636.cos-website.ap-shanghai.myqcloud.com/img/bg.png";
var opt = { ext: ".png" };
var onComplete = (err: Error | null, data: ImageAsset) => {
  const spriteFrame = new SpriteFrame();
  const texture = new Texture2D();
  texture.image = data;
  spriteFrame.texture = texture;
  this.sprite.spriteFrame = spriteFrame;
}
oops.res.loadRemote<ImageAsset>(url, opt, onComplete);

资源Bundle的加载

// 以InitRes中加载Bundle为例:
private loadBundle(queue: AsyncQueue) {
  queue.push(async (next: NextFunction, params: any, args: any) => {
    // 设置默认加载的外部资源包名
    oops.res.defaultBundleName = oops.config.game.bundleName;

    // 加载外部资源包
    if (oops.config.game.bundleEnable) {
			let server = oops.config.game.bundleServer;
      let version = oops.config.game.bundleVersion;
      await oops.res.loadBundle(server, version);
    }
    else {
      await oops.res.loadBundle(oops.config.game.bundleName);
    }
    next();
  });
}

资源目录加载

var onProgressCallback = (finished: number, total: number, item: any) => {
    console.log("资源加载进度", finished, total);
}

var onCompleteCallback = () => {
    console.log("资源加载完成");
}
oops.res.loadDir("game", onProgressCallback, onCompleteCallback);

其他

在框架中,由于我们动态资源在 LoadingViewComp 的进度条页面已经通过 loadDir进行了加载,所以在动态更换某个资源时候,我们不需要再调用oops.res.load方法,我们可以这样:

this._sprite.node.active = false;
let url = `game/textures/image/spriteFrame`;

// 获取资源,如果获取到则直接替换,如果没有则动态加载
let spriteFrame = oops.res.get(url, SpriteFrame);
if (spriteFrame != null) {
  this._sprite.spriteFrame = spriteframe;
  this._sprite.node.active = true;
}
else {
  oops.res.load(url, SpriteFrame, (err, spriteframe) => {
    if (err) {
      return console.error(err.message);
    }
		this._sprite.spriteFrame = spriteframe;
  	this._sprite.node.active = true;
  });
}

在项目的开发中,关于动态加载图片用到的地方很多,我们可以对此进行封装下:

// 精灵节点,路径,bundle名
static loadSprite(sprite: Sprite, resUrl: string, bundleName?: string) {
  if (!sprite) {
    console.error("sprite null!!!");
    return;
  }
	
  // 获取bundle名称,如果为空,则为默认值
  if (bundleName == null) {
    bundleName = oops.res.defaultBundleName;
  }
  
  sprite.node.active = false;
  let spriteFrame = oops.res.get(resUrl, SpriteFrame, bundleName);
  if (spriteFrame != null) {
    sprite.spriteFrame = spriteFrame;
    sprite.node.active = true;
  } 
  else {
    oops.res.load(bundleName, resUrl, SpriteFrame, (err, spriteFrame) => {
      if (err) {
        return console.error(err.message);
      }

      if (sprite) {
        sprite.spriteFrame = spriteFrame;
        sprite.node.active = true;
      }
    });
  }
}

最后,祝大家学习生活工作愉快!

oops.res.load方法加载的好慢,就像关卡列表页面中的关卡小图标有时候动态加载好慢,这是什么情况呢?还是图片放错了位置要放到resource里?