前言
文章按照作者調研和開發順序初步介紹和理解了微信小游戲和白鷺引擎,並產出了基於白鷺引擎的應用初始化程序egret-wechat-start。 以下是正文——
微信小游戲
官方文檔
wx.createCanvas() 創建畫布,
getContext獲取對象后,剩下的就是對原生canvas接口的操作了。 理解到這一點之后,我們就會發現小游戲僅僅是封裝了下創建畫布的接口,剩下的就是用戶需要在畫布里用原生canvas繪制了,並沒有提供其他方便開發的功能。到此我們再看看微信開發者工具創建小游戲項目時,初始化的一個飛機游戲的demo。
是如上圖的一個很簡單的游戲,說下這個游戲的大致實現邏輯:
canvas.addEventListener('click', (event)=>{
獲取event對象x,y
獲取 buttonA:x,y,width,height
判斷是否點擊
獲取 buttonB:x,y,width,height
判斷是否點擊
})
一個彈窗上面的按鈕點擊,反而把彈框下面的按鈕也點擊到了,這不符合預期,那要解決這個問題,我們還需要一個層級管理器,根據層級判斷誰應該觸發,誰不應該觸發。 目前就事件處理我們需要實現兩個基礎功能,事件監聽池和元素對象層級管理器,因為事件只能綁定在canvas上,canvas事件觸發以后,需要一個事件監聽池來遍歷監聽池里的元素對象並判斷誰被觸發了(監聽池也會隨時增減監聽對象),監聽池獲取的依然是一個對象集,層級管理器判斷出對象集里最上層的元素進行觸發。 想想功能好像越來越復雜了。 目前還沒考慮完善,不僅僅是事件處理問題,還可能會有其它大大小小的問題。 用canvas原生開發,工作量可能會非常大。 所以這樣看來,自己把這些實現了是不科學的,需要使用三方引擎開發才行。 因為兩年前用過白鷺引擎,所以就事件監聽和層級管理這個事情,我知道白鷺引擎已經實現了,除開事件,圖形繪制,動畫等等印象中白鷺都提供了,如果用引擎開發小游戲實現成本被大大降低。
白鷺引擎
- Egret Engine2D
- Texture Merger
- Egret 擴展庫
- Egret Wing

Egret Engine2D
開發中主要的核心api
Texture Merger
Texture Merger 可將零散紋理拼合為整圖,同時也可以解析SWF、GIF動畫,制作Egret位圖文本,導出可供Egret使用的配置文件。 我主要使用其中的精靈圖功能,把圖片集合到一張圖上,並且會同時導出一個json的精靈圖的在圖片中的位置等配置信息
Egret 擴展庫
擴展庫在核心引擎功能之上提供了更高級的api,擴展庫在引擎配置文件里配置好以后,會直接把方法和對象載入到egret全局對象中,目前我主要使用的擴展庫有:
- RES: 資源管理庫
- EUI: EUI是一套基於Egret核心顯示列表的UI擴展庫,它封裝了大量的常用UI組件,能夠滿足大部分的交互界面需求,即使更加復雜的組件需求,您也可以基於EUI已有組件進行組合或擴展,從而快速實現需求。
- Game:這個庫好像沒有什么專門的定義,我主要使用了:ScrollView 滾動視圖。 來處理需要滾動的頁面
- Tween: 緩動動畫庫,類似於GreenSock庫

Egret Wing
egret launcher

開始egret開發
你可以快速瀏覽一遍官方教程,以便更好對下文有所理解,http://developer.egret.com/cn/github/egret-docs/Engine2D/getStarted/helloWorld/index.html 。 文章不是教程所以會省略掉那些白鷺官網里的教程。 現在我們使用egret launcher創建一個初始化項目,初始化后的文件結構如下圖,我展開了resource和src文件夾,因為我們需要操作的主要是這兩個文件夾,resource文件夾主要是存放靜態資源,我們的代碼都在src里,白鷺使用的是typescript。

在wing工具里,我們可以馬上開啟調試,就可以在瀏覽器或者它自帶的容器里預覽效果。 main.ts是啟動文件,main中首先使用await對resource中定義好的圖片資源進行了預加載,所以預覽開始后會出現loading效果,loading的繪制是寫在src中LoadingUI.ts,圖片加載完成以后,main里直接創建了下圖2的頁面,並且添加了一個按鈕,點擊后會出現一個彈窗。 效果如下圖。

至此,初始化demo已經告訴了我們如何繪制圖像和綁定事件了,如下圖,我只截取了click按鈕的代碼,圖像繪制首先需要創建一個相應的egret或者eui對象,比如eui.Button、egret.TextField、egret.Bitmap等等,然后給對象設置相應屬性,比如label、x y坐標,width, height等。 再使用main的addChild載入到畫布中(下面的this就是main對象,main繼承於eui.UILayer)。 demo中的代碼在載入loading的時候,使用了this.stage.addChild,直接addChild或者使用stage.addChild都可以載入到畫布中。 白鷺封裝的addEventListener方法和原生js的監聽方法是一樣的使用方法。

demo的代碼說到這里總結一下,我們在main入口對象中可以使用addChild載入一個視圖對象到畫布中,比如文本,按鈕等。 我們也可以在main里addChild一個視圖容器A,視圖容器A也可以添加文本按鈕等,那我們在視圖容器A中再次addChild視圖容器B,那么這樣就形成了層級嵌套main->A->B,如果想象成dom元素就是div.main->div.A->div.B的關系,我們用代碼來對比一下:
class Main extends eui.UILayer { protected createChildren(): void { let A = new egret.DisplayObjectContainer(); this.addChild(A); let textA = new egret.TextField(); textA.text = 'text A Description'; A.addChild(textA); let B = new egret.DisplayObjectContainer(); A.addChild(B); let buttonB = new eui.Button(); buttonB.label = 'button B'; B.addChild(buttonB); } }
對應
<div class="main"> <div class="A"> <span>text A Description</span> <div class="B"> <button value="button B"></button> </div> </div> </div>
根據以上代碼的理解和我們要做的需求(實現一個回合制游戲,這個游戲也有很多頁面,首頁就包含很多按鈕和可能出現的彈窗,也有各種列表頁,還有最關鍵的戰斗頁面)。 我在main里寫一個initElement方法,創建基層容器,代碼如下圖,addChild默認根據先后順序確定上下層關系,先載入的在下層。 首先最下層創建了一個背景層,接着是ScrollView和baseContent,頁面容器會載入到他們之中,如果頁面需要滾動會把頁面視圖對象載入到SV中,不需要滾動會載入到baseContent中,Layer和loading在更上層的位置。

基層容器准備好以后,我們可以創建一個首頁頁面。 我會創建3個文件:base.ts,Index_ui.ts,Index.ts。 Index繼承Index_ui,Index_ui繼承base。 所有的_ui都會繼承base,base會定義通用方法和屬性。 因為一個頁面到最后可能代碼量會比較大,甚至比較亂,所以才把一個頁面拆分成page和page_ui,_ui里寫視圖相關代碼,page里調用_ui的方法、處理請求和編寫邏輯,達到視圖和邏輯分離的效果。 當首頁寫好以后,需要創建一個簡易路由,用路由提供的方法把Index添加到SV容器中。 我把路由直接寫到了main中,changePage就是頁面切換的方法,代碼大致如下:

通過remove和add視圖容器達到了切換頁面的效果。 下面說說編寫_ui頁面的規則,下面是Index_ui的部分代碼,el_layout提前把頁面元素的布局信息提前定義並統一管理。 把Index邏輯頁面需要操作的元素引用到$el對象里方便調用和操作。 把數據信息統一放在$data中。 創建頁面視圖元素之前,需要把第一個元素的y坐標傳給 $firstEleY 這是為了后面pageContentCenter方法能獲取到准確的頁面內容高度,pageContentCenter要執行在所有頁面元素創建完成之后,pageContentCenter會根據當前頁面的高度再匹配當前設備的高度進行垂直居中。
class Index_ui extends Base {
public el_layout = {
indexbg: {x:0, y:0, w:750, h:1665},
gold: {x:300, y:100, w:300, h:39}
};
public constructor() {
super();
this.RES_index = RES.getRes('index');
this.RES_common = RES.getRes('common');
}
public RES_index;
public RES_common;
public $el = {
gold: Object(egret.TextField)
}
public $data = {
gold: '0'
}
public async createView() {
//背景
let RES_bg = new egret.Bitmap( RES.getRes('indexbg') );
$util.setLayout(RES_bg, this.el_layout['indexbg']);
RES_bg.fillMode = egret.BitmapFillMode.REPEAT;
this.$main.PageBg.addChild(RES_bg);
//頂部元素必傳值
this.$firstEleY = this.el_layout.gold.y;
this.pageContentCenter(true);//根據內容計算處理居中
}
}
一個簡易的開發封裝的核心代碼已經搭建好了,而后我們還需要封裝一些其它工具類,如下圖:配置文件($config)、封裝攔截器($api)、濾鏡($filter)、工具函數($util)、微信api封裝(Wx)。 Platform.ts是白鷺自動生成的文件,根據它的規則自己寫了一個Wx.ts文件,由於不同平台的接口形式各有不同,白鷺推薦開發者通過這種方式封裝平台邏輯,以保證整體結構的穩定,白鷺推薦開發者將所有接口封裝為基於 Promise 的異步形式。

和src同級的還有一個texture文件夾,里面是TextureMeger使用精靈圖的相關文件,放在倉庫里是方便后期管理。

簡易的初始化demo,我已經更新到github上https://github.com/zimv/egret-wechat-start。 egret-resource是源碼,egret-resource_wxgame是白鷺打包后的文件夾,它在開發者工具里運行。 egret-resource_wxgame應該在ignore里忽略,這里沒有忽略是方便下載源碼的朋友直接在開發者工具里運行demo。 當前程序使用白鷺引擎版本5.2.5。

demo里隨便寫了幾個頁面,看下效果:

坑
還有踩過很多坑,下面記錄一下:
- 在公眾號后台把設置里的服務類設置成游戲類,輸入appId后會自動打開開發者工具游戲開發的界面
- 小游戲自定義字體微信支持程度差
- 部分功能和api需要注冊的小程序才能使用,比如轉發功能,目前注冊了一個個人小游戲用於前期開發
- 使用wing工具編輯代碼,編譯調試,編譯后的代碼會存放在bin-debug文件夾里,我用的mac,項目菜單里有三個選項編譯、調試和清理。我新增了一個xx文件,卻在調試的時候一直報錯,檢查瀏覽器source里也沒有新增的文件,bin-debug也沒有,弄了很久,一直以為是自己代碼寫錯了,最后意識到可能是編譯器有問題,這個時候我點擊了清理按鈕,新增的文件就在bin-debug里出現了。應該是個bug,要多注意檢查bin-debug里的文件是否有更新
- RES.getResByUrl是網絡異步加載,需要提前addChild保證層級正常,請求完成再修改對象的texture屬性,也可以通過addChildAt方法指定層級。
- TextField 字體size小於10會影響布局,文本是否換行取決於設置的元素高度
- webgl模式無法加載網絡url圖片
- scrollView有addChild方法,但是方法里的代碼是直接拋錯,表示不能用這個接口。它的子元素綁定touchStart move等事件會失效,所以目前又增加里一個baseContent,根據需求切換父容器
- measuredHeight這個測量接口只會測量最上面元素和最下面元素的實際高度,所以第一個元素如果y值大於0要注意配置$firstEleY
- 所有圖片用工具壓縮,會減少上傳代碼的大小和提升資源加載速度
結
當這一切都准備好以后,剩下的就是體力活啦,當然還有游戲最重要的核心玩法實現、動畫和交互效果,這些可能是一個游戲實現難度最大的部分。倉庫地址:https://github.com/zimv/egret-wechat-start 。
