1. 程式人生 > >[學習筆記]Egret全新RES模組詳解

[學習筆記]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所處位置。這裡需要對安裝方法做一個簡單介紹。

當你想使用新版本的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介面包含兩個方法,你可以選擇實現。

  • 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!