Egret全新RES模块详解

近期Egret发布了全新的4.0版本,此次版本中最大的特色就是释放了全新的RES资源管理模块。相信不少人在官网或者直播中已经对新的RES资源管理模块有所了解。这篇文章就全新的RES进行一次介绍。与此同时,在引擎新版本中,由于引入了TypeScript 2.1.4,所以在语法糖层面也增加不少特性,我后续会在其他文章中逐步介绍。

首先来简单说面一下这次RES模块升级后的特点。相对于旧版本,变化如下:

  1. 借助新的ES规范,API使用发生明显变化
  2. 资源配置文件格式改变,减小体积的同时,增加了热更新机制。
  3. 新版本RES采用单独运行方式,和旧版本可以保持很好的共存。当然,在你的项目中你只能选择使用其中一种。
  4. 旧版本中的自定义格式解析器被废除,取而代之的是新的格式解析扩展方法。

开发者在刚刚接触新版本时,由于语法糖变化,可能会带来暂时的不习惯,当你熟悉这部分语法后,使用起来效率会比以前高很多。为了兼容旧版本,在新版本中你也可以使用旧版本的API调用方式,但不同的是,一部分API的参数和行为会发生少许变化,这对于就项目迁移尤为需要注意。

安装

新的RES模块并没有放置于引擎之中,很多人更新Egret引擎后不知道新版RES所处位置。这里需要对安装方法做一个简单介绍。

如果你想查看源码,可以访问我们的git仓,直接clone相关代码即可,git仓地址:https://github.com/egret-labs/resourcemanager

当你想使用新版本的RES模块时,你需要借助npm来记性安装,安装命令如下:

npm install egret-resource-manager -g

在这行命令中,添加 -g 参数,我们希望你能够以全局方式进行安装,保证在不同系统账户下都可以使用RES命令。

安装完成后,你即可使用新版本RES模块,但对于要使用的项目需要进行进一步操作。

将项目切换为全新的RES模块

进入到你的项目中,为了实验对比,我们新建两个项目,一名明明为old,另外一个命名为new 。然后进入到new目录下,执行如下命令:

res upgrade

如果命令执行完成后,并没有报错,即证明新版本RES切换成功。

对比新旧两个项目,我们来看一下发生了哪些变化:

  1. 新项目中,在bin目录下,新增了一个名为resourcemanager的文件夹,该文件夹存放全新RES模块代码。这个文件夹中的代码文件共有4个,四个文件也就是git仓中bin目录下的4个文件。
  2. egretProperties.json文件中,原有modules节点中的res节点被删除,取而代之的是名为resourcemanager的模块配置。

除了以上两点,你还需要自己修改一下tsconfig.json文件。因为全新的RES模块依赖于ES2015标准中的Promise对象,所以在编译器编译阶段,我们需要在这里添加对Promise对象的编译支持。修改后内容如下:

{
   "compilerOptions": {
      "target": "es5",
      "experimentalDecorators":true,
      "lib": [
          "es5","dom","es2015.promise"
      ]
   },
   "exclude": [
      "node_modules"
   ]
}

以上工作都完成后,我们即可使用全新的RES模块来编写代码。

RES模块代码编写第一步

在使用旧版本时候,我们所有的资源都依赖于一个名为default.res.json的资源配置文件,该文件记录这资源的相对路径和配置name属性名称。那么新版本的资源配置文件如何生成?这里需要使用RES命令的另外一个操作,build资源。进入到项目目录上一层,然后执行如下命令:

res build 你的项目名称

这里的项目名称和你的文件夹名称相同,如果没有发生任何报错,那么你在打开项目中的resource目录,会发现其中多了一个名称为config.json的文件。该文件就是新版RES模块的配置文件。我们可以来对比一下两个配置文件的不同。

旧版本

{
	"groups":[
	{
		"keys":"bg_jpg,egret_icon_png,description_json",
		"name":"preload"
	}],
	"resources":[
	{
		"name":"bg_jpg",
		"type":"image",
		"url":"assets/bg.jpg"
	},
	{
		"name":"egret_icon_png",
		"type":"image",
		"url":"assets/egret_icon.png"
	},
	{
		"name":"description_json",
		"type":"json",
		"url":"config/description.json"
	}]
}

新版本

{
	"alias": {
		"bg_jpg": "assets/bg.jpg",
		"egret_icon_png": "assets/egret_icon.png",
		"description_json": "config/description.json"
	},
	"groups": {
		"preload": [
			"bg_jpg",
			"egret_icon_png",
			"description_json"
		]
	},
	"resources": {
		"default.res.json": "default.res.json",
		"assets": {
			"bg.jpg": "assets/bg.jpg",
			"egret_icon.png": "assets/egret_icon.png"
		},
		"config": {
			"description.json": "config/description.json"
		},
		"config.json": "config.json"
	}
}

两个配置文件格式存在明显区别,在新版本中,我们取消了资源的name属性,取而代之的是直接使用资源的路径作为资源获取时所传入的参数。这样就避免了开发过程中资源匹配识别错误而引发的一系列麻烦。

与此同时,该格式也有助于我们后面的资源热更新解决方案,关于热更新我们后面会有所介绍。

这里需要注意一点,当你对资源进行修改后,每一次修改都需要执行res build命令,以便生成新的资源配置文件。

编写资源加载代码

由于TypeScript编译器的升级,我们现在可以编写ES6标准代码,也可以编写ES2015标注代码。下面我们来一一介绍。至于两种标注,该如何选取,取决于你对标准的熟悉程度和习惯。

旧版本的RES模块

旧版本中,资源操作中的所有事件回调,全部依赖于Egret内部所提供的事件模型。换句话说,当你在非Egret引擎下,旧版RES模块就无法使用,代码编写风格如下:

//首先根据我们的需要注册事件侦听器,一遍当状态放生变化时,回调我们指定的函数。
RES.addEventListener(RES.ResourceEvent.CONFIG_COMPLETE, this.onConfigComplete, this);
//执行加载动作
RES.RES.loadConfig("resource/default.res.json", "resource/");

//当配置文件加载完成后,调用onConfigComplete方法
private onConfigComplete(event: RES.ResourceEvent): void {
    ...
}

上面一段代码是我们旧版本中编写一处资源加载的代码,由于大家比较熟悉,不再赘述。

ES6下的RES模块

ES6标准中,同样实现上面功能,我们可以使用Promise对象的标准异步语法,同时借助箭头函数来完成相关回调函数的定义。

RES.loadConfig().then(()=>{
	console.log("config file load complete!");
})

上面一段代码中,loadConfig方法返回一个Promise对象,该对象拥有一个then函数。当事件完成后,则回调then函数参数中的函数。

如果你想捕获资源加载失败的事件,则调用Promise对象的catch函数即可,代码如下:

RES.LoadConfig.then(()=>{
	console.log("config file load complete!");
}).catch((err)=>{
	console.log(err);
});

catch中的回调函数,可接受一个error参数,参数中是错误类型和错误信息。

ES2015下的RES

从刚才的讲解中,我们可以看到相比旧版本的API,借助ES6的语法特性,我们的代码更加简洁,并且了原有的事件帧听过程。但函数回调所带来的不仅仅是代码简约,同时也让代码看上去极为丑陋。使用ES2015中的await关键词,可极大的解决这方面的问题,但你需要注意的时,异步阻塞过程并非你想的如此简单,一些时候你可能会被自己坑掉。

解决函数回调最好的形式是让回调函数能够像正常函数调用一样,代码看上去是顺序执行。也就是完成第一行执行后,再执行第二行。而对于网络加载来说,我们的资源需要等待一段时间才能够正常使用,这个过程中,我们希望游戏的其他逻辑还能够正常执行。为了解决这个问题,你需要在使用await的同时,一并使用async异步操作。

我们来看下面一段代码:

RES.loadConfig();
RES.getResAsync("images/logo.jpg");

这段代码执行后会发生报错,简单原因在于,当第一行执行后,会立刻执行第二行语句。而此时我们的资源配置文件并没有加载完成。虽然传递了参数,但无法找到对应的资源。所以会发生报错。那么如果让代码在执行完第一行后,等待加载结束再执行第二行呢?答案是使用await阻塞。

修改代码如下:

await RES.loadConfig();
await getResAsync("images/logo.jpg");

这样修改后,我们的就可以实现想要的效果。你需要注意的是,await仅对Promise对象有效。

如果你将这样的代码放到游戏业务逻辑之中,依然存在恼人的Bug。因为此时不仅网络加载操作被阻塞,你的其他逻辑也会被阻塞。此时如果你想做其他操作,就必须等待loadConfig加载完成。这个又不是我们想要的效果。那么再次修改代码如下:

private asyn loadRes() {
	await RES.loadConfig();
	await RES.getResAsync("images/logo.jpg);
}

我们将浙西阻塞代码全部放到一个异步函数中,从而解决刚才的问题。async关键词必须在访问权限修饰词之后。在调用时,使用方法如下:

this.loadRes();
this.runGame();

通过这种调用,我们在开始加载资源后,可以立刻执行runGame方法。

如果你需要捕获加载错误信息,可以使用try...catch方法,代码如下:

try{
	this.loadRes();
	this.runGame();
}catch(err){
	console.log( err );
}

加载进度问题

游戏资源加载过程中,通常都会存在一个loading进度条,或者能够表示加载进度的展示方法。在新的RES模块中,读取加载进度也非常的方便。在RES模块中,新增了一个名为PromiseTaskReporter的接口,借助这个接口,我们可以实现读取加载进度的效果。

PromiseTaskReporter接口包含两个方法,你可以选择实现。

使用时,你可以创建一个对象或者一个类,来实现PromiseTaskReporter接口,并在loadGroup加载组时,将其实例化对象作为参数传递进去。实例代码如下:

private async loadRes()
{
	await RES.loadConfig();

	let loading:RES.PromiseTaskReporter = {
		onProgress(current: number, total: number){
			console.log(current+"/"+total)
		}
	};
	await RES.loadGroup("preload",0,loading);
	this.createGameScene();
}

运行项目,你可以在控制台看到打印如下内容:

1/4
2/4
3/4
4/4

资源获取方法

由于新版的RES模块和旧版本保持较好的向前兼容性,所以在资源获取方面没有太大变化。在一些特定API下,由于资源格式定义规则发生变化,所以行为也会有少许变化。其中包含两个API存在功能类似问题,但你可以在不同场景中使用不同的API。

你以前熟悉且常用的getRes等方法,他们的行为保持不变。

自定义格式解析器

当你用res命令升级项目后,会发现在Main.ts文件的class定义前多出一段代码,内容如下:

@RES.mapConfig("config.json",()=>"resource",path => {
    var ext = path.substr(path.lastIndexOf(".") + 1);
    var typeMap = {
        "jpg": "image",
        "png": "image",
        "webp": "image",
        "json": "json",
        "fnt": "font",
        "pvr": "pvr",
        "mp3": "sound"
    }
    var type = typeMap[ext];
    if (type == "json") {
        if (path.indexOf("sheet") >= 0) {
            type = "sheet";
        } else if (path.indexOf("movieclip") >= 0) {
            type = "movieclip";
        };
    }
    return type;
})

这是TypeScript中的注解语法,借助这段语法,我们可以在res build命令时配置需要的资源格式,并且在运行时,根据后缀名来判断资源类型。

如果你将这段代码中typeMap变量进行修改,例如删除jpg一行,那么资源中所有后缀名为jpg的资源都不会被放入到配置文件中。

了解以上内容,如果你要为自己的格式制作解析器,需要关注三个接口,分别是:

我们来看一下具体添加一个解析器方法,我们添加一种后缀名为jsn的文件,该类型文件实际上就是json格式文件,为了方便讲解,我们不在自定义一个格式,大家可以举一反三。

第一步

我们复制description.json文件,并将其名称改为d.jsn,放置于同目录下。

第二步

创建一个新的类,名称为JsonAnalyzer的对象,并实现RES.processor.Processor接口。

代码如下:

let JsonAnalyzer: RES.processor.Processor = {
	async onLoadStart(host, resource) {
		let data = await host.load(resource, RES.processor.JsonProcessor);
		return data;
	},

	onRemoveStart(host, request) {
		return Promise.resolve();
	}
}

第三步

我们需要将新的解释器注入到RES模块中,并且修改开始的注解函数。

注入RES模块方法如下:

RES.processor.map("myjson", JsonAnalyzer);

我将这个格式内部名称定义为myjson,注解函数代码如下:

@RES.mapConfig("config.json", () => "resource", path => {
    var ext = path.substr(path.lastIndexOf(".") + 1);
    var typeMap = {
        "jpg": "image",
        "png": "image",
        "webp": "image",
        "json": "json",
        "fnt": "font",
        "pvr": "pvr",
        "mp3": "sound",
        "font": "fft",
        "jsn": "myjson"
    }
    var type = typeMap[ext];
    if (type == "json") {
        if (path.indexOf("sheet") >= 0) {
            type = "sheet";
        } else if (path.indexOf("movieclip") >= 0) {
            type = "movieclip";
        };
    }
    return type;
})

再一次执行res build,然后编写测试代码,看能否打印出你要的内容。

console.log(RES.getResAsync("config/d.jsn"));

关于新的RES模块的介绍就到这里!

enjoy!