2048游戏简单实现
前言
最近突然心血來潮想寫一個網頁小游戲,我看網上有很多人推薦寫2048來練練手,遂開始寫。目前為止,基本功能已經實現,只是沒有添加相應的動畫效果,待以后有機會補上(其實我就是動畫這塊太菜了 T_T)
前方長文預警!!!
游戲截圖
????
項目結構
這個項目結構挺簡單的,應該也都看得懂,在此僅對js文件夾進行描述,其余的就不再贅述啦
(main.js是入口文件,move.js主要就是一些移動的處理,support.js里是對移動塊背景顏色和文字顏色的處理,后面要添加的動畫效果也準備寫在這個文件里)
主要功能
1.游戲初始化:新建游戲4×4的16宮格畫布,隨機格子上生成2或者4兩個數字
2.格子的移動:先判斷能否移動,移動后判斷能否合并,合并后改變格子顏色和數字
3.新格子的生成:移動一次,就在剩余的空格子中隨機生成一個2或者4
4.判贏:16宮格中合并出了“2048”則為游戲勝利
5.判輸:16宮格中沒有剩余空格子且不能再向任何方向移動則為游戲失敗
分步代碼
一、HTML結構
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>2048</title><scriptsrc="http://code.jquery.com/jquery-3.3.1.min.js"></script><link rel="stylesheet" href="css/index.css"> </head> <body><header><div class="left"><span class="title">2048</span><p class="slogan_1">Play 2048 Game Online</p><p class="slogan_2">Join the numbers and get to the 2048 tile!</p></div><div class="right"><p class="score-box">score<br /> <span id="score">669</span></p><button type="button" id="new-game">New Game</button></div></header><div id="grid-con"><!-- 第一行 --><div class="grid-cell" id="grid-cell-0-0"></div><div class="grid-cell" id="grid-cell-0-1"></div><div class="grid-cell" id="grid-cell-0-2"></div><div class="grid-cell" id="grid-cell-0-3"></div><!-- 第二行 --><div class="grid-cell" id="grid-cell-1-0"></div><div class="grid-cell" id="grid-cell-1-1"></div><div class="grid-cell" id="grid-cell-1-2"></div><div class="grid-cell" id="grid-cell-1-3"></div><!-- 第三行 --><div class="grid-cell" id="grid-cell-2-0"></div><div class="grid-cell" id="grid-cell-2-1"></div><div class="grid-cell" id="grid-cell-2-2"></div><div class="grid-cell" id="grid-cell-2-3"></div><!-- 第四行 --><div class="grid-cell" id="grid-cell-3-0"></div><div class="grid-cell" id="grid-cell-3-1"></div><div class="grid-cell" id="grid-cell-3-2"></div><div class="grid-cell" id="grid-cell-3-3"></div></div><div class="mask"></div> <!-- 灰色遮罩層:游戲結束時隨gameover框一并出現 --><div id="game-over">Game Over!</div><div id="game-win">Congradulation!</div><script src="js/main.js"></script><script src="js/move.js"></script><script src="js/support.js"></script> </body> </html>HTML結構的主要思路為:利用網格布局將游戲畫布的16宮格繪制出來,通過class定制樣式,id定位到具體的格子以重繪格子樣式。
網格布局的詳細教程移步 ——>?CSS網格布局(Grid)完全教程
二、CSS樣式文件
* {margin: 0;padding: 0; } body {text-align: center;font-family:Arial, Helvetica, sans-serif;position: relative;background: #F2EAE3; } header {width: 500px;margin: 0 auto;display: flex;justify-content: space-between;margin-top: 30px; } header .left {display: flex;flex-direction: column;align-items: flex-start;justify-content: space-between; } header .left .slogan_1 {color: #776E65;font-weight: bold; } header .left .slogan_2 {color: #7F7265; } header span.title {color: #776E65;font-size: 70px;font-weight: bold; } header button {width: 100px;height: 50px;border: 0;background: #907B66;font-size: 14px;font-weight: bold;color: #fff;padding: 5px;border-radius: 3px;margin-top: 10px;outline: none;cursor: pointer; } header p.score-box {width: 100px;background: #BBADA0;color: #EEE4DA;padding: 10px 5px;margin-top: 10px;font-size: 18px;box-sizing: border-box;border-radius: 3px; } header p.score-box #score {color: #fff;font-size: 22px;letter-spacing: 2px;font-weight: bold; } #grid-con {width: 500px;height: 500px;background: #BCAEA1;border-radius: 10px;margin: 30px auto 0;padding: 15px;display: grid;grid-template-columns: repeat(4,1fr);grid-template-rows: repeat(4,1fr);grid-gap: 15px;grid-template-areas: "grid-cell-0-0 grid-cell-0-1 grid-cell-0-2 grid-cell-0-3""grid-cell-1-0 grid-cell-1-1 grid-cell-1-2 grid-cell-1-3""grid-cell-2-0 grid-cell-2-1 grid-cell-2-2 grid-cell-2-3""grid-cell-3-0 grid-cell-3-1 grid-cell-3-2 grid-cell-3-3"; } #grid-con .grid-cell {background: #CDC1B4;border-radius: 10px; } #grid-con .grid-cell.number-cell {text-align: center;background: #EEE4DA;border-radius: 10px;color: #776E65;font-size: 50px;font-weight: bold;display: flex;justify-content: center;align-items: center; } .mask {width: 530px;height: 530px;background: #F2EAE3;position: absolute;/* top: -30px; */top: 167px;left: calc(50vw - 265px);z-index: 1000;opacity: .6; } #game-over,#game-win {width: 300px;height: 100px;color: #fff;font-size: 30px;font-weight: bold;background: rgba(39,40,34,.6);border-radius: 5px;line-height: 100px;text-align: center;position: absolute;top: 370px;left: calc(50vw - 150px);z-index: 1001; } .hide {display: none; }三、JS邏輯代碼
1 主程序入口文件main.js:
思路:創建全局變量score、board分別用于保存游戲得分和16宮格數字信息,并在瀏覽器加載后立即初始化一局新游戲newGame()。開始游戲后用戶通過鍵盤的方向鍵控制格子的移動:
- 移動操作過程中需要先檢測該方向上是否能夠移動,能移動則16宮格中所有數字格子均向該方向移動,不能移動則按下方向鍵界面不產生響應;
- 移動后需檢測移動方向上相鄰兩個格子中的數字是否一樣,一樣則可以進行合并(合并后需再次向該方向移動,重繪16宮格數據),若不能合并則完成本次移動操作。
1.1 整個流程
1.1.1 首先,將需要用到的全局變量聲明在文件的開頭,在此有兩個:
score和board的作用前文已經講到過,在此不再贅述
var score = 0 var board = new Array()1.1.2 新建一局游戲newGame()
初始化16宮格為游戲截圖圖一的樣式,init()和generateOnNumber()方法分別見1.2.1和1.2.2
function newGamen () {// 初始化16宮格init()// 初始生成兩個數字 2 4 8 16 32 64 128 ...generateOneNumber()generateOneNumber() }1.1.3 監聽方向鍵按下事件觸發相應方向的移動
根據用戶按下的方向鍵決定16宮格中數字的移動方向,先判斷能否向該方向移動,若能則移動后重新渲染16宮格,然后在隨機生成一個2或者4,此時還需判斷新生成一個數字格后有沒有造成游戲判輸(16宮格中沒有空余空格且四個方向均不能移動)或者判贏(16宮格中出現了2048);若不能向該方向移動,則什么都不做。
注:moverToXXX(true)方法返回true時,就意味著此時可移動且改變了board二維數組的值,應將改變后的二維數組值重新渲染到頁面16宮格對應位置,renderBoard()方法見1.2.3。moveToXXX()方法將在后文的move.js移動文件中說明。
$(document).keyup(function (e) {switch (e.keyCode) {case 37 : // 向左if(moveToLeft(true)) {renderBoard()generateOneNumber()if(!isGameWin())isGameOver()}breakcase 38 : // 向上if(moveToTop(true)) {renderBoard()generateOneNumber()if(!isGameWin())isGameOver()}breakcase 39 : // 向右if(moveToRight(true)) {renderBoard()generateOneNumber()if(!isGameWin())isGameOver()}breakcase 40 : // 向下if(moveToBottom(true)) {renderBoard()generateOneNumber()if(!isGameWin())isGameOver()}breakdefault: break;} })1.1.4 點擊“New Game”按鈕新建一局游戲
$(document).on("click","button#new-game",function () {newGamen() })1.1.5 重新計算分數并回填到頁面
計分規則是:每合并一次數字格分數加4
function setScore () {score += 4$("header #score").text(score) }1.2 具體功能代碼塊
1.2.1 init()
遮罩層功能在前面的HTML代碼中注釋過,它就是一個灰色的遮罩層,將16宮格覆蓋住,只是一個樣式,不具有任何功能(若是點擊頁面某個地方觸發移動,遮罩層就有防止游戲結束用戶再次點擊的作用,可這里是用鍵盤事件觸發的移動操作,所以,這里的遮罩層是沒有啥作用的喔~)。
坑1:嚴格來說這不叫坑,是自己腦筋一時沒轉過彎來(捂臉orz...)。游戲結束時用戶按下方向鍵是不會有任何作用的,我苦思冥想N久游戲結束時如何阻止用戶的鍵盤事件未果,結果一問同事,同事說,難道不是因為用戶按四個方向鍵都不再起任何作用時才觸發的游戲結束嗎?此時根本就不必再去限制鍵盤事件了啊。emmmm....好吧,我蠢了
function init () {score = 0 // 初始化分數為0$("header #score").text(score) // 回填分數到頁面指定DOM節點上$(".mask,#game-over,#game-win").addClass("hide") // 隱藏遮罩層、gameover、gamewin框for(var x = 0;x < 4;x++) { // 將board二維數組的值全部置為0以初始化16宮格board[x] = new Array()for(var y = 0;y < 4;y++) {board[x][y] = 0}} }1.2.2?generateOnNumber()
function generateOneNumber () {// 隨機生成一個數字 2or4var randNumber = Math.random() < 0.5 ? 2 : 4// 隨機生成位置var randNumberX = Math.floor(Math.random()*4)var randNumberY = Math.floor(Math.random()*4)// 檢查該位置上是否已有值,沒有則直接在該位置上生成新數字格,若有值則重新隨機生成位置if(board[randNumberX][randNumberY] !== 0) {generateOneNumber()} else {board[randNumberX][randNumberY] = randNumber}// board二維數組中重新生成了新數字當然要將board重新渲染到頁面中咯renderBoard() }1.2.3 renderBoard()
循環遍歷二維數組board中每一個值(賦值給num),若num不為零代表該位置對應的16宮格上有數字格,改變該格子的樣式(addClass("number-cell")),并將num值填入格子中(html(board[x][y])),之后再根據num值獲取到該數字格的背景顏色和數字顏色;若num為零則表示該位置對應的16宮格上沒有數字,為空格子,此時應移除該位置上的數字格樣式并把數字“清空”(html("")),然后將該空格子的背景色置為初始值顏色。
注:此處涉及getNumberCellBgColor(num)和getNumberColor(num)兩個方法,將在后文的support.js公共方法文件中提及并說明。
function renderBoard () {for(var x = 0;x < 4;x++) {for(var y = 0;y < 4;y++) {var num = board[x][y]if(num !== 0) {$(`#grid-cell-${x}-${y}`).addClass("number-cell").html(board[x][y]).css("background",getNumberCellBgColor(num)).css("color",getNumberColor(num))} else {$(`#grid-cell-${x}-${y}`).removeClass("number-cell").html("").css("background",getNumberCellBgColor(num))}}} }1.2.4 游戲的判贏/判輸
根據前文提及的規則:
- 判贏:16宮格中合并出了“2048”則為游戲勝利
- 判輸:16宮格中沒有剩余空格子且不能再向任何方向移動則為游戲失敗
在每次成功移動數字格且再次隨機生成一個新數字格之后需要對當前16宮格進行判定,檢查其此時是否觸發游戲的勝利或者失敗。
tips:之所以在上文的1.1.3中先判斷isGameWin(),是因為判贏的代碼比判輸的簡單,先將其作為一個“關卡”判斷此部分能否繼續下去,這樣就不用每次都觸發較為麻煩的isGameOver()方法了。
1.2.4.1 判贏
function isGameWin () {for(var x = 0;x < 4;x++) {for(var y = 0;y < 4;y++) {if(board[x][y] === 2048) {gamewin()return true}}}return false } function gamewin () {$(".mask,#game-win").removeClass("hide") }1.2.4.2 判輸
moveToXXX()方法接收一個參數,該參數用于判斷此時調用moveToXXX()方法是想移動數字格還是僅僅判斷能否移動,詳細一點來講,就是傳入一個參數來判斷此時要不要更新board中的值,如果僅僅是判斷該方向上能否移動,則無需更新board數組,傳入false,若是需要判斷后移動數字格則需更新board
tips:此處isGameOver()方法中涉及一個“邏輯中斷(邏輯與)”,其實作用和上一個tips中一樣,先判斷簡單的noSpace(),若其返回值為false那么就不必再執行邏輯相對復雜的noMove()方法。
function isGameOver () {if(noSpace()&&noMove()) {gameover()} } // 判斷此時四個方向上能否有一個能移動 function noMove () {if(moveToLeft(false) || moveToTop(false) || moveToRight(false) || moveToBottom(false)) return falsereturn true } // 判斷此時16宮格中是否還有空格子 function noSpace () {for(var x = 0;x < 4;x++) {for(var y = 0;y < 4;y++) {if(board[x][y] === 0) return false}}return true } // 去掉對遮罩層和gameover框的隱藏效果 function gameover () {$(".mask,#game-over").removeClass("hide") }2 移動文件move.js
2.1 移動原理
首先,如果要保存16宮格中的情況,我們很容易就想到要用二維數組:
用一個4×4的二維數組來模擬16宮格中數字格子的位置關系,沒有數字的空格子用"0"來表示,其余有數字的將其數值存入該位置對應的二維數組,例如:
?對應的二維數組board為:[[8,16,8,16],[64,8,0,2],[8,4,0,0],[4,0,0,0]]
在開始研究原理之前,我們需要想清楚:當玩家按下方向鍵時,16宮格中的格子肯定是朝著按下的那個方向移動的,不管是向左、上、右、下移動,都是整行/列4個格子為一組進行移動,而每一組的行為都是一致的(朝著同一方向移動),所以,要研究移動的原理,只需研究長度為4的數組中值的移動規律。
此時,我們再來看看移動的原理:
以 [0,4,8,8] 為例,假如此時玩家按下向左的方向鍵,那么照理來看,此數組應該變為 [4,16,0,0] 。這要如何實現呢?
我的做法是:
第一步:拋開相同數字可以合并的規則,先將所有數字移動到它的"最終位置"上去。即遍歷該數組,去掉其中的 0。因為0代表此格子上沒有值,后面的數字格是可以移動到這個位置上來的。故此時,[0,4,8,8]就變成了[4,8,8]。
就這樣?當然不行,數組的長度可是固定為4的!arr.splice(index,length)方法會刪除數組中下標為index開始的長度為length的數值,所以每當刪去一個零,就應該在數組的末尾添上一個0(arr.push(0))來保證數組長度始終為4,這樣上述數組才按照我們預想的,變成了[4,8,8,0]
所以,第一步總結來看,就是刪0補0,將數字全部移至最左邊,數字與數字之間不會存在0的情況。
// 先刪除數組中的0 var tag = 0 for(var i = 0;i < arr.length;i++) {if(arr[tag] === 0) {arr.splice(tag,1)arr.push(0)} else {tag++continue} }第二步:完成相同數字的合并。依舊是循環遍歷該數組,但區別于第一步的遍歷,此時的遍歷從下標為1開始,有兩種情況:
若當前值為0,則可直接結束這步操作,因為在第一步中我們已經將該數組中間位置的所有0都刪除了,唯一可能出現0的情況,就是在數組的末尾或者是該數組全為0,不管是哪一種,都表示當前值到循環結束之間已經不存在有值的位置了。
若當前值不為0,則判斷當前值與上一個值是否相等,如相等就將上一個值×2并刪除當前值,在數組末尾push(0),如不相等就continue
總結來講,第二步就是刪除相鄰的重復值,并將前一個值×2,數組末尾添0。
// 判斷相加/合并 for(var i = 1;i < arr.length;i++) {if(arr[i] === 0) breakif(arr[i] === arr[i-1]) {setScore()arr[i-1] *= 2arr.splice(i,1)arr.push(0)} }此時,完成了我們想要的“最終效果”,即[0,4,8,8]變成了[4,16,0,0],然后利用renderBoard()將board渲染到16宮格中便大功告成了!!然鵝,當你多玩幾次就會發現bug了。
如果數組為[2,2,4,8],按照上面的步驟操作下來,數組就變成了[4,4,8,0],然鵝我們的預想應該是[16,0,0,0]才對啊!所以,此時還需在第二步的基礎上進行改進,即為下述的第三步:
第三步:合并數字后下標的回退。兩個相鄰的相同數字合并之后還應檢查其合并后的值與其移動方向上一位的值是否一致,若一致,則應再次觸發合并操作。例如:[2,2,4,8]第一次合并之后為[4,4,8,0],此時若想進行第二次合并,則應將下標再一次從1開始重新遍歷數組,重復第二步的操作,得到[8,8,0,0],第三次重復遍歷后才能得到最終值[16,0,0,0]。
總結第三步就是如遇合并重復遍歷。
// 判斷相加/合并 for(var i = 1;i < arr.length;i++) {if(arr[i] === 0) breakif(arr[i] === arr[i-1]) {setScore()arr[i-1] *= 2arr.splice(i,1)arr.push(0)i = 0 // 第三步!此處不能 i -= 1 [8,2,2,4]會出問題} }坑:如上述第三步的代碼注釋中,i=0不能寫成i-=1。我最初的想法是,合并完成之后,將下標回退1然后for循環i++之后,下標i還是指向的是剛剛判斷過的那一位,這樣就完成了合并之后再次判斷當前位與其移動方向上一位的值的比較。想來是沒有什么問題,直到遇到[8,2,2,4]這種數組。
eg:[8,2,2,4]
第一次,循環到下標i=2時產生合并,即a[2]=a[2-1] = 2,a[2-1]=4,刪去a[2],在數組末尾添0,處理之后為[8,4,4,0],此時下標i回退1,i = 1
第二次,for循環會使第一次得到的i先+1,即i = 2,此時a[2] = a[2-1] = 4,a[2-1] = 8,刪去a[2],在數組末尾添0,處理之后為[8,8,0,0],此時下標i回退1,i=1
第三次,i仍是先+1,即i=2,此時a[2] = 0,會直接break循環,產生錯誤的結果——>[8,8,0,0],而非[16,0,0,0]
解決:故此,下標的回退必須從頭開始。
完整的移動處理函數如下:
function updateArr (arr) {// 先刪除數組中的0var tag = 0for(var i = 0;i < arr.length;i++) {if(arr[tag] === 0) {arr.splice(tag,1)arr.push(0)} else {tag++continue}}// 判斷相加/合并for(var i = 1;i < arr.length;i++) {if(arr[i] === 0) breakif(arr[i] === arr[i-1]) {setScore()arr[i-1] *= 2arr.splice(i,1)arr.push(0)i = 0 // 此處不能 i -= 1 [8,2,2,4]會出問題}} }2.2 按序傳入數組
講完了移動的原理,剩下的就好辦多了,幾乎不怎么需要動腦子了。2.1中的移動原理是基于長度為4的數組往左移的情況,但我們的小游戲中16宮格中的數字格會隨玩家按下的方向鍵不同而朝著不同方向移動,這可如何處理呢?
很簡單,以下圖中的情況為例進行說明:
2.2.1 左移
updateArr(arr)方法接收一個數組,并將其左移進行相關的合并操作,那么如果想將16宮格(4×4的二維數組)進行移動操作,就應將二維數組分行傳入該處理函數,即分次傳入[8,0,0,0],[16,0,0,0],[8,2,2,0],[2,8,4,0]
function moveToLeft () {var arr = []for(var x = 0;x < 4;x++) {arr[x] = board[x]updateArr(arr[x])} }!tips:此處用一個新數組arr來保存board每次傳入的一維數組的目的,待后文闡述。
2.2.2 右移
同理,如要將二維數組向右移動,則應把每次傳入的數組逆序傳入處理函數,即分次傳入[0,0,0,8],[0,0,0,16],[0,2,2,8],[0,4,8,2]
function moveToRight () {var arr = []for(var x = 0;x < 4;x++) {arr[x] = new Array()// 數組反向傳入處理函數arr[x] = board[x].concat()arr[x].reverse()updateArr(arr[x])} }!tips:此處涉及一個數組API的問題,arr.reverse()方法會將數組元素倒序,但改變的是原數組的值,然而我想保留原數組board[x]值不變,將新數組arr[x]倒序傳入處理函數即可。這里采用的是board[x].concat()來返回一個新數組并賦值給arr[x]的方式來保留原數組不受reverse的影響(保留原數組的原因見后文)。
2.2.3 上移
上移操作較麻煩一丟丟,但也還好。如上圖,若要上移且滿足updateArr()方法中的處理程序,應分次傳入[8,16,8,2],[0,0,2,8],[0,0,2,4],[0,0,0,0]
function moveToTop () {var arr = []for(var y = 0;y < 4;y++) {arr[y] = new Array()for(var x = 0;x < 4;x++) {arr[y].push(board[x][y])}updateArr(arr[y])} }2.2.4 下移
同上移的原理,但傳入updateArr()方法的一維數組同樣應該逆序傳入,即分次傳入:[2,8,16,8],[8,2,0,0],[4,2,0,0],[0,0,0,0]
function moveToBottom () {var arr = []for(var y = 0;y < 4;y++) {arr[y] = new Array()for(var x = 0;x < 4;x++) {arr[y].unshift(board[x][y])}updateArr(arr[y])} }2.3 能否移動
到此為止,移動的原理及相關處理基本介紹完畢。之前為了簡化對移動的處理,未在移動之前先檢查該方向上能否移動。
如何才叫能移動呢?——>[0,4,8,16],[2,2,0,0]這兩種情況。用文字來描述就是:
- 當前值不為0,且當前值之前有為0的值
- 該數組中存在相鄰位置的值相等的情況(即能產生合并)
據此可寫出代碼如下:
function canMove (arr) {var hasZero = falsefor(var i = 0;i < arr.length;i++) {if(arr[i] === 0) {hasZero = true} else if(arr[i] !== 0 && hasZero || arr[i] === arr[i+1]) {return true} else {continue}}return false }2.4 完整移動
完整的移動邏輯應該是:先判斷該方向上能否移動,但由于我們的移動是分次傳入,只要有一次的結果是可移動,那么整個二維數組都是可移動的,所以要先將分次傳入的數組進行移動處理后的結果保存在新數組里,當四次分次傳入均處理完且其中有一個能移動時,將新數組的值賦值給原數組board,完成二維數組的條件更新。
以左移為例:
function moveToLeft (type) {var arr = [],tag = falsefor(var x = 0;x < 4;x++) {arr[x] = new Array()arr[x] = board[x]tag = tag || canMove(arr[x])if(type) updateArr(arr[x])}if(tag && type) board = arrarr = nullreturn tag }tag用于接收能否移動的結果,type的作用前文已講過,不再細說。
3 support.js工具文件
主要是數字格的背景色和文字顏色的設置,理解上沒什么難度。
function getNumberCellBgColor (num) {switch (num) {case 2 :return "#EEE4DA"case 4 :return "#EDE0C8"case 8:return "#F26179"case 16:return "#F59563"case 32:return "#F67C5F"case 64:return "#F65E36"case 128:return "#EDCF72"case 256:return "#EDCC61"case 512:return "#9C0"case 1024:return "#3365A5"case 2048:return "#09C"case 4096:return "#a6bc"case 8192:return "#93c"default: return "#CDC1B4"} }function getNumberColor(number) {if (number <= 4){return "#776e65";}return "white"; }完整代碼
完整代碼詳見:https://github.com/Crystal-Zx/2048-H5-GAME
總結
以上是生活随笔為你收集整理的2048游戏简单实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 空间相关分析(四) 空间相关分析实战—
- 下一篇: PLSQL配置Oracle 64位