[學習筆記]Egret全新RES模組詳解
近期Egret釋出了全新的4.0版本,此次版本中最大的特色就是釋放了全新的RES資源管理模組。相信不少人在官網或者直播中已經對新的RES資源管理模組有所瞭解。這篇文章就全新的RES進行一次介紹。與此同時,在引擎新版本中,由於引入了TypeScript 2.1.4,所以在語法糖層面也增加不少特性,我後續會在其他文章中逐步介紹。
首先來簡單說面一下這次RES模組升級後的特點。相對於舊版本,變化如下:
- 藉助新的ES規範,API使用發生明顯變化
- 資源配置檔案格式改變,減小體積的同時,增加了熱更新機制。
- 新版本RES採用單獨執行方式,和舊版本可以保持很好的共存。當然,在你的專案中你只能選擇使用其中一種。
- 舊版本中的自定義格式解析器被廢除,取而代之的是新的格式解析擴充套件方法。
開發者在剛剛接觸新版本時,由於語法糖變化,可能會帶來暫時的不習慣,當你熟悉這部分語法後,使用起來效率會比以前高很多。為了相容舊版本,在新版本中你也可以使用舊版本的API呼叫方式,但不同的是,一部分API的引數和行為會發生少許變化,這對於就專案遷移尤為需要注意。
安裝
新的RES模組並沒有放置於引擎之中,很多人更新Egret引擎後不知道新版RES所處位置。這裡需要對安裝方法做一個簡單介紹。
當你想使用新版本的RES模組時,你需要藉助npm來記性安裝,安裝命令如下:
npm install egret-resource-manager -g
在這行命令中,新增 -g
引數,我們希望你能夠以全域性方式進行安裝,保證在不同系統賬戶下都可以使用RES命令。
安裝完成後,你即可使用新版本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模組來編寫程式碼。
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
的資源都不會被放入到配置檔案中。
瞭解以上內容,如果你要為自己的格式製作解析器,需要關注三個介面,分別是:
- RES.processor.Processor
- RES.ProcessHost
- RES.ResourceInfo
我們來看一下具體新增一個解析器方法,我們新增一種字尾名為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!