这个游戏JS部分全都是用原生JS代码写的,加有少量的CSS3动画,并简单的兼容了一下移动端。
先看一下在线的demo:https://yuan-yiming.github.io/2048-online-game/
github地址:https://github.com/Yuan-Yiming/2048-online-game
====================================================================
下面简单分析一下JS代码:
1.游戏中包含的主要事件:
new game按钮的click事件:点击后重新开始一次新的游戏;
game over后重新开始按钮的click事件:点击后重新开始一次新的游戏;
桌面端键盘上的方向键keydown的事件:按下方向键后数字会向相应的方向移动;
移动端的touch事件:通过计算touchstart和touchend的坐标变化,判断手指在移动端屏幕的滑动方向;
2.业务逻辑
2.1 重新开始一次游戏:
需要清空游戏盘,并且随机给一个格子生成一个数字2,每次生成不同数字都会给格子配相应的背景颜色;
2.2 按下方向盘或滑动屏幕时格子移动情况(核心部分)
游戏面板有4 x 4的格子,遍历每一行的每一个格子,若为空格则不作处理,非空格则要判断其下一步(注意每次只判断一步的动作,方法howToGo())的运动情况,即:
a:空格,不作处理
b:非空格,进一步判断下一步动作,有3种情况:
b1:若需要移动的格子前面是没有数字的空格,则格子往前移动一个,即将数字插入到前面格子内,原本格子中的数字销毁,然后从b开始循环再次判断;
b2:若需要移动的格子前面不是空格,且与前面格子包含的数字不相等,则格子无需移动,该格子运动判断完毕,进行下一个格子的运动判断;
b3:若需要移动的格子前面不是空格,且与前面格子包含的数字相等,则将前面格子内的数字 x 2,原本格子中的数字销毁,该格子运动判断完毕,进行下一个格子的运动判断;
注意:由于是每次运动都是由运动方向的最前面格子开始运动的,即b2,b3两种情况碰到前面有格子时,说明前面的格子已经完成运动,前面没有空位了,所以当前格子运动完成,继续移动后面的格子。
以下使用图片来说明格子运动的情况:
上面有两行格子,如果是向左运动,首先遍历第1行,对1~4号格子依次判断其运动情况,首先1号格子先运动,由于是1号格子靠边,可认为其前面是包含不同数字的格子,运动情况为b2,无需运动;2号格子为空格,运动情况为a;3号格子前面为空格,首先运动情况为b1,运动后3号格子的数字插到2号格子,3号格子数字销毁成为空格,然后需要对2号格子再次判断,再进行一次运动,即b3,使得1号格子数字变成4,2号格子成为空格;后面4号格子为空格不作处理。遍历第2行,对5~8号格子依次判断其运动情况,5号格子与1号格子情况相同,6、7、8号格子为空格,无需处理。
向左运动后的格子:
如果是向右运动,遍历第1行要从右边开始,4~1号格子依次运动,首先4号格子空格无需处理;然后3号格子运动情况b1,向前移动一格,数字2插入到4号格子内;2号空格无需处理;1号空格先连续进行两次b1运动,再进行一次b3运动,碰到4号格子内数字相同,最终使得4号格子内数字变成4。遍历第2行,8~5号格子依次运动,8、7、6号为空格均无需处理,5号格子连续3次b1运动,使得8号格子内数字变成2。
向右运动后的格子:
向上或者向下运动同理。
=====================================================================
下面废话不说,直接上代码。
HTML代码:index.html
2048Play 2048 Game Online!
Score
0+12Best
1024+12New GameGame is over...
Try again?
You win the game!!
Congratulation!!
Try again?
CSS桌面版代码:desktop.css
* { list-style: none; color: #8f7a66; padding: 0; margin: 0; box-sizing: border-box; } body { background-color: #ffffe0; } .wrapper { position: relative; width: 400px; height: 540px; /*border: 1px solid red;*/ margin: 0 auto; } /*头部*/ .header { width: 400px; height: 140px; /*border: 1px solid green;*/ position: relative; /*opacity: 0.4;*/ } .title, .slogan, .score, .best, .new-game, .github>a { position: absolute; } .title strong { display: inline-block; width: 260px; height: 100px; font-size: 78px; line-height: 100px; /*text-align: center;*/ padding: 0 5px; /*border: 1px solid black;*/ } .slogan { padding: 0 5px; top: 85px; /*border: 1px solid black;*/ } .github>a { display: inline-block; width: 50px; height: 50px; /*border: 1px solid red;*/ top: 8%; right: 5%; /*margin: 5px;*/ background: url("Git.png") no-repeat 0 0; border-radius: 50%; background-size: 100%; } .github>span>span { display: inline-block; width: 100px; height: 16px; line-height: 16px; position: absolute; bottom: -24px; left: -25px; text-align: center; color: #2c2c2c; } /*分数*/ /* 分数动画 */ .score-animation, .best-animation{ display: none; position: absolute; top: 25px; left: 10px; width: 65px; height: 30px; font-size: 24px; font-weight: bold; } .score { left: 220px; } .best { left: 315px; } .score, .best { position: absolute; width: 85px; height: 60px; line-height: 28px; top: 20px; background-color: #bbada0; } .score span, .best span, .new-game span { color: #ffffff; } .score, .best, .new-game, .game-board, .grid { text-align: center; border-radius: 5px; } .best .number, .score .number, .new-game { font-size: 22px; font-weight: bold; } .new-game { width: 180px; height: 40px; line-height: 40px; left: 220px; top: 90px; text-align: center; background-color: #8e7963; cursor: pointer; } .new-game:hover { width: 182px; height: 42px; line-height: 42px; left: 219px; top: 89px; font-size: 24px; } /*游戏主面板*/ .game-board { width: 400px; height: 400px; padding: 5px 5px; background-color: #bbada0; /*opacity: 0.4;*/ } .grid { position: relative; float: left; width: 87.5px; height: 87.5px; line-height: 87.5px; /*font-size: 48px;*/ font-weight: bold; margin: 5px; background-color: #b0c4de; } .game-board .grid span { /*color: */ } /*game over or win the game弹出页面*/ .popup .game-over, .popup .win { position: absolute; left: 60px; text-align: center; width: 280px; height: 160px; border-radius: 5px; /*border: 1px solid red;*/ opacity: 1.0; } .popup p { color: #8f7a66; } .popup .game-over { display: none; top: 230px; font-size: 36px; font-weight: bold; } .popup .win { display: none; top: 220px; font-size: 28px; font-weight: bold; } p.try-again { color: #fff; font-size: 22px; width: 150px; height: 50px; line-height: 50px; border-radius: 5px; margin: 0 auto; background-color: #8f7a66; cursor: pointer; } .header, .game-board { /*opacity: 0.4;*/ } .new-grid { background-color: #b0c4de !important; } /* 生成新格子动画 */ @keyframes tempgrid { from {width: 45px; height: 45px; top: 24px;left: 24px; font-size: 18px; line-height: 45px;display: block;} to {width: 87.5px; height: 87.5px; top: 0px;left: 0px; font-size: 48px; line-height: 87.5px;display: none;} } @-webkit-keyframes tempgrid { from {width: 45px; height: 45px; top: 24px;left: 24px; font-size: 18px; line-height: 45px;display: block;} to {width: 87.5px; height: 87.5px; top: 0px;left: 0px; font-size: 48px; line-height: 87.5px;display: none;} } @-moz-keyframes tempgrid { from {width: 45px; height: 45px; top: 24px;left: 24px; font-size: 18px; line-height: 45px;display: block;} to {width: 87.5px; height: 87.5px; top: 0px;left: 0px; font-size: 48px; line-height: 87.5px;display: none;} } .temp-grid { animation: tempgrid 0.15s ease-in forwards; -webkit-animation: tempgrid 0.15s ease-in forwards; -ms-animation: tempgrid 0.15s ease-out forwards; -moz-animation: tempgrid 0.15s ease-out forwards; }
CSS移动端兼容代码:mobile.css
body { background-color: #fffadd; } .wrapper { width: 100%; /*height: 540px;*/ /*border: 1px solid red;*/ /*margin: 0 auto;*/ } /*头部*/ .header { width: 100%; /*height: 140px;*/ position: relative; } .title, .slogan, .score, .best, .new-game { position: absolute; float: left; text-align: center; } .title, .slogan { width: 100%; } .title strong { display: inline-block; width: 100%; /*height: 260px;*/ font-size: 487%; /* 100% 16px */ /*line-height: 192%;*/ text-align: center; /*padding: 0 5px;*/ /*border: 1px solid black;*/ } .slogan { /*margin-top: 10px;*/ top: 65%; } /* github */ /*.github>a { top: 4%; right: 6.25%; }*/ /*分数*/ .score, .best, .new-game { width: 25%; /*border: 1px solid green;*/ } /* 分数动画 */ .score-animation, .best-animation{ display: none; /*position: absolute;*/ top: 25px; left: 10px; width: 65px; height: 30px; font-size: 24px; font-weight: bold; } .score, .best { /*position: absolute;*/ line-height: 28px; top: 90%; background-color: #bbada0; } .score { left: 47.5%; } .best { left: 75%; } .new-game { width: 45%; height: 60px; line-height: 60px; left: 0; top: 90%; text-align: center; background-color: #8e7963; cursor: pointer; /*padding-bottom: 2em;*/ font-size: 28px; } .new-game:hover { width: 45%; /*height: 42px;*/ /*line-height: 42px;*/ height: 60px; line-height: 60px; left: 0; top: 90%; /*line-height: 2e;*/ font-size: 28px; } .score span, .best span, .new-game span { color: #ffffff; } .score, .best, .new-game, .game-board, .grid { text-align: center; border-radius: 5px; } .best .number, .score .number { font-size: 22px; font-weight: bold; } /*游戏主面板*/ .game-board { position: absolute; /*display: none;*/ width: 100%; height: auto; /*height: 400px;*/ /*padding: 10px 10px;*/ background-color: #bbada0; /*opacity: 0.4;*/ top: 36%; } .grid { /*position: relative;*/ float: left; width: 22%; /*height: */ /*padding-bottom: 21.95%;*/ /*height: 100%;*/ line-height: 80px; /*font-size: 48px;*/ font-weight: bold; /*padding-bottom: 410px;*/ padding: 1.5%; background-color: #b0c4de; } /* 生成新格子动画 */ @keyframes tempgrid { from {width: 50%; height: 50%; top: 25%;left: 25%; font-size: 24px; line-height: 192%;display: block;} to {width: 100%; height: 100%; top: 0px;left: 0px; font-size: 48px; line-height: 192%;display: none;} } @-webkit-keyframes tempgrid { from {width: 50%; height: 50%; top: 25%;left: 25%; font-size: 24px; line-height: 192%;display: block;} to {width: 100%; height: 100%; top: 0px;left: 0px; font-size: 48px; line-height: 192%;display: none;} } @-moz-keyframes tempgrid { from {width: 50%; height: 50%; top: 25%;left: 25%; font-size: 24px; line-height: 192%;display: block;} to {width: 100%; height: 100%; top: 0px;left: 0px; font-size: 48px; line-height: 192%;display: none;} }
js代码:2048.js
// 页面加载时 window.Onload= function () { // var temp = tempGrid(2); giveNumber(2); newGameBotton(); getReady(); backgroundColorToNumber(); scaleWidth(); touch(); } // 在移动端使得格子宽高比例1:1 function scaleWidth() { // 获取格子的宽度 var grid = document.getElementsByClassName("grid"), width = window.getComputedStyle(grid[0], null)["width"]; // width = grid[0].style.width; //给格子高度赋值 for (var i = 0; i <16; i++) { grid[i].style.height = width; } } // 创建一个临时的格子 function createTempGrid(num) { var temp = document.createElement("div"); temp.innerHTML = "" + num + ""; temp.style.position = "absolute"; temp.style.backgroundColor = "#fff8dc"; temp.style.width = "87.5px"; temp.style.height = "87.5px"; temp.style.lineHeight = "87.5px"; temp.style.fOntWeight= "bold"; temp.style.fOntSize= "48px"; temp.style.borderRadius = "5px"; temp.style.top = "0"; temp.style.left = "0"; // temp.style.display = "none"; // console.log(temp); temp.classList.add("temp-grid"); return temp; }; // 删除临时格子 function deleteTempGrid() { var temp = document.getElementsByClassName("temp-grid"); for (var i = 0; i") { newGrid.classList.add("new-grid"); newGrid.innerHTML = "" + num + ""; newGrid.appendChild(tempGrid); break; } } // return blankGrid; } // clearGrid:清空格子及分数归零 function clearGrid() { var grid = document.getElementsByClassName("grid"), score = document.getElementsByClassName("score")[0].children[2]; score.innerText = "0"; for (var i = 0; i "; // grid[i].style.backgroundColor = "#b0c4de"; } backgroundColorToNumber(); } // 重新开始一次游戏 function newGame() { clearGrid(); giveNumber(2); backgroundColorToNumber(); return true; } // 触发新一次游戏的按钮 function newGameBotton() { var newGameBtn = document.getElementsByClassName("new-game")[0]; newGameBtn.addEventListener("click", function () { newGame(); }, false); newGameBtn.addEventListener("touchend", function () { newGame(); }, false); } // backgroundColorToNumber:数字跟背景颜色/数字大小对应 function backgroundColorToNumber() { var gridNum, // child, grid = document.getElementsByClassName("grid"); for (var i = 0; i 1) { count += go; // 累计每一次运动的得分 } } } else if (dir == "down" || dir == "right") { for (var j = 2; j >= 0; j --) { max = 3 - j; go = howToGo(arr[j], dir, max); // gridMove(arr[j], dir, 1); // console.log("go:" + go); signal += go; if (go > 1) { count += go; // 累计每一次运动的得分 } } } } // 格子有运动signal > 0 if (signal > 0) { // console.log("signal:" + signal); giveNumber(2); backgroundColorToNumber(); testGameOver(); } // 格子移动,且得分>0 if (count > 0) { addScore(count); } return count; } // 移动端使用touch事件来监听滑块移动 function touch() { var gameBoard = document.getElementsByClassName("game-board")[0]; gameBoard.addEventListener("touchstart",function (e) { // e.preventDefault(); startX = e.changedTouches[0].pageX; startY = e.changedTouches[0].pageY; },false); gameBoard.addEventListener('touchend',function(e){ e.preventDefault(); // 阻止浏览器的默认行为,例如滚动、跳转等!! //获取滑动屏幕时的X,Y endX = e.changedTouches[0].pageX, endY = e.changedTouches[0].pageY; //获取滑动距离 distanceX = endX-startX; distanceY = endY-startY; //判断滑动方向,滑动角度大于15° if(Math.abs(distanceX) / Math.abs(distanceY) > 1.73 && distanceX > 0){ deleteTempGrid(); keyDown(39); }else if(Math.abs(distanceX) / Math.abs(distanceY) > 1.73 && distanceX <0){ deleteTempGrid(); keyDown(37); }else if(Math.abs(distanceY) / Math.abs(distanceX) > 1.73 && distanceY <0){ deleteTempGrid(); keyDown(38); }else if(Math.abs(distanceY) / Math.abs(distanceX) > 1.73 && distanceY > 0){ deleteTempGrid(); keyDown(40); }else{ // console.log('点击未滑动'); } }); } // 3.记录分数,分数会增加, function addScore(score) { var span = document.getElementsByClassName("number"), currentScore = parseInt(span[0].innerText), bestScore = parseInt(span[1].innerText); span[0].innerText = score + currentScore; scoreUpAnimaton("score", score); if (span[0].innerText > bestScore) { scoreUpAnimaton("best", score); span[1].innerText = span[0].innerText; } } // howToGoLeft(ele, direction, max):该函数判断单个格子怎么移动 function howToGo(ele, direction, max, testMode) { var prevGrid, prevGridNum, gridNum = 0, go, addNum, numLen, doubleNumGrid; // console.log(prevGrid); // 各个方向 prevGrid = getPrevGrid(ele, direction); gridNum = getGridNum(ele); if (prevGrid) { prevGridNum = getGridNum(prevGrid); } else { prevGridNum = "null"; } // 前面是空格,要继续判断。。。。。。。。。。。。。。。。。。。。。 if (gridNum && !prevGridNum) { prevGrid.innerHTML = ele.innerHTML; ele.children[0].innerText = ""; max -= 1; // gridMove(ele, direction, 1); if (max) { go = howToGo(prevGrid, direction, max); // 0、double、continue } // 返回1 // console.log("go:" + (go || 1)); // if (max == 0) { // console.log("before:" + typeof(go)); // go = 1; // console.log("after" + typeof(go)); // } return go || 1; // 若go = 0,返回1;go = double,返回double,go = underfied,返回1 // 和前面数字相同 } else if (gridNum == prevGridNum) { if (!testMode) { gridNum *= 2; // addScore(gridNum); // gridMove(ele, direction, 1); prevGrid.children[0].innerText = gridNum + ""; // 在这里添加数字变大的动画: // numLen = (gridNum + "").length; ele.children[0].innerText = ""; // console.log('gridNum:' + gridNum) if (gridNum == 2048) { popup("win"); } // 如果数字叠加,就返回得分,且得分≥4 } // console.log("gridNum: " + gridNum); return gridNum; } else { // 格子没动,返回0 return 0; } } // 4.怎么判断game over,或者达到2048为winner // test geme over function testGameOver() { var content, leftTest, rightTest, upTest, downTest, count = 0; grid = document.getElementsByClassName("grid"); for (var i = 0; i ") { count += 1; } } // console.log("count:" + count); if (count == 16) { if (getGridNum(grid[3]) == getGridNum(grid[4])) { count -= 2; } if (getGridNum(grid[7]) == getGridNum(grid[8])) { count -= 2; } if (getGridNum(grid[11]) == getGridNum(grid[12])) { count -= 2; } for (var i = 0; i