A闪的 BLOG 技术与人文
近期Egret发布了全新的4.0版本,此次版本中最大的特色就是释放了全新的RES资源管理模块。相信不少人在官网或者直播中已经对新的RES资源管理模块有所了解。这篇文章就全新的RES进行一次介绍。与此同时,在引擎新版本中,由于引入了TypeScript 2.1.4,所以在语法糖层面也增加不少特性,我后续会在其他文章中逐步介绍。
首先来简单说面一下这次RES模块升级后的特点。相对于旧版本,变化如下:
开发者在刚刚接触新版本时,由于语法糖变化,可能会带来暂时的不习惯,当你熟悉这部分语法后,使用起来效率会比以前高很多。为了兼容旧版本,在新版本中你也可以使用旧版本的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模块,但对于要使用的项目需要进行进一步操作。
进入到你的项目中,为了实验对比,我们新建两个项目,一名明明为old
,另外一个命名为new
。然后进入到new
目录下,执行如下命令:
res upgrade
如果命令执行完成后,并没有报错,即证明新版本RES切换成功。
对比新旧两个项目,我们来看一下发生了哪些变化:
bin
目录下,新增了一个名为resourcemanager
的文件夹,该文件夹存放全新RES模块代码。这个文件夹中的代码文件共有4个,四个文件也就是git仓中bin
目录下的4个文件。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模块来编写代码。
在使用旧版本时候,我们所有的资源都依赖于一个名为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
接口包含两个方法,你可以选择实现。
onProgress
:该方法类似于以前的GROUP_PROGRESS
事件,用于读取加载进度。onCancel
:取消使用时,你可以创建一个对象或者一个类,来实现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。
getResAsync
:你可以直接使用该方法获取远程服务器资源,参数为资源相对路径。getResByUrl
:你可以根据资源相对路径获取服务器资源,但推荐在方位不同域资源时使用此方法。你以前熟悉且常用的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!