您好,登錄后才能下訂單哦!
這篇文章主要講解了“基于JS怎么實(shí)現(xiàn)消消樂游戲”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“基于JS怎么實(shí)現(xiàn)消消樂游戲”吧!
首先我們思考游戲的機(jī)制: 游戲有一個“棋盤”,是一個n*m的矩形。矩形中有若干個顏色(或者類型)的方塊,相同類型的方塊,在一個橫行或者豎行,有3個或者3個以上時,便會消除。
在部分方塊消除后,這些方塊上方的方塊便會下墜并補(bǔ)充這些消除方塊的缺口,同時,上方又會生成新的方塊來補(bǔ)充下墜方塊的位置,在執(zhí)行完上述步驟后,便完成了一個游戲過程的循環(huán)。
一共有3個步驟,將生成一個游戲循環(huán):消除,下墜,補(bǔ)充。在補(bǔ)充后,如果方塊們無法自然消除,循環(huán)便會結(jié)束。這時候就需要玩家來交還兩個相鄰方塊,來人為制造可以消除的情況,以重新進(jìn)入循環(huán)。
如果玩家的交換并不能使得重新進(jìn)入消除循環(huán)呢?那么這個交換將重新?lián)Q回原樣。
基本機(jī)制思考完畢,現(xiàn)在開始代碼構(gòu)建:
首先考慮到方塊們會進(jìn)行大量的動畫過程(主要是四種:移動,消除,下墜,冒出),于是我們使用絕對定位來安排這些方塊,并且在其行類樣式當(dāng)中添加屬性:transition,用css來實(shí)現(xiàn)這些方塊的動畫。具體實(shí)現(xiàn)如下:
移動:通過left和top值的改變,控制方塊的移動。
消除:通過修改transform,修改為scale(0),以實(shí)現(xiàn)消除的動畫。
下墜:通過top值的改變,同移動。
冒出:通過修改transform,將本來為scale(0)的transform值修改為scale(1),以實(shí)現(xiàn)冒出的動畫。
考慮到這些動畫是一個接一個的執(zhí)行,我們應(yīng)該是需要使用異步來執(zhí)行這些動畫的,當(dāng)然使用回調(diào)函數(shù)也能實(shí)現(xiàn),但回調(diào)函數(shù)可能會很麻煩,所以我們使用Promise對象來解決這些麻煩。
廢話太多了!現(xiàn)在開始寫代碼。
首先是棋盤的實(shí)現(xiàn),簡單操作就定義了一個棋盤的整體結(jié)構(gòu)出來。當(dāng)然,給#app添加position:relative或者position:absolute是必不可少的。
<body> <div id="app"> </div> </body>
接下來我們用面向?qū)ο蟮乃枷?,?gòu)造棋盤的具體內(nèi)容:
首先一個棋盤有它的寬度和高度(x和y),我們還同時還定義它的方塊大小(size)。
matrix則為之后要用到的,存放不同type的矩陣,types則為所有的棋子種類。
除此之外,還有幾個屬性,這些之后再說。
class GameMap { constructor(x, y, size) { this.x = x; this.y = y; this.size = size; this.matrix = []; this.useSwap = false; this.handleable = true; this.types = emojis.length; } }
我們再來構(gòu)造“棋子”,棋子的屬性很多,所以我們通過僅將options作為參數(shù),并將options解構(gòu),來賦予棋子這些屬性,這些屬性分別是
class Cell { constructor(options) { const { position, status, type, left, top, right, bottom, instance } = options; this.type = type; this.position = position; this.status = status; this.top = top; this.bottom = bottom; this.left = left; this.right = right; this.instance = instance; } }
type 類型(顏色),number類型表示,相同的number即被視為同樣的類型。
position 位置,用一個形如[m,n]的二維數(shù)組存儲
status 狀態(tài),分為'common' 普通 'collapse' 崩塌 'emerge' 冒出,一共三種
top 棋子上方的棋子對象,值也是一個Cell實(shí)例,如果棋子上方?jīng)]有棋子,那它就是undefined
left 棋子的左側(cè)棋子對象
right 棋子的右側(cè)棋子對象
bottom 棋子的下方棋子對象
instance 根據(jù)上述屬性刻畫出的真實(shí)的棋子的DOM對象,最終這些對象會在GameMap中展現(xiàn)出來
在這里我們使用emoji表情來展現(xiàn)這些棋子,我們定義全局變量emojis:
const emojis = ['????', '????', '????', '????', '????'];
有了棋盤和棋子,我們就能渲染棋盤了。
首先我們定義全局變量cells,用以存放棋盤中所有的棋子,所有的Cell類。
然后我們在GameMap中定義方法genMatrix(),初始化棋盤。根據(jù)棋盤寬度和高度(x和y)的配置,我們填好了一個x*y的點(diǎn)陣,不過現(xiàn)在這里面還沒有內(nèi)容。
genMatrix() { const { x, y } = this; const row = new Array(x).fill(undefined); const matrix = new Array(y).fill(undefined).map(item => row); this.matrix = matrix; return this; }
接下來的工作是用隨機(jī)數(shù)填滿點(diǎn)陣,我們定義方法genRandom()。
genRandom() { const { x, y } = this; this.matrix = this.matrix.map(row => row.map(item => Math.floor(Math.random() * this.types))); return this; }
如圖所示的一個點(diǎn)陣就此生成。我們再用這些點(diǎn)陣渲染出真實(shí)的畫面。
定義方法init()
目前來看,init()的寫法有些晦澀難懂,這也很正常,之后我們還會提到它。
init() { cells = []; const { x, y } = this; for (let i = 0; i < y; i++) { for (let j = 0; j < x; j++) { const type = this.matrix[i][j]; const random = Math.floor(Math.random() * this.types); cells.push(new Cell({ type: (type == undefined) ? random : type, position: [j, i], status: (type == undefined) ? 'emerge' : 'common', left: undefined, top: undefined, right: undefined, bottom: undefined, instance: undefined })); } } cells.forEach(cell => { const [row, col] = cell.position; cell.left = cells.find(_cell => { const [_row, _col] = _cell.position; return (_row == row - 1) && (_col == col); }); cell.right = cells.find(_cell => { const [_row, _col] = _cell.position; return (_row == row + 1) && (_col == col); }); cell.top = cells.find(_cell => { const [_row, _col] = _cell.position; return (_row == row) && (_col == col - 1); }); cell.bottom = cells.find(_cell => { const [_row, _col] = _cell.position; return (_row == row) && (_col == col + 1); }); cell.genCell(); }); return this; }
之前定義的全局變量cells,就用以存放所有的Cell類的實(shí)例。
對于一個新鮮生成的棋盤來說,所有的Cell的狀態(tài)(status)都是common,其他情況我們之后會講到。回到Cell類的構(gòu)造過程,我們發(fā)現(xiàn)這一步完成了Cell實(shí)例的塑造,這些棋子將被進(jìn)一步的加工為最后的游戲畫面,而最后一步cell.genCell()則最終把這些抽象的類實(shí)體化。
genCell()方法是在Cell類中我們定義的方法,我們根據(jù)
genCell() { const cell = document.createElement('div'); const size = gameMap.size; const [x, y] = this.position; cell.type = this.type; cell.style.cssText = ` width:${size}px; height:${size}px; left:${size * x}px; top:${size * y}px; box-sizing:border-box; border:5px solid transparent; transition:0.5s; position:absolute; transform:scale(${this.status == 'emerge' ? '0' : '1'}); display:flex; justify-content:center; align-items:center `; cell.innerHTML = `<span >${emojis[this.type]}</span>`; this.instance = cell; }
genCell根據(jù)init()之前對cell數(shù)據(jù)的定義和演算,生成了棋子,但目前,棋子尚未渲染到頁面中,只是作為DOM對象暫存。
最后我們定義方法genCellMap()生成真實(shí)的游戲畫面。
genCellMap() { app.innerHTML = ''; cells.forEach(cell => { app.append(cell.instance); }); return this; }
遍歷之前全局變量cells中的內(nèi)容,找到每個Cell中的instance,再將這些instance掛載到#app上,一個游戲棋盤就躍然而出了。
這就是一個Cell實(shí)例的模樣,其中的instance是一個實(shí)實(shí)在在的div。
游戲畫面則是一個最簡單的棋盤,沒有做其他的美化修飾(其實(shí)是因?yàn)閼械脤懥耍R驗(yàn)榭紤]到emoji不像圖片加載這么麻煩,同時,也不存在失真的情況,所以我們使用emoji來刻畫這些棋子。
一個真實(shí)的instance的樣子。
之前我們已經(jīng)明確了游戲的一個循環(huán)過程發(fā)送的三件事情消除,下墜,補(bǔ)充,所以說我們把這三件事情分別通過定義GameMap的三個方法來刻畫,這三個方法分別為 :
genCollapse() genDownfall() genEmerge()
代碼如下
genCollapse() { return new Promise((resolve, reject) => { this.handleable = false; this.markCollapseCells(); setTimeout(() => { cells.forEach(cell => { if (cell.status == 'collapse') { cell.instance.style.transform = 'scale(0)'; } }); }, 0); setTimeout(() => { resolve('ok'); }, 500); }); }
genCollapse的過程中還有一個步驟叫做markCollapseCells(),用以標(biāo)記將會崩塌的棋子,該方法代碼如下:
markCollapseCells() { cells.forEach((cell) => { const { left, right, top, bottom, type } = cell; if (left?.type == type && right?.type == type) { left.status = "collapse"; cell.status = "collapse"; right.status = "collapse"; } if (top?.type == type && bottom?.type == type) { top.status = "collapse"; cell.status = "collapse"; bottom.status = "collapse"; } }); return this; }
遍歷整個cells,如果一個棋子的左邊右邊和自己都為同一個類型,那他們仨的狀態(tài)都會被標(biāo)記為'collapse',同理,如果棋子的上面下面和自己為同一個類型,也會被標(biāo)記。我們不害怕重復(fù)的情況,因?yàn)橐呀?jīng)被標(biāo)記的棋子,再被標(biāo)記一次也無所謂。
標(biāo)記完成后,我們便將這些被標(biāo)記的Cell對象,他們的instance的style加入一項(xiàng)transform:scale(0),在transition的作用下,它們會逐步(實(shí)際上很快)萎縮為看不見的狀態(tài),實(shí)際上它們并沒有因此消失。而在這一個逐步萎縮的過程,我們通過Promise的性質(zhì),來阻塞該方法的執(zhí)行,以等待棋子萎縮完畢,再進(jìn)入下一個過程:下墜。0.5s后拋出resolve,完成放行,順利進(jìn)入到下一個步驟。
genDownfall() { return new Promise((resolve, reject) => { setTimeout(() => { cells.forEach(cell => { if (cell.status != 'collapse') { let downfallRange = 0; let bottom = cell.bottom; while (bottom) { if (bottom.status == 'collapse') { downfallRange += 1; } bottom = bottom.bottom; } cell.instance.style.top = (parseInt(cell.instance.style.top) + gameMap.size * downfallRange) + 'px'; } }); }, 0); setTimeout(() => { resolve('ok'); }, 500); }); }
genDownfall()的關(guān)鍵是我們需要得知,哪些棋子會下墜,這些棋子應(yīng)該下墜多少距離。我們得知下墜距離后,再設(shè)置這些Cell中新的top值,同樣是通過transition的效果,來制造下墜的動畫。
首先明確什么棋子可能會下墜:
其實(shí)很簡單,除開status為collapse的棋子都可能會下墜,但也不見得,比如最下方一排的棋子無論如何也不會下墜。
所以下一步就是計(jì)算它們下墜的距離:
這一步也不復(fù)雜,之前的Cell類中我們已經(jīng)事先定義了棋子的bottom屬性,我們只需要知道棋子下方有多少個狀態(tài)為collapse的棋子,我們就知道該棋子會下墜多少距離了,距離=棋子的size*下方狀態(tài)為collapse的棋子數(shù)量。
通過while大法,逐一查詢該棋子下方棋子的狀態(tài),便能得到答案。
我們故伎重施,使用Promise的性質(zhì)阻塞整個過程0.5s,再放行到下一步。
genEmerge()的過程相比之下要復(fù)雜很多,因?yàn)榇藭r棋盤已經(jīng)被打亂,需要重塑整個棋盤,重塑完畢后,再將缺失的棋盤補(bǔ)充出來。補(bǔ)充出來的方法之前也提到了,就是scale(0)->scale(1),以得到一種勃勃生機(jī),萬物競發(fā)的效果。
代碼如下:
genEmerge() { return new Promise((resolve, reject) => { this.regenCellMap(); this.genCellMap(); setTimeout(() => { cells.forEach(cell => { if (cell.status == 'emerge') { cell.instance.style.transform = 'scale(1)'; } }); }, 0); setTimeout(() => { resolve('ok'); }, 500); }); }
其中有一個步驟叫做regenCellMap(),該步驟代碼如下
regenCellMap() { const size = gameMap.size; const findInstance = (x, y) => { return cells.find(item => { const { offsetLeft, offsetTop } = item.instance; return (item.status != 'collapse' && (x == offsetLeft / size) && (y == offsetTop / size)); })?.instance; }; this.genMatrix(); this.matrix = this.matrix.map((row, rowIndex) => row.map((item, itemIndex) => findInstance(itemIndex, rowIndex)?.type)); this.init(); }
這其中關(guān)鍵的一步就是findInstance,我們要重新找到執(zhí)行了downfall后的棋子它們的position是什么,并將它們的type和位移后的position一一對應(yīng),我們用這些重新一一對應(yīng)的信息,重新構(gòu)造matrix,以完成對整個棋盤的重新塑造。注意該方法的最后一步,init(),也就是說我們對棋盤重新進(jìn)行了初始化,下面我們再來看init()的代碼,或許你就能理解init()為什么這么寫了。
重塑后的matrix長這樣。
下墜的棋子填了消除棋子的坑,那上面的空隙也就出現(xiàn)了,數(shù)組的find方法,找不到內(nèi)容,便會返回undefined,因此findInstance()在找不到棋子的時候,便會返回undefined,也因此將這一結(jié)果作用到了重塑的matrix上。
我們重新來看init()
init() { cells = []; const { x, y } = this; for (let i = 0; i < y; i++) { for (let j = 0; j < x; j++) { const type = this.matrix[i][j]; const random = Math.floor(Math.random() * this.types); cells.push(new Cell({ type: (type == undefined) ? random : type, position: [j, i], status: (type == undefined) ? 'emerge' : 'common', left: undefined, top: undefined, right: undefined, bottom: undefined, instance: undefined })); } } cells.forEach(cell => { const [row, col] = cell.position; cell.left = cells.find(_cell => { const [_row, _col] = _cell.position; return (_row == row - 1) && (_col == col); }); cell.right = cells.find(_cell => { const [_row, _col] = _cell.position; return (_row == row + 1) && (_col == col); }); cell.top = cells.find(_cell => { const [_row, _col] = _cell.position; return (_row == row) && (_col == col - 1); }); cell.bottom = cells.find(_cell => { const [_row, _col] = _cell.position; return (_row == row) && (_col == col + 1); }); cell.genCell(); }); return this; }
注意這兩句代碼:
type: (type == undefined) ? random : type
status: (type == undefined) ? 'emerge' : 'common'
重塑后的棋盤,空隙的部位得以補(bǔ)充:首先它們的類型為隨機(jī)生成,它們的狀態(tài)也有別于common,而是emerge,在genEmerge()過程中它們將會出現(xiàn)。
最復(fù)雜的工作目前已經(jīng)完成,我們已經(jīng)刻畫出了整個游戲的核心循環(huán),現(xiàn)在我們再用genLoop方法來整合這一系列過程,代碼如下:
async genLoop() { await gameMap.genCollapse(); let status = cells.some(cell => cell.status == 'collapse'); while (cells.some(cell => cell.status == 'collapse')) { await gameMap.genDownfall(); await gameMap.genEmerge(); await gameMap.genCollapse(); } gameMap.handleable = true; return status; }
考慮到我們使用了Promise,所以我們將genLoop弄成異步函數(shù)。while循環(huán)將循環(huán)往復(fù)的執(zhí)行這些過程,直到我們無法將任何的棋子狀態(tài)標(biāo)記為collapse。之前我們有個變量沒提到,就是handleable,它決定了我們是否可以對棋盤進(jìn)行互動。
最后我們考慮的是我們操縱棋盤的過程。實(shí)際上消消樂游戲中,需要我們交互的場景是很少的,大多數(shù)時間都是在觀看動畫,genSwap是我們交換兩個棋子的過程。
genSwap(firstCell, secondCell) { return new Promise((resolve, reject) => { const { instance: c1, type: t1 } = firstCell; const { instance: c2, type: t2 } = secondCell; const { left: x1, top: y1 } = c1.style; const { left: x2, top: y2 } = c2.style; setTimeout(() => { c1.style.left = x2; c1.style.top = y2; c2.style.left = x1; c2.style.top = y1; }, 0); setTimeout(() => { firstCell.instance = c2; firstCell.type = t2; secondCell.instance = c1; secondCell.type = t1; resolve('ok'); }, 500); }); }
套路還是很明確的,先改變兩枚棋子的left和top值,獲得動畫效果,0.5s后再重構(gòu)Cell對象。
最后是游戲的執(zhí)行代碼:
const app = document.getElementById('app'); const btn = document.getElementById('btn'); const emojis = ['????', '????', '????', '????', '????']; let cells = []; let gameMap = new GameMap(6, 10, 50); gameMap.genMatrix().genRandom(); gameMap.init().genCellMap(); gameMap.genLoop(); let cell1 = null; let cell2 = null; app.onclick = () => { if (gameMap.handleable) { const target = event.target.parentNode; const { left: x, top: y } = target.style; const _cell = cells.find(item => item.instance == target); if (!gameMap.useSwap) { target.className = 'active'; cell1 = _cell; } else { cell2 = _cell; cell1.instance.className = ''; if (['left', 'top', 'bottom', 'right'].some(item => cell1[item] == cell2)) { (async () => { await gameMap.genSwap(cell1, cell2); let res = await gameMap.genLoop(); if (!res) { await gameMap.genSwap(cell1, cell2); } })(); } } gameMap.useSwap = !gameMap.useSwap; } };
實(shí)際上主要的工作是花在構(gòu)造GameMap和Cell兩個類上面,清楚的構(gòu)造了兩個類之后,剩下的工作就不那么復(fù)雜了。
這里的點(diǎn)擊事件稍微復(fù)雜一點(diǎn),因?yàn)閷?shí)際上消消樂當(dāng)中只有相鄰的兩個單元格才能交換,并且交換后還需與判斷是否有單元格會因此崩塌,如果沒有,這個交換將被重置。app.onclick當(dāng)中便刻畫了這個效果。
感謝各位的閱讀,以上就是“基于JS怎么實(shí)現(xiàn)消消樂游戲”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對基于JS怎么實(shí)現(xiàn)消消樂游戲這一問題有了更深刻的體會,具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。