作者 | huangjianke
責編 | 伍杏玲
【CSDN 編者按】據微信最新數據,微信小遊戲累計註冊用戶量已突破10億。那麼初學者如何開發一款好玩又燒腦的微信小遊戲呢?本文作者將詳細為大家講解。
「啟邏輯之高妙,因想像而自由。」層疊拼圖Plus是一款需要空間想像力和邏輯推理能力完美結合的微信小遊戲,偶消奇不消,在簡單的遊戲規則下卻有著無數種可能性,需要你充分發揮想像力去探索,看似簡單卻具有極大的挑戰性和趣味性,Talk is cheap. Show me the code!
層疊拼圖Plus微信小遊戲採用JavaScript+Canvas實現,沒有使用任何遊戲引擎,對於初學者來說,也比較容易入門。下面是小遊戲頁面:
如何解決Canvas繪圖模糊?
Canvas 繪圖時,會從兩個物理像素的中間位置開始繪製並向兩邊擴散 0.5 個物理像素。當設備像素比為 1 時,一個 1px 的線條實際上佔據了兩個物理像素(每個像素實際上只佔一半),由於不存在 0.5 個像素,所以這兩個像素本來不應該被繪製的部分也被繪製了,於是 1 物理像素的線條變成了 2 物理像素,視覺上就造成了模糊
繪圖模糊的原因知道了,在微信小遊戲裡面又該如何解決呢?
可以看到,我們先通過 wx.getSystemInfoSync().pixelRatio 獲取設備的像素比ratio,然後將在屏 Canvas 的寬度和高度按照所獲取的像素比ratio進行放大,在繪製文字、圖片的時候,坐標點 x、y 和所要繪製圖形的 width、height均需要按照像素比 ratio 進行縮放,這樣我們就可以清晰的在高清屏中繪製想要的文字、圖片。
可參考微信官方縮放策略調整
另外,需要注意的是,這裡的 canvas 是由 weapp-adapter 預先調用 wx.createCanvas() 創建一個上屏 Canvas,並暴露為一個全局變量 canvas。
如何繪製任意多邊形圖形?
任意一個多邊形圖形,是由多個平面坐標點所組成的圖形區域。
在遊戲畫布內,我們以左上角為坐標原點 {x: 0, y: 0} ,一個多邊形包含多個單位長度的平面坐標點,如:[{ x: 1, y: 3 }, { x: 5, y: 3 }, { x: 3, y: 5 }] 表示為一個三角形的區域,需要注意的是,x、y 並不是真實的平面坐標值,而是通過屏幕寬度計算出來的單位長度,在畫布內的真實坐標值則為 {x: x * itemWidth, y: y * itemWidth} 。
繪製多邊形代碼實現如下:
使用:
效果如下圖:
CanvasRenderingContext2D其他使用方法可參考:CanvasRenderingContext2D API 列表
1 + 1 = 0,「偶消奇不消」的效果如何實現?
1 + 1 = 0,是層疊拼圖Plus小遊戲玩法的精髓所在。
有經驗的同學,也許一眼就發現了,1 + 1 = 0 剛好符合通過異或運算得出的結果。當然,細心的同學也可能已經發現,上文有一句特殊的代碼:this.ctx.globalCompositeOperation = 'xor',也正是通過設置 CanvasContext 的 globalCompositeOperation 屬性值為 xor 便實現了「偶消奇不消」的神奇效果。
globalCompositeOperation 是指 在繪製新形狀時應用的合成操作的類型,其他效果可參考:globalCompositeOperation 示例
如何判斷一個點是否在任意多邊形內部?
當迴轉數為 0 時,點在閉合曲線外部。
講到這裡,我們已經知道如何在Canvas畫布內繪製出偶消奇不消效果的層疊圖形了,接下來我們來看下玩家如何移動選中的圖形。我們發現繪製出的圖形對象並沒有提供點擊事件綁定之類的操作,那又如何判斷玩家選中了哪個圖形呢?這裡我們就需要去實現如何判斷玩家觸摸事件的x,y坐標在哪個多邊形圖形內部區域,從而判斷出玩家選中的是哪一個多邊形圖形。
判斷一個點是否在任意多邊形內部有多種方法,比如:
射線法面積判別法叉乘判別法迴轉數法...在層疊拼圖Plus小遊戲內,採用的是迴轉數法來判斷玩家觸摸點是否在多邊形內部。迴轉數是拓撲學中的一個基本概念,具有很重要的性質和用途。當然,展開討論迴轉數的概念並不在該文的討論範圍內,我們僅需了解一個概念:當迴轉數為 0 時,點在閉合曲線外部。
圖源:http://www.html-js.com/article/1538
上面面這張圖動態演示了迴轉數的概念:圖中紅色曲線關於點(人所在位置)的迴轉數為 2。
對於給定的點和多邊形,迴轉數應該怎麼計算呢?
用線段分別連接點和多邊形的全部頂點
圖源:http://www.html-js.com/article/1538
計算所有點與相鄰頂點連線的夾角
圖源:http://www.html-js.com/article/1538
計算所有夾角和。注意每個夾角都是有方向的,所以有可能是負值
圖源:http://www.html-js.com/article/1538
最後根據角度累加值計算迴轉數。360°(2π)相當於一次迴轉。
在使用 JavaScript 實現時,需要注意以下問題:
JavaScript 的數只有 64 位雙精度浮點這一種。對於三角函數產生的無理數,浮點數計算不可避免會造成一些誤差,因此在最後計算迴轉數需要做取整操作。通常情況下,平面直角坐標系內一個角的取值範圍是 -π 到 π 這個區間,這也是 JavaScript 三角函數 Math.atan2() 返回值的範圍。但 JavaScript 並不能直接計算任意兩條線的夾角,我們只能先計算兩條線與 x 正軸夾角,再取兩者差值。這個差值的結果就有可能超出 -π 到 π 這個區間,因此我們還需要處理差值超出取值區間的情況。代碼實現:
如何判斷遊戲結果是否正確?
探索的過程固然精彩,而結果卻更令我們期待
通過前面的介紹我們可以知道,判斷遊戲結果是否正確其實就是比對玩家組合圖形的 xor 結果與目標圖形的 xor 結果。那麼如何求多個多邊形 xor 的結果呢?polygon-clipping 正是為此而生的。它不僅支持 xor 操作,還有其他的比如:union, intersection, difference 等操作。在層疊拼圖Plus遊戲內通過 polygon-clipping 又是怎樣實現遊戲結果判斷的呢?
目標圖形
多邊形平面坐標點集合:
獲取 多個多邊形 xor 結果:
xor結果:
同理計算出玩家操作圖形的xor結果進行比對即可得出答案正確與否。
需要注意的是,獲取玩家的 xor 結果並不能直接拿來與目標圖形xor 結果進行比較,我們需要將xor 的結果以左上角為參考點將圖形平移至原點內,然後再進行比較,如果結果一致,則代表玩家答案正確。
排行榜的展示
有人的地方就有江湖,有江湖的地方就有排行
在看本章節內容之前,建議先瀏覽一遍排行榜相關的官方文檔:好友排行榜、關係鏈數據,以便對相關內容有個大概的了解。
開放數據域開放數據域是一個封閉、獨立的 JavaScript 作用域。要讓代碼運行在開放數據域,需要在 game.json 中添加配置項 openDataContext 指定開放數據域的代碼目錄。添加該配置項表示小遊戲啟用了開放數據域,這將會導致一些限制。
在遊戲內使用 wx.setUserCloudStorage(obj) 對玩家遊戲數據進行託管。在開放數據域內使用 wx.getFriendCloudStorage(obj)拉取當前用戶所有同玩好友的託管數據展示關係鏈數據如果想要展示通過關係鏈 API 獲取到的用戶數據,如繪製排行榜等業務場景,需要將排行榜繪製到 sharedCanvas 上,再在主域將 sharedCanvas 渲染上屏。
sharedCanvas 是主域和開放數據域都可以訪問的一個離屏畫布。在開放數據域調用 wx.getSharedCanvas() 將返回 sharedCanvas。
在主域中可以通過開放數據域實例訪問 sharedCanvas,通過 drawImage() 方法可以將 sharedCanvas 繪製到上屏畫布。
sharedCanvas 本質上也是一個離屏 Canvas,而重設 Canvas 的寬高會清空 Canvas 上的內容。所以要通知開放數據域去重繪 sharedCanvas。
需要注意的是:sharedCanvas 的寬高只能在主域設置,不能在開放數據域中設置。
遊戲性能優化
性能優化,簡而言之,就是在不影響系統運行正確性的前提下,使之運行地更快,完成特定功能所需的時間更短。
一款能讓人心情愉悅的遊戲,性能問題必然不能成為絆腳石。那麼可以從哪些方面對遊戲進行性能優化呢?
離屏 Canvas
在層疊拼圖Plus小遊戲內,針對需要大量使用且繪圖繁複的靜態場景,都是使用離屏 Canvas進行繪製的,如首頁網格背景、關卡列表、排名列表等。在微信內 wx.createCanvas() 首次調用創建的是顯示在屏幕上的畫布,之後調用創建的都是離屏畫布。初始化時將靜態場景繪製完備,需要時直接拷貝離屏Canvas的圖像即可。Canvas 繪製本身就是不斷的更新幀從而達到動畫的效果,通過使用離屏 Canvas,就大大減少了一些靜態內容在上屏Canvas的繪製,從而提升了繪製性能。
內存優化
玩家在遊戲過程中拖動方塊的移動其實就是不斷更新多邊形圖形的坐標信息,然後不斷的清空畫布再重新繪製,可以想像,這個繪製是非常頻繁的,按照普通的做法就需要不斷去創建多個新的 Block 對象。針對遊戲中需要頻繁更新的對象,我們可以通過使用對象池的方法進行優化,對象池維護一個裝著空閒對象的池子,如果需要對象的時候,不是直接new,而是從對象池中取出,如果對象池中沒有空閒對象,則新建一個空閒對象,層疊拼圖Plus小遊戲內使用的是官方demo內已經實現的對象池類,實現如下:
垃圾回收
小遊戲中,JavaScript 中的每一個 Canvas 或 Image 對象都會有一個客戶端層的實際紋理儲存,實際紋理儲存中存放著 Canvas、Image 的真實紋理,通常會佔用相當一部分內存。
每個客戶端實際紋理儲存的回收時機依賴於 JavaScript 中的 Canvas、Image 對象回收。在 JavaScript 的 Canvas、Image 對象被回收之前,客戶端對應的實際紋理儲存不會被回收。通過調用 wx.triggerGC() 方法,可以加快觸發 JavaScriptCore Garbage Collection(垃圾回收),從而觸發 JavaScript 中沒有引用的 Canvas、Image 回收,釋放對應的實際紋理儲存。
但 GC 具體觸發時機還要取決於 JavaScriptCore 自身機制,並不能保證調用 wx.triggerGC() 能馬上觸發回收,層疊拼圖Plus小遊戲在每局遊戲開始或結束都會觸發一下,及時回收內存垃圾,以保證最良好的遊戲體驗。
多線程 Worker
對於遊戲來說,每幀 16ms 是極其寶貴的,如果有一些可以異步處理的任務,可以放置於 Worker 中運行,待運行結束後,再把結果返回到主線程。Worker 運行於一個單獨的全局上下文與線程中,不能直接調用主線程的方法,Worker 也不具備渲染的能力。Worker與主線程之間的數據傳輸,雙方使用 Worker.postMessage() 來發送數據,Worker.onMessage() 來接收數據,傳輸的數據並不是直接共享,而是被複製的。
需要注意的是:Worker 最大並發數量限制為 1 個,創建下一個前請用 Worker.terminate() 結束當前 Worker
其他 Worker 相關的內容請參考微信官方文檔:多線程 Worker
結語
短短的一篇文章,定不能將層疊拼圖Plus小遊戲的前前後後講明白講透徹。其實最讓人心累的還是軟著的申請過程,由於各種原因前前後後花了將近三個月的時間,後續可以給大家分享軟著申請相關的內容,希望可以幫助到需要的童鞋。
江湖不遠,我們遊戲裡見!
作者簡介:huangjianke,高級iOS開發/前端開發工程師,五年開發經驗。
需要體驗小遊戲的童鞋可在微信小程序搜索層疊拼圖Plus。