天天看点

用jquery制作2048小游戏(超详细)

先放个效果图吧

用jquery制作2048小游戏(超详细)

首先,先把html结构搭好。新建一个html文件,设置好标题2048,按钮New Game以及score:0,这里按钮是用a标签来将javascript中的newgame函数给传进来,score后面的分数由于是动态的,所以给它添加一个span标签,并加上一个id,代码如下

<header>
        <h1>2048</h1>
        <a href="javascript:newgame();" id="newgamebutton">New Game</a> <!-- 点击a标签后,执行一个js的自定义函数newgame() -->
        <a href="javascript:undo();" id="undobutton">Undo</a> <!-- 点击a标签后,执行返回上一步的操作 -->
        <p>score: <span id="score">0</span></p>
    </header>
           

然后将所有的格子布置好,这里是用div标签来表示每个格子,后续将会用相对定位,绝对定位以及js中的函数来编写每个格子的位置。代码如下

<div id="grid_container">
    <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>
           

现在html结构已经搭完了,开始编写css样式。新建一个css文件,这里css样式其实没什么好讲的,各块内容多大字号,什么背景颜色,padding,margin为多少都是看自己喜好,该居中的居中,我设的大盒子长宽为500px,每个格子长宽为100px,代码如下

header {
    margin: 0 auto;
    width: 500px;
    text-align: center;
}

header h1 {
    font-family: Arial;
    font-size: 40px;
    font-weight: bold;
}

#newgamebutton {
    /* 为了设置各方向的padding和margin,将其变成块元素 */
    display: block;
    margin: 20px auto;
    width: 100px;
    color: white;
    background-color: #8f7a66;
    text-decoration: none;
    padding: 10px;
    border-radius: 10px;
    font-family: Arial;
}

#newgamebutton:hover {
    background-color: #9f8b77;
}

#undobutton {
    display: inline-block;
    margin: 5px 20px;

    width: 100px;
    padding: 10px 10px;
    background-color: #8f7a66;

    font-family: Arial;
    color: white;

    border-radius: 10px;

    text-decoration: none;
}

#undobutton:hover {
    background-color: #9f8b77;
}

header p {
    font-family: Arial;
    font-size: 25px;
    margin: 20px auto;
}

#grid_container {
    width: 460px;
    height: 460px;
    padding: 20px;
    margin: 30px auto;
    border-radius: 10px;
    background-color: #bbada0;
    /* 给大盒子设置相对定位 */
    position: relative;
}

.grid_cell {
    width: 100px;
    height: 100px;
    border-radius: 6px;
    background-color: #ccc0b3;
    /* 给小盒子设置绝对定位 */
    position: absolute;
}
           

css样式设置完后,就可以开始编写js程序了,新建一个js文件。

首先先想清楚我们需要设置什么。从头开始看,我们用a标签定义了一个按钮,在a标签中调用了一个js的函数newgame,那么点击New Game按钮会发生什么呢?初始化,以及产生两个随机数。

初始化中又有什么功能?每一个格子的数字全部初始化为0(0这个数字可以自己随便取),那么我们就需要定义一个数组专门存放数字。并且分数score也归零。

var board = []; //存放数字的数组
var score = 0; //存放得分
var hasConflicted = []; //存放是否冲突的数组
let preValue = [];//存放每一步时每个格子的值
let preScore = [];//存放每一步时的得分
           

此处我们定义了一个数组hasConflicted ,现在先不讲,等到最后所有功能完成了我们再讲。另外我们之前给每个格子设置了绝对定位,但是还没设置具体的位置,那么在此处一并设置了。代码如下

$(function () {
    newgame();
})

// 初始化
function newgame() {
    for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 4; j++) {
            var gridCell = $("#grid_cell_" + i + "_" + j); // 定义变量gridCell来编写每个格子的位置
            gridCell.css("top", getPosTop(i, j));
            gridCell.css("left", getPosLeft(i, j)); //只需定义格子的左上角即可
        }
    }

    for (var i = 0; i < 4; i++) {
        board[i] = [];
        hasConflicted[i] = [];
        for (var j = 0; j < 4; j++) {
            board[i][j] = 0; //把board定义为二维数组来更方便地表达出每个格子,board为0即这个格子没有数字,不为0即有数字
            hasConflicted[i][j] = false; //初始化时需要将每个格子是否冲突的属性定义为false
        }
    }

    updateBoardView(); //定义此函数来将数字放到该格子中

    generateOneNumber(); //每次初始化游戏都会随机产生两个数字
    generateOneNumber();

    score = 0; //初始化
    updateScore(score);
}
           

先看第一段循环,此处定义了一个变量gridCell来方便编写每个格子的位置,然后又调用了两个函数getPosTop和getPosLeft。

用jquery制作2048小游戏(超详细)

可以看到每个格子的顶部距离大盒子的顶部都是20px+120px乘纵坐标数,同理,左端也是20px+120px乘横坐标数。因此代码如下

function getPosTop(i, j) {
    return 20 + i * 120; //每个格子的上方与大盒子顶端的距离
}

function getPosLeft(i, j) {
    return 20 + j * 120; //每个格子的左边与大盒子左边的距离
}
           

再接着看newgame函数,由于board是存放每个格子的数字的数组,因此它是一个二维数组,所以第二个循环就是完成这件事的,此处我是将board初始化为0来代表这个格子没有数字,大家也可以定义为其他数字。hasConflicted最后再讲。

然后就是定义了一个函数updateBoardView,这个函数是用来将每个数字放到相应的格子中去的。所以我们每次进行上下左右的操作时,都要调用该函数。代码如下

function updateBoardView() {
    $(".number_cell").remove(); //由于每一次操作都要调用该函数,所以每次调用之前都要先将上次操作后产生的数字清除
    for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 4; j++) {
            $("#grid_container").append("<div class='number_cell' id='number_cell_" + i + "_" + j + "'>"); //给每个格子添加数字
            var theNumberCell = $("#number_cell_" + i + "_" + j); //定义该变量来编写每个格子中数字的css样式
            if (board[i][j] == 0) {
                theNumberCell.css("width", "0"); //当该位置没有数字时,宽高设为0就行
                theNumberCell.css("height", "0");
                theNumberCell.css("top", getPosTop(i, j));
                theNumberCell.css("left", getPosLeft(i, j));
            } else {
                theNumberCell.css("width", "100px");
                theNumberCell.css("height", "100px");
                theNumberCell.css("top", getPosTop(i, j));
                theNumberCell.css("left", getPosLeft(i, j));
                theNumberCell.css("background-color", getNumberBackgroundColor(board[i][j])); //定义该函数来存放不同数字的背景颜色
                theNumberCell.css("color", getNumberColor(board[i][j])); //定义该函数来存放不同数字的颜色
                theNumberCell.text(board[i][j]); //将数字值赋予给相应的格子
            }
            hasConflicted[i][j] = false; //由于每一次操作都要调用该函数,所以每次调用之前都要先将每个格子是否冲突的属性定义为false
        }
    }
}
           

我们刚说了,每一次操作都要调用该函数,那么相应的,每一次操作后,每个格子中的数字都要先删掉,然后再赋值,第一行就是完成这件事。

然后由于不同的格子有不同的数字,所以我们得定义一个变量theNumberCell来方便编写。要定义该变量,我们就得先给每个格子设置一个number_cell的类和id,所以我们先动态添加一个html标签。

添加完标签后,定义变量theNumberCell,然后又由于没有数字的格子和有数字的格子样式不一样,所以此处分开讨论。

当该位置没有数字时(即board=0),也就是不显示数字,那么宽高设为0就行,位置的话我们之前设置了两个位置函数,这里直接调用就行。当该位置有数字时,宽高设为100px,位置照样调用位置函数,背景颜色和数字的颜色我们定义了两个函数,最后将该数字放到该格子中去就行。

这里新加了一堆div标签,所以我们再给它们添加一下css样式,代码如下

.number_cell {
    font-family: Arial;
    font-size: 30px;
    font-weight: bold;
    line-height: 100px;
    text-align: center;
    border-radius: 6px;
    position: absolute;
}
           

背景颜色和数字颜色的函数我就直接给代码了,没什么好说的

function getNumberBackgroundColor(number) {
    switch (number) {
        case 2:
            return "#eee4da";
            break;
        case 4:
            return "#ede0c8";
            break;
        case 8:
            return "#f2b179";
            break;
        case 16:
            return "#f59563";
            break;
        case 32:
            return "#f67c5f";
            break;
        case 64:
            return "#f65e3b";
            break;
        case 128:
            return "#edcf72";
            break;
        case 256:
            return "#edcc61";
            break;
        case 512:
            return "#9c0";
            break;
        case 1024:
            return "#33b5e5";
            break;
        case 2048:
            return "#09c";
            break;
        case 4096:
            return "#a6c";
            break;
        case 8192:
            return "#93c";
            break;
    }
    return "black";
}

function getNumberColor(number) {
    if (number <= 4) {
        return "#776e65";
    } else {
        return "white";
    }
}
           

回到newgame函数中,那么现在updateBoardView这个函数也讲完了,之后的就是随机生成两个数字的函数。代码如下

function generateOneNumber() {
    if (nospace(board)) { //先判断还有没有空位置
        return false;
    } else {
        //随机生成一个位置
        var randx = parseInt(Math.floor(Math.random() * 4));
        var randy = parseInt(Math.floor(Math.random() * 4));
        var times = 0;
        while (times < 50) {
            if (board[randx][randy] == 0) {
                break; //判断该位置是否有数字,若没有,则直接跳出循环,若有,则继续生成randx和randy
            } else {
                randx = parseInt(Math.floor(Math.random() * 4));
                randy = parseInt(Math.floor(Math.random() * 4));
            }
            times++;
        }
        if (times == 50) {
            for (var i = 0; i < 4; i++) { //若算法运行了50次后仍然没有找到空位置,则人工采用for循环来找
                for (var j = 0; j < 4; j++) {
                    if (board[i][j] == 0) {
                        randx = i;
                        randy = j;
                    }
                }
            }
        }
        //随机生成一个数字(2或者4)
        var randNumber = Math.random() < 0.5 ? 2 : 4; //当随机数小于0.5时,取2,否则取4
        //把数字放进空位置
        board[randx][randy] = randNumber;
        showNumberWithAnimation(randx, randy, randNumber); //给生成数字的动作添加动画
        return true;
    }
}
           

要想生成数字,首先得判断一下还有没有空位置,此处调用了一个新函数。怎么判断有没有空位置呢?遍历所有的格子,如果有格子的数字为0,那么就有空位置,全部遍历完都没有为0的数字的话,就没有空位置,代码如下

function nospace(board) {
    for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 4; j++) {
            if (board[i][j] == 0) {
                return false;
            }
        }
    }
    return true;
}
           

回到generateOneNumber函数中,判断完后,就该生成数字了。生成数字有两步,先随机生成一个空位置,再随机生成一个数字。

先来看第一步,怎么随机生成一个空位置呢?由于每个格子的坐标都是从0开始的(从html处编写的grid_cell的id也可以看出),所以坐标值的话就随机取[0,1,2,3]即可,此处采用Math.random(),该语句代表随机生成0到1的数,所以将它*4后再取floor即可,由于此处生成的还是浮点型数据,我们采用一个parseInt将其改成整型数据。

范围取好后,再决定随机取哪个位置。此处我们定义一个变量times来代表循环次数,设置一个步数50步。循环中,我们就采用一个if语句便可以完成,该位置没有数字就跳出循环,有数字就继续寻找。如果50步仍然没有找到空位置,那么我们就人为地利用两个for循环来寻找。

随机位置生成后,再随机生成数字。此处我们仍然使用Math.random()语句来完成,如果随机生成的数字小于0.5,就取2,否则就取4.

最后就是将该随机数字放进该随机位置,并且此处我们使用了一个动画来显示该数字。代码如下

function showNumberWithAnimation(i, j, randNumber) {
    var numberCell = $("#number_cell_" + i + "_" + j);
    numberCell.css({
        "background-color": getNumberBackgroundColor(randNumber), //当numberCell从没有数字变到有数字,或者从一个数字变成另一个数字时,该numberCell的css样式要发生改变
        "color": getNumberColor(randNumber)
    });
    numberCell.text(randNumber);

    numberCell.animate({
        "width": "100px",
        "height": "100px",
        "top": getPosTop(i, j),
        "left": getPosLeft(i, j)
    }, 50);
}
           

传入了3个参数,横纵坐标值以及该位置的数字。首先定义一个变量numberCell,由于该位置从没有数字变成有数字时,或者从一个数字变成另一个数字时,css样式会发生改变,所以此处仍然要定义一波该变量的背景颜色以及数字颜色,直接调用这两个函数就行,最后就是自定义动画了,宽高和位置照旧,然后赋予一个50ms的时间来完成该动画。

至此,generateOneNumber函数完成,再回到newgame函数中,最后还初始化了一个score,将其变为0。newgame函数也完成。这时我们已经完成了整个页面的布局,效果图应该是和最开始放的那张图一样。

然后我们要编写的就是操作上下左右的程序了。首先编写一个键盘keydown的事件,代码如下

$(document).keydown(function (event) {
    switch (event.keyCode) { //键盘敲击事件,此处为keyCode不是keycode
        case 37: //左
            if (moveLeft()) {
                setTimeout("generateOneNumber()", 210); //先要判断是否能够向左动,向左动了后会随机产生一个数字,并判断游戏是否结束
                setTimeout("isgameover()", 300);
            }
            break;
        case 38: //上
            if (moveTop()) {
                setTimeout("generateOneNumber()", 210);
                setTimeout("isgameover()", 300);
            }
            break;
        case 39: //右
            if (moveRight()) {
                setTimeout("generateOneNumber()", 210);
                setTimeout("isgameover()", 300);
            }
            break;
        case 40: //下
            if (moveDown()) {
                setTimeout("generateOneNumber()", 210);
                setTimeout("isgameover()", 300);
            }
            break;
        default:
            break;
    }
})
           

这里首先要注意的是keyCode处的C是大写的,这个bug我当初找了好久…

先看向左动,首先写了一个if语句,如果可以向左动的话,那么会随机产生一个数字,并且需要判断会不会游戏结束,这个setTimeout我们留到后面一起讲。

先来看看moveLeft这个函数,代码如下

function moveLeft() {
    if (canMoveLeft(board)) {
        for (var i = 0; i < 4; i++) {
            for (var j = 1; j < 4; j++) {
                if (board[i][j] != 0) {
                    for (var k = 0; k < j; k++) {
                        if (board[i][k] == 0 && noBlockHorizontal(i, k, j, board)) { //寻找可以移动到的位置board[i][k]
                            showMoveAnimation(i, j, i, k); //添加移动时的动画
                            board[i][k] = board[i][j];
                            board[i][j] = 0;
                            continue;
                        } else if (board[i][k] == board[i][j] && noBlockHorizontal(i, k, j, board) && !hasConflicted[i][k]) {
                            showMoveAnimation(i, j, i, k);
                            board[i][k] += board[i][j];
                            board[i][j] = 0;
                            score += board[i][k]; //分数叠加
                            updateScore(score); //将分数动态传回#score
                            hasConflicted[i][k] = true; //每一次移动后都要将该位置是否存在冲突的属性设为true
                            continue;
                        }
                    }
                }
            }
        }
        setTimeout("updateBoardView()", 200); //由于每一次操作,board都会发生变化,所以每一次操作都要调用updateBoardView()
        return true;
    } else {
        return false;
    }
}
           

首先要判断能不能向左动,此处我们分析一下,最左边的那一列格子是肯定不能向左动的,所以我们只需要遍历右边的那三列就行,所以定义了一个新函数,代码如下

function canMoveLeft(board) {
    for (var i = 0; i < 4; i++) {
        for (var j = 1; j < 4; j++) {
            if (board[i][j] != 0) {
                if (board[i][j - 1] == 0 || board[i][j - 1] == board[i][j]) { //当board[i][j]处左边的格子没有数字或者数字和它自身相等时,该格子可以左移
                    return true;
                }
            }
        }
    }
    return false;
}
           

可以看到先遍历右边的三列,如果某个位置有数字的话,它什么情况下可以左移呢?如果它左边那个格子没有数字或者数字和它相等的话,是不是就可以左移了,该函数即可完成。

回到moveLeft函数,当可以左移时,我们就需要进行左移的操作了。照样我们遍历右边的三列,先找到某个有数字的位置board[i][j],然后它要左移的话是不是得先找到一个格子可以让它左移,所以此处给出一个for循环来寻找j左边的所有格子,再来一一判断。

如果找到了某个格子没有数字,并且他们俩之间没有障碍物,此时就可以进行左移的操作了。没有障碍物的函数代码如下

function noBlockHorizontal(row, col1, col2, board) {
    for (var i = col1 + 1; i < col2; i++) {
        if (board[row][i] != 0) {
            return false;
        }
    }
    return true;
}
           

就是寻找某一行的两列之间有没有数字为0,遍历一遍就行。再回到moveLeft函数,此处我们给移动的动作添加了一个动画,代码如下

function showMoveAnimation(fromx, fromy, tox, toy) {
    var numberCell = $("#number_cell_" + fromx + "_" + fromy);
    numberCell.animate({
        "top": getPosTop(tox, toy),
        "left": getPosLeft(tox, toy)
    }, 200);
}
           

就是简单的用到了四个坐标值而已,然后添加了200ms的时间来完成该动画。

再回到moveLeft函数,添加完动画后,将board[i][k]处的数字由0变成board[i][j]的数字,并且board[i][j]自己的数字变为0 。

然后当board[i][k]处的数字等于board[i][j]的数字时,照样先添加一个动画,然后board[i][k]处的数字加上board[i][j]的数字,board[i][j]的数字也变为0,分数同时也会叠加,并且分数要动态地传回score中,所以此处定义了一个函数,代码如下

function updateScore(score) {
    $("#score").text(score);
}
           

再回到moveLeft函数,hasConflicted后面再讲,我们之前讲过,每做一次操作就需要调用updateBoardView函数,所以在此处循环完后,调用一次该函数,此处设置了一个200ms的setTimeout,是因为我们在showMoveAnimation函数中给动画添加的完成时间是200ms,如果我们不给updateBoardView添加一个200ms的延迟的话,就会发现动画还没完成,数字就已经显示出来了。

至此,moveLeft就已经讲完了,后面的moveRight,moveTop,moveDown大家可以自己试着写一下,就不细讲了,此处我给出moveRight的代码

function moveRight() {
    if (canMoveRight(board)) {
        for (var i = 0; i < 4; i++) {
            for (var j = 2; j >= 0; j--) {
                if (board[i][j] != 0) {
                    for (var k = 3; k > j; k--) {
                        if (board[i][k] == 0 && noBlockHorizontal(i, j, k, board)) {
                            showMoveAnimation(i, j, i, k);
                            board[i][k] = board[i][j];
                            board[i][j] = 0;
                            continue;
                        } else if (board[i][k] == board[i][j] && noBlockHorizontal(i, j, k, board) && !hasConflicted[i][k]) {
                            showMoveAnimation(i, j, i, k);
                            board[i][k] += board[i][j];
                            board[i][j] = 0;
                            score += board[i][k];
                            updateScore(score);
                            hasConflicted[i][k] = true;
                            continue;
                        }
                    }
                }
            }
        }
        setTimeout("updateBoardView()", 200);
        return true;
    } else {
        return false;
    }
}
           

此处有一个地方稍微需要思考一下,为什么在第二个for循环处j是从大往小写,而不是跟moveLeft处一样从小到大遍历?

答案揭晓!如果此处是从小到大遍历的话,我们玩的时候会发现一个问题,当左边两列或者三列有数字时,这时我们按右键,会发现最左边的那一列不会右移,为什么呢?因为j从0开始循环的话,算法会发现最左边一列的右边有数字,无法右移,所以右移的算法应该要先从最右边开始遍历,等右边的数字移动完了再移动左边的。

同理,下面的那个定义k的for循环也就得跟着从大往小变化。

当其他的几个操作编写完之后,我们在键盘keydown事件中还编写了一个isgameover函数,此时来定义它,代码如下

function isgameover() {
    if (nospace(board) && nomove(board)) {
        alert("game over"); //当没有空位且不能移动时游戏结束
    }
}
           

什么时候会gameover呢?当所有的格子都有数字且都不能移动时,游戏就结束了,我这里就简答地写了一个弹窗,nomove函数的代码如下

function nomove(board) {
    if (canMoveLeft(board) || canMoveRight(board) || canMoveTop(board) || canMoveDown(board)) {
        return false;
    }
    return true;
}
           

我们之前已经定义了判断是否可以左移右移上移下移的函数,这里直接调用就行。

现在理论上是可以玩了,但是我们玩的时候还会发现一个问题。例如现在某一行数字是这样的,2,2,4,8,这时我们按左移的话,如果是原版2048,这里会变成4,4,8,0。但是我们编写的程序会直接变成16,0,0,0。所以我们得编写一个功能让某个格子在一次操作时该格子的数字只能变化一次。

就是我们之前已经出现过的hasConflicted,当时我是说后面再讲,首先我们先定义一个空数组

由于它也是每个格子都有的属性,所以也得把它变成一个二维数组,这是newgame函数的代码

// 初始化
function newgame() {
    for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 4; j++) {
            var gridCell = $("#grid_cell_" + i + "_" + j); // 定义变量gridCell来编写每个格子的位置
            gridCell.css("top", getPosTop(i, j));
            gridCell.css("left", getPosLeft(i, j)); //只需定义格子的左上角即可
        }
    }

    for (var i = 0; i < 4; i++) {
        board[i] = [];
        hasConflicted[i] = [];
        for (var j = 0; j < 4; j++) {
            board[i][j] = 0; //把board定义为二维数组来更方便地表达出每个格子,board为0即这个格子没有数字,不为0即有数字
            hasConflicted[i][j] = false; //初始化时需要将每个格子是否冲突的属性定义为false
        }
    }

    updateBoardView(); //定义此函数来将数字放到该格子中

    generateOneNumber(); //每次初始化游戏都会随机产生两个数字
    generateOneNumber();

    score = 0; //初始化
    updateScore(score);
}
           

初始化当然要将它的属性设为false,也就是这个格子还没有变化过,然后我们为了防止上述问题的出现,就得在每次操作时加上一个判定

function moveLeft() {
    if (canMoveLeft(board)) {
        for (var i = 0; i < 4; i++) {
            for (var j = 1; j < 4; j++) {
                if (board[i][j] != 0) {
                    for (var k = 0; k < j; k++) {
                        if (board[i][k] == 0 && noBlockHorizontal(i, k, j, board)) { //寻找可以移动到的位置board[i][k]
                            showMoveAnimation(i, j, i, k); //添加移动时的动画
                            board[i][k] = board[i][j];
                            board[i][j] = 0;
                            continue;
                        } else if (board[i][k] == board[i][j] && noBlockHorizontal(i, k, j, board) && !hasConflicted[i][k]) {
                            showMoveAnimation(i, j, i, k);
                            board[i][k] += board[i][j];
                            board[i][j] = 0;
                            score += board[i][k]; //分数叠加
                            updateScore(score); //将分数动态传回#score
                            hasConflicted[i][k] = true; //每一次移动后都要将该位置是否存在冲突的属性设为true
                            continue;
                        }
                    }
                }
            }
        }
        setTimeout("updateBoardView()", 200); //由于每一次操作,board都会发生变化,所以每一次操作都要调用updateBoardView()
        return true;
    } else {
        return false;
    }
}
           

可以看到在else if中最后还加了一个!hasConflicted[i][k],意思是该位置还没有变化过,可以移动,然后将它的属性设为true,这时该位置在这一次循环时就已经不能再变化了,这时上述问题就已经解决好了。那么在这次操作完后在哪将它的属性由true变回false呢?我们不是调用了一个updateBoardView函数了嘛,在这里将它设置回去就可以了,一定要记得设置回去,不然下次操作这个位置就不能变化了。

function updateBoardView() {
    $(".number_cell").remove(); //由于每一次操作都要调用该函数,所以每次调用之前都要先将上次操作后产生的数字清除
    for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 4; j++) {
            $("#grid_container").append("<div class='number_cell' id='number_cell_" + i + "_" + j + "'>"); //给每个格子添加数字
            var theNumberCell = $("#number_cell_" + i + "_" + j); //定义该变量来编写每个格子中数字的css样式
            if (board[i][j] == 0) {
                theNumberCell.css("width", "0"); //当该位置没有数字时,宽高设为0就行
                theNumberCell.css("height", "0");
                theNumberCell.css("top", getPosTop(i, j));
                theNumberCell.css("left", getPosLeft(i, j));
            } else {
                theNumberCell.css("width", "100px");
                theNumberCell.css("height", "100px");
                theNumberCell.css("top", getPosTop(i, j));
                theNumberCell.css("left", getPosLeft(i, j));
                theNumberCell.css("background-color", getNumberBackgroundColor(board[i][j])); //定义该函数来存放不同数字的背景颜色
                theNumberCell.css("color", getNumberColor(board[i][j])); //定义该函数来存放不同数字的颜色
                theNumberCell.text(board[i][j]); //将数字值赋予给相应的格子
            }
            hasConflicted[i][j] = false; //由于每一次操作都要调用该函数,所以每次调用之前都要先将每个格子是否冲突的属性定义为false
        }
    }
}
           

然后我们之前还有个地方没讲,之前讲键盘keydown事件时还设置了两个setTimeout

$(document).keydown(function (event) {
    switch (event.keyCode) { //键盘敲击事件,此处为keyCode不是keycode
        case 37: //左
            if (moveLeft()) {
                setTimeout("generateOneNumber()", 210); //先要判断是否能够向左动,向左动了后会随机产生一个数字,并判断游戏是否结束
                setTimeout("isgameover()", 300);
            }
            break;
        case 38: //上
            if (moveTop()) {
                setTimeout("generateOneNumber()", 210);
                setTimeout("isgameover()", 300);
            }
            break;
        case 39: //右
            if (moveRight()) {
                setTimeout("generateOneNumber()", 210);
                setTimeout("isgameover()", 300);
            }
            break;
        case 40: //下
            if (moveDown()) {
                setTimeout("generateOneNumber()", 210);
                setTimeout("isgameover()", 300);
            }
            break;
        default:
            break;
    }
})
           

这里不加setTimeout的话,会导致已经弹窗游戏结束了,最后一个数字还没有生成,所以给它加一个时间,分别设置为210ms和300ms,这时就大功告成了。

最后再添加一个返回上一步Undo的操作。思路就是将每一步的数据(每个格子的值和得分)存入数组中,需要时读取出来即可。

首先定义两个空数组

let preValue = [];//存放每一步时每个格子的值
let preScore = [];//存放每一步时的得分
           

然后记录下每一步的数据。我是在每一次移动的函数中,在移动操作之前先读取当前位置的数据,例如下面这段代码就是在moveLeft函数的开头

for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
        preValue.push(board[i][j]);
    }
}
preScore.push(score);
           

然后在moveRight(),moveDown()和moveTop()的开头都加上这一段。之后就是书写undo函数。由于数据是倒着进数组的,所以读取时也要倒着读。其实这就是一个栈,后进先出。

function undo() {
    let obj = {};
    for (let i = 0; i < 4; i++) {
        for (let j = 0; j < 4; j++) {
            if (obj[board[i][j]]) {
                obj[board[i][j]]++;
            } else {
                obj[board[i][j]] = 1;
            }
        }
    }
    for (let key in obj) {
        if (key == 0 && obj[key] == 14) { //当场上只有两个格子有数字即newgame时,按下undo无反应
            return false;
        }
    }
    for (let i = 3; i >= 0; i--) {
        for (let j = 3; j >= 0; j--) {
            board[i][j] = preValue.pop();
        }
    }
    score = preScore.pop();
    updateBoardView();
    updateScore(score);
}
           

至此所有功能已经书写完。

代码已经上传到gitee上了,请自取:

https://gitee.com/azsx582/game-2048.git

第一次写这么长的博客,废话有点多,观众老爷们凑合着看吧。有错误的话欢迎大家指出来,共同进步!

继续阅读