啓邏輯之高妙,因想象而自由javascript
層疊拼圖Plus是一款須要空間想象力和邏輯推理能力完美結合的微信小遊戲,偶消奇不消,在簡單的遊戲規則下卻有着無數種可能性,須要你充分發揮想象力去探索,看似簡單卻具備極大的挑戰性和趣味性,這就是其魅力所在!html
預覽:java
Talk is cheap. Show me the codegit
層疊拼圖Plus微信小遊戲採用js
+canvas
實現,沒有使用任何遊戲引擎,對於初學者來講,也比較容易入門。接下來,我將經過如下幾個點按部就班的講解層疊拼圖Plus微信小遊戲的實現。github
canvas 繪圖時,會從兩個物理像素的中間位置開始繪製並向兩邊擴散 0.5 個物理像素。當設備像素比爲 1 時,一個 1px 的線條實際上佔據了兩個物理像素(每一個像素實際上只佔一半),因爲不存在 0.5 個像素,因此這兩個像素原本不該該被繪製的部分也被繪製了,因而 1 物理像素的線條變成了 2 物理像素,視覺上就形成了模糊json
繪圖模糊的緣由知道了,在微信小遊戲裏面又該如何解決呢?canvas
const ratio = wx.getSystemInfoSync().pixelRatio
let ctx = canvas.getContext('2d')
canvas.width = screenWidth * ratio
canvas.height = screenHeight * ratio
ctx.fillStyle = 'black'
ctx.font = `${18 * ratio}px Arial`
ctx.fillText('我是清晰的文字', x * ratio, y * ratio)
ctx.fillStyle = 'red'
ctx.fillRect(x * ratio, y * ratio, width * ratio, height * ratio)
複製代碼
能夠看到,咱們先經過 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}
。
繪製多邊形代碼實現以下:
/** * 繪製多邊形 */
export default class Block {
constructor() { }
init(points, itemWidth, ctx) {
this.points = []
this.itemWidth = itemWidth // 單位長度
this.ctx = ctx
for (let i = 0; i < points.length; i++) {
let point = points[i]
this.points.push({
x: point.x * this.itemWidth,
y: point.y * this.itemWidth
})
}
}
draw() {
this.ctx.globalCompositeOperation = 'xor'
this.ctx.fillStyle = 'black'
this.ctx.beginPath()
this.ctx.moveTo(this.points[0].x, this.points[0].y)
for (let i = 1; i < this.points.length; i++) {
let point = this.points[i]
this.ctx.lineTo(point.x, point.y)
}
this.ctx.closePath()
this.ctx.fill()
}
}
複製代碼
使用:
let points = [
[{ x: 4, y: 5 }, { x: 8, y: 9 }, { x: 4, y: 9 }],
[{ x: 10, y: 8 }, { x: 10, y: 12 }, { x: 6, y: 12 }],
[{ x: 7, y: 4 }, { x: 11, y: 4 }, { x: 11, y: 8 }]
]
points.map((sub_points) => {
let block = new Block()
block.init(sub_points, this.itemWidth, this.ctx)
block.draw()
})
複製代碼
效果以下圖:
CanvasRenderingContext2D
其餘使用方法可參考:CanvasRenderingContext2D API 列表
1 + 1 = 0,是層疊拼圖Plus小遊戲玩法的精髓所在。
有經驗的同窗,也許一眼就發現了,1 + 1 = 0
恰好符合經過 異或運算
得出的結果。固然,細心的同窗也可能已經發現,在 如何繪製任意多邊形圖形
這一章節內,有一句特殊的代碼:this.ctx.globalCompositeOperation = 'xor'
,也正是經過設置 CanvasContext
的 globalCompositeOperation
屬性值爲 xor
便實現了「偶消奇不消」的神奇效果。
globalCompositeOperation
是指 在繪製新形狀時應用的合成操做的類型
,其餘效果可參考:globalCompositeOperation 示例
當迴轉數爲 0 時,點在閉合曲線外部。
講到這裏,咱們已經知道如何在Canvas
畫布內繪製出偶消奇不消效果的層疊圖形了,接下來咱們來看下玩家如何移動選中的圖形。咱們發現繪製出的圖形對象並無提供點擊事件綁定之類的操做,那又如何判斷玩家選中了哪一個圖形呢?這裏咱們就須要去實現如何判斷玩家觸摸事件的x
,y
座標在哪一個多邊形圖形內部區域,從而判斷出玩家選中的是哪個多邊形圖形。
判斷一個點是否在任意多邊形內部有多種方法,好比:
在層疊拼圖Plus小遊戲內,採用的是 迴轉數
法來判斷玩家觸摸點是否在多邊形內部。迴轉數
是拓撲學中的一個基本概念,具備很重要的性質和用途。固然,展開討論 迴轉數
的概念並不在該文的討論範圍內,咱們僅需瞭解一個概念:當迴轉數爲 0 時,點在閉合曲線外部。
上面面這張圖動態演示了迴轉數的概念:圖中紅色曲線關於點(人所在位置)的迴轉數爲 2
。
對於給定的點和多邊形,迴轉數應該怎麼計算呢?
最後根據角度累加值計算迴轉數。360°(2π)至關於一次迴轉。
在使用 JavaScript
實現時,須要注意如下問題:
JavaScript
的數只有 64
位雙精度浮點這一種。對於三角函數產生的無理數,浮點數計算不可避免會形成一些偏差,所以在最後計算迴轉數須要作取整操做。JavaScript
三角函數 Math.atan2()
返回值的範圍。但 JavaScript
並不能直接計算任意兩條線的夾角,咱們只能先計算兩條線與 x
正軸夾角,再取二者差值。這個差值的結果就有可能超出 -π
到 π
這個區間,所以咱們還須要處理差值超出取值區間的狀況。代碼實現:
/** * 判斷點是否在多邊形內/邊上 */
isPointInPolygon(p, poly) {
let px = p.x,
py = p.y,
sum = 0
for (let i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
let sx = poly[i].x,
sy = poly[i].y,
tx = poly[j].x,
ty = poly[j].y
// 點與多邊形頂點重合或在多邊形的邊上
if ((sx - px) * (px - tx) >= 0 &&
(sy - py) * (py - ty) >= 0 &&
(px - sx) * (ty - sy) === (py - sy) * (tx - sx)) {
return true
}
// 點與相鄰頂點連線的夾角
let angle = Math.atan2(sy - py, sx - px) - Math.atan2(ty - py, tx - px)
// 確保夾角不超出取值範圍(-π 到 π)
if (angle >= Math.PI) {
angle = angle - Math.PI * 2
} else if (angle <= -Math.PI) {
angle = angle + Math.PI * 2
}
sum += angle
}
// 計算迴轉數並判斷點和多邊形的幾何關係
return Math.round(sum / Math.PI) === 0 ? false : true
}
複製代碼
注:該章節內容圖片均來自網絡,若有侵權,請告知刪除。另外有興趣的同窗可使用其餘方法來實現判斷一個點是否在任意多邊形內部。
探索的過程當然精彩,而結果卻更令咱們期待
經過前面的介紹咱們能夠知道,判斷遊戲結果是否正確其實就是比對玩家組合圖形的 xor
結果與目標圖形的 xor
結果。那麼如何求多個多邊形 xor
的結果呢? polygon-clipping 正是爲此而生的。它不只支持 xor
操做,還有其餘的好比:union
, intersection
, difference
等操做。 在層疊拼圖Plus遊戲內經過 polygon-clipping 又是怎樣實現遊戲結果判斷的呢?
多邊形平面座標點集合:
points = [
[{ x: 6, y: 6 }, { x: 10, y: 6 }, { x: 10, y: 10 }, { x: 6, y: 10 }],
[{ x: 8, y: 6 }, { x: 10, y: 8 }, { x: 8, y: 10 }, { x: 6, y: 8 }]
]
複製代碼
/** * 獲取 多個多邊形 xor 結果 */
const polygonClipping = require('polygon-clipping')
polygonXor(points) {
let poly = []
points.forEach(function (sub_points) {
let temp = []
sub_points.forEach(function (point) {
temp.push([point.x, point.y])
})
poly.push([temp])
})
let results = polygonClipping.xor(...poly)
// 找出左上角的點
let min_x = 100, min_y = 100
results.forEach(function (sub_results) {
sub_results.forEach(function (temps) {
temps.forEach(function (point) {
if (point[0] < min_x) min_x = point[0]
if (point[1] < min_y) min_y = point[1]
})
})
})
// 以左上角爲參考點 多邊形平移至 原點 {x: 0, y: 0}
results.forEach(function (sub_results) {
sub_results.forEach(function (temps) {
temps.forEach(function (point) {
point[0] -= min_x
point[1] -= min_y
})
})
})
}
複製代碼
let result = this.polygonXor(points)
複製代碼
xor
結果:
[
[[[0, 0], [2, 0], [0, 2], [0, 0]]],
[[[0, 2], [2, 4], [0, 4], [0, 2]]],
[[[2, 0], [4, 0], [4, 2], [2, 0]]],
[[[2, 4], [4, 2], [4, 4], [2, 4]]]
]
複製代碼
同理計算出玩家操做圖形的xor
結果進行比對便可得出答案正確與否。
須要注意的是,獲取玩家的 xor
結果並不能直接拿來與目標圖形xor
結果進行比較,咱們須要將xor
的結果以左上角爲參考點將圖形平移至原點內,而後再進行比較,若是結果一致,則表明玩家答案正確。
有人的地方就有江湖,有江湖的地方就有排行
在看本章節內容以前,建議先瀏覽一遍排行榜相關的官方文檔:好友排行榜、關係鏈數據,以便對相關內容有個大概的瞭解。
開放數據域
是一個封閉、獨立的 JavaScript
做用域。要讓代碼運行在開放數據域,須要在 game.json
中添加配置項 openDataContext
指定開放數據域的代碼目錄。添加該配置項表示小遊戲啓用了開放數據域,這將會致使一些限制。
// game.json
{
"openDataContext": "src/myOpenDataContext"
}
複製代碼
在遊戲內使用 wx.setUserCloudStorage(obj)
對玩家遊戲數據進行託管。
在開放數據域內使用 wx.getFriendCloudStorage(obj)
拉取當前用戶全部同玩好友的託管數據
展現關係鏈數據
若是想要展現經過關係鏈 API
獲取到的用戶數據,如繪製排行榜等業務場景,須要將排行榜繪製到 sharedCanvas
上,再在主域將 sharedCanvas
渲染上屏。
// src/myOpenDataContext/index.js
let sharedCanvas = wx.getSharedCanvas()
function drawRankList (data) {
data.forEach((item, index) => {
// ...
})
}
wx.getFriendCloudStorage({
success: res => {
let data = res.data
drawRankList(data)
}
})
複製代碼
sharedCanvas
是主域和開放數據域均可以訪問的一個離屏畫布。在開放數據域調用 wx.getSharedCanvas()
將返回 sharedCanvas
。
// src/myOpenDataContext/index.js
let sharedCanvas = wx.getSharedCanvas()
let context = sharedCanvas.getContext('2d')
context.fillStyle = 'red'
context.fillRect(0, 0, 100, 100)
複製代碼
在主域中能夠經過開放數據域實例訪問 sharedCanvas
,經過 drawImage()
方法能夠將 sharedCanvas
繪製到上屏畫布。
// game.js
let openDataContext = wx.getOpenDataContext()
let sharedCanvas = openDataContext.canvas
let canvas = wx.createCanvas()
let context = canvas.getContext('2d')
context.drawImage(sharedCanvas, 0, 0)
複製代碼
sharedCanvas
本質上也是一個離屏 Canvas
,而重設 Canvas
的寬高會清空 Canvas
上的內容。因此要通知開放數據域去重繪 sharedCanvas
。
// game.js
openDataContext.postMessage({
command: 'render'
})
// src/myOpenDataContext/index.js
openDataContext.onMessage(data => {
if (data.command === 'render') {
// 重繪 sharedCanvas
}
})
複製代碼
須要注意的是:sharedCanvas
的寬高只能在主域設置,不能在開放數據域中設置。
性能優化,簡而言之,就是在不影響系統運行正確性的前提下,使之運行地更快,完成特定功能所需的時間更短。
一款能讓人心情愉悅的遊戲,性能問題必然不能成爲絆腳石。那麼能夠從哪些方面對遊戲進行性能優化呢?
Canvas
在層疊拼圖Plus小遊戲內,針對須要大量使用且繪圖繁複的靜態場景,都是使用離屏 Canvas
進行繪製的,如首頁網格背景、關卡列表、排名列表等。在微信內 wx.createCanvas()
首次調用建立的是顯示在屏幕上的畫布,以後調用建立的都是離屏畫布。初始化時將靜態場景繪製完備,須要時直接拷貝離屏Canvas
的圖像便可。Canvas
繪製自己就是不斷的更新幀從而達到動畫的效果,經過使用離屏 Canvas
,就大大減小了一些靜態內容在上屏Canvas
的繪製,從而提高了繪製性能。
this.offScreenCanvas = wx.createCanvas()
this.offScreenCanvas.width = this.width * ratio
this.offScreenCanvas.height = this.height * ratio
this.ctx.drawImage(this.offScreenCanvas, x * ratio, y * ratio, this.offScreenCanvas.width, this.offScreenCanvas.height)
複製代碼
玩家在遊戲過程當中拖動方塊的移動其實就是不斷更新多邊形圖形的座標信息,而後不斷的清空畫布再從新繪製,能夠想象,這個繪製是很是頻繁的,按照普通的作法就須要不斷去建立多個新的 Block
對象。針對遊戲中須要頻繁更新的對象,咱們能夠經過使用對象池
的方法進行優化,對象池維護一個裝着空閒對象的池子,若是須要對象的時候,不是直接new
,而是從對象池中取出,若是對象池中沒有空閒對象,則新建一個空閒對象,層疊拼圖Plus小遊戲內使用的是官方demo
內已經實現的對象池
類,實現以下:
const __ = {
poolDic: Symbol('poolDic')
}
/** * 簡易的對象池實現 * 用於對象的存貯和重複使用 * 能夠有效減小對象建立開銷和避免頻繁的垃圾回收 * 提升遊戲性能 */
export default class Pool {
constructor() {
this[__.poolDic] = {}
}
/** * 根據對象標識符 * 獲取對應的對象池 */
getPoolBySign(name) {
return this[__.poolDic][name] || ( this[__.poolDic][name] = [] )
}
/** * 根據傳入的對象標識符,查詢對象池 * 對象池爲空建立新的類,不然從對象池中取 */
getItemByClass(name, className) {
let pool = this.getPoolBySign(name)
let result = ( pool.length
? pool.shift()
: new className() )
return result
}
/** * 將對象回收到對象池 * 方便後續繼續使用 */
recover(name, instance) {
this.getPoolBySign(name).push(instance)
}
}
複製代碼
小遊戲中,JavaScript
中的每個 Canvas
或 Image
對象都會有一個客戶端層的實際紋理儲存,實際紋理儲存中存放着 Canvas
、Image
的真實紋理,一般會佔用至關一部份內存。
每一個客戶端實際紋理儲存的回收時機依賴於 JavaScript
中的 Canvas
、Image
對象回收。在 JavaScript
的 Canvas
、Image
對象被回收以前,客戶端對應的實際紋理儲存不會被回收。經過調用 wx.triggerGC()
方法,能夠加快觸發 JavaScriptCore Garbage Collection
(垃圾回收),從而觸發 JavaScript
中沒有引用的 Canvas
、Image
回收,釋放對應的實際紋理儲存。
但 GC
具體觸發時機還要取決於 JavaScriptCore
自身機制,並不能保證調用 wx.triggerGC()
能立刻觸發回收,層疊拼圖Plus小遊戲在每局遊戲開始或結束都會觸發一下,及時回收內存垃圾,以保證最良好的遊戲體驗。
對於遊戲來講,每幀 16ms
是極其寶貴的,若是有一些能夠異步處理的任務,能夠放置於 Worker
中運行,待運行結束後,再把結果返回到主線程。Worker
運行於一個單獨的全局上下文與線程中,不能直接調用主線程的方法,Worker
也不具有渲染的能力。 Worker
與主線程之間的數據傳輸,雙方使用 Worker.postMessage()
來發送數據,Worker.onMessage()
來接收數據,傳輸的數據並非直接共享,而是被複制的。
// game.json
{
"workers": "workers"
}
// 建立worker線程
let worker = worker = wx.createWorker('workers/request/index.js') // 文件名指定 worker 的入口文件路徑,絕對路徑
// 主線程向 Worker 發送消息
worker.postMessage({
msg: 'hello worker'
})
// 主線程監聽 Worker 返回消息
worker.onMessage(function (res) {
console.log(res)
})
複製代碼
須要注意的是:Worker
最大併發數量限制爲 1
個,建立下一個前請用 Worker.terminate()
結束當前 Worker
其餘 Worker
相關的內容請參考微信官方文檔:多線程 Worker
短短的一篇文章,定不能將層疊拼圖Plus小遊戲的前先後後講明白講透徹,加上文筆有限,有描述不當的地方還望多多海涵。其實最讓人心累的仍是軟著的申請過程,因爲各類緣由前先後後花了將近三個月的時間,原本也想寫一下軟著申請相關的內容,最後發現篇幅有點長,無奈做罷,爭取後面花點時間整理一下我這邊的經驗,但願能夠幫助到須要的童鞋。
因爲項目結構以及代碼還比較混亂,我的以爲,目前暫時還不適合開源。好在,小遊戲內的全部核心代碼以及遊戲實現思想均已呈上,有興趣的同窗若是有相關方面的疑問也能夠與我多多交流,你們互相學習,共同進步。
江湖不遠,咱們遊戲裏見!