微信小遊戲和白鷺引擎開發實踐

子慕大詩人發表於2019-03-03

前言

文章按照作者調研和開發順序初步介紹和理解了微信小遊戲和白鷺引擎,併產出了基於白鷺引擎的應用初始化程式egret-wechat-start。 以下是正文——

微信小遊戲

官方文件

如何開發和理解微信小遊戲,先從官方文件和官方demo入手。 提供一個連結developers.weixin.qq.com/minigame/de…,可以快速瀏覽一下官方文件再繼續看下面的內容。 這裡對微信文件做個簡單的理解總結,小遊戲和小程式很多地方類似,都是提供了同一套微信Api,比如獲取使用者資訊、toast等等,只是有部分提供的api不同。 小遊戲對canvas做了封裝,通過 wx.createCanvas() 建立畫布,getContext獲取物件後,剩下的就是對原生canvas介面的操作了。 理解到這一點之後,我們就會發現小遊戲僅僅是封裝了下建立畫布的介面,剩下的就是使用者需要在畫布裡用原生canvas繪製了,並沒有提供其他方便開發的功能。到此我們再看看微信開發者工具建立小遊戲專案時,初始化的一個飛機遊戲的demo。
是如上圖的一個很簡單的遊戲,說下這個遊戲的大致實現邏輯:
1. 繪製遊戲區域,背景圖片
2. 建立敵機物件,使用者飛機物件,子彈物件
3. 控制3種物件載入畫布和位置改變,控制背景圖片移動,新增音效
4. 判斷子彈碰撞,機身碰撞,並且生成對應結果(敵機消失,遊戲結束)
遊戲中和使用者有互動操作有拖動飛機和彈框中的按鈕,總體是一個很簡單的小遊戲,實現過程也並不複雜。 官方demo中最核心的動畫內容就在loop方法裡,使用的是幀動畫( requestAnimationFrame )來實現介面動畫。 針對遊戲實現動畫效果主要有兩種方式,一種就是requestAnimationFrame幀動畫,一種是用定時器實現。 幀動畫和裝置的處理速度有關係,預設1秒60幀,但是在手機裝置裡即便很簡單的動畫,效能差點的裝置可能幀率都只有20-30左右。 因為幀動畫每秒就要呼叫n次,也許並不需要那麼高頻率的函式呼叫,而定時器總的來說對時間的把控和函式呼叫次數更準確。 比如這個飛機遊戲裡如果有血條的概念,血條的加減其實可以用單獨的定時器來控制。 一個遊戲裡可以兩種方式都使用,根據應用場景選擇更合理的方式。
現在根據一個新的需求來做一個遊戲,再來理解小遊戲的開發。 現在需求實現一個回合制遊戲,這個遊戲也有很多頁面,首頁就包含很多按鈕和可能出現的彈窗,也有各種列表頁,還有最關鍵的戰鬥頁面。 在做實現需求之前,需要提供一些公共的基礎模組:資源預載入,介面攔截器,簡易路由等等。 跳過這些階段,如果我們拿到ui設計,開始做首頁了,首頁有很多按鈕,我們需要給A按鈕新增繫結事件,那我們需要給canvas畫布繫結一個點選事件,點選觸發以後我們獲取到當前使用者點選位置,並取出A按鈕的位置寬高並計算出範圍,進行判斷是否點選位置在範圍內,最後再觸發繫結的方法。 好像有點麻煩,但是還能實現,繼續做下去。 後來需要在首頁做一個彈框,這個時候,給彈框的B按鈕繫結點選事件,又需要通過同樣的方法判斷是否點選到B按鈕。 這個時候彈框的B按鈕剛好和A按鈕重疊都在一個點選範圍內,那按鈕A和B的回撥都會被執行。 程式碼如下:
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全域性物件中,目前我主要使用的擴充套件庫有:

  1.  RES: 資源管理庫
  2.  EUI: EUI是一套基於Egret核心顯示列表的UI擴充套件庫,它封裝了大量的常用UI元件,能夠滿足大部分的互動介面需求,即使更加複雜的元件需求,您也可以基於EUI已有元件進行組合或擴充套件,從而快速實現需求。
  3.  Game:這個庫好像沒有什麼專門的定義,我主要使用了:ScrollView 滾動檢視。 來處理需要滾動的頁面
  4.  Tween: 緩動動畫庫,類似於GreenSock庫

Egret Wing

白鷺開發的程式碼編輯器,像其他編輯器一樣,推薦使用它。

egret launcher

當然還需要安裝一個egret launcher來管理引擎、工具和專案打包,小遊戲就需要打包之後才能在微信開發者工具裡使用

開始egret開發

你可以快速瀏覽一遍官方教程,以便更好對下文有所理解,developer.egret.com/cn/github/e… 。 文章不是教程所以會省略掉那些白鷺官網裡的教程。 現在我們使用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);//載入到main的PageBg裡去,保證背景不滾動

        //頂部元素必傳值
        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上github.com/zimv/egret-…。 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
  • 所有圖片用工具壓縮,會減少上傳程式碼的大小和提升資源載入速度

  當這一切都準備好以後,剩下的就是體力活啦,當然還有遊戲最重要的核心玩法實現、動畫和互動效果,這些可能是一個遊戲實現難度最大的部分。倉庫地址:github.com/zimv/egret-…

相關文章