2017-12-23
最近没找到喜欢的项目视频,准备自己写点什么看看到底学得如何,偶然间发现了别人写的贪吃蛇和2048,觉得写个这玩意儿也可以当做小练手.于是在扔了10次硬币之后选择了自己做个2048出来.这里是成品地址
做游戏嘛,首先要搞明白规则,百度上说2048是一位叫Gabriele Cirulli的大佬写的,便去github上顺藤摸瓜找到了项目主页:2048.玩了一会觉得这种游戏可能不适合自己,最高玩到1024就死掉了...
2048的游戏规则就是可以通过方向键/按钮/念力来让所有方块向同一方向移动,如果遇到相同的方块就把两个方块上的数字相加生成一个新方块,每次移动/相加都会在随机空位置生成一个值为2的方块.
了解游戏规则之后就方便多了,无非就是两种操作: 移动(Move)和结合(Merge).可以把2048看成4*4的矩阵,没有数字的位置就是0,那么移动方块就是将它与边上的0互换位置,以左移为例的话,MoveLeft()
应该是从方块A[i]j开始向左寻找非0位置B[i]k,找到后如果B[i]k和A[i]j相等,则B[i]k*2,A[i]j=0,否则将方块A移动到B[i]k的右边[i]k+1处,同时A[i]j=0.
落实到代码上就是这样:
var moveLeft = function () { //左移
for (var i = 0; i < len; i++) {
for (var j = 1; j < len; j++) { //从[0][1]开始遍历数组,因为是左移,最左面一行不用检查
if (board[i][j] != 0) { //遇见非零位[i][j]时
var k = 1;
while (j - k >= 0 && board[i][j - k] === 0) { //从[i][j]向左检查k位
k = k + 1; //若[i][j-k]为0则继续向左检查
}
if (j - k >= 0) { //[i][j-k]依然在数组中
if (board[i][j - k] === board[i][j]) { //相加
board[i][j - k] = board[i][j - k] * 2;
board[i][j] = 0;
} else if (k > 1) { //若检查了不止一位,防止后面[j-k+1]===[j]时实际上没有移动
var tmp = board[i][j]; //将[i][j]移到[i][j-k+1]
board[i][j] = 0;
board[i][j - k + 1] = tmp;
}
} else { //若检查了j位全为0,将[i][j]移到最左面[i][0]
board[i][0] = board[i][j];
board[i][j] = 0;
}
}
}
}
};
同理可以写出右移,上移,下移的函数,区别在于遍历数组的顺序以及检查的方向.
该方法目前有BUG,会导致0224进行左移的时候直接变为8000,不会一步一步的结合.
核心代码的第二部分就是在随机位置生成方块2,这里采用的方法有两种:
1.利用Math.random()
方法生成两个随机数(i,j)作为随机位置的坐标[i]j,检查该位置是否为0,为0则[i]j=2,否则调用自身重新生成随机数i,j.这种方法就是逻辑上比较简单,但是明显能看出还是有很多缺点,比如若数组已满,要判断至少16次才能确定,以及判断数组已满的方法又是两次循环.所以这里采用第二种方法生成随机坐标.
var randNum = function () {
var ranRow = Math.floor(Math.random() * 4);
var ranCol = Math.floor(Math.random() * 4);
if (board[ranRow][ranCol] != 0) {
if (isFull()) {
return;
} else {
randNum();
}
} else {
board[ranRow][ranCol] = 2;
}
return;
}
2.遍历数组,生成一个包含所有0坐标的新数组list,比较list的长度与所要生成随机数坐标的个数n,若list.length
小于坐标个数则将list中每个坐标所对应的原数组的值设为2,否则在新数组中随机抽取n个元素作为生成随机数的坐标.这种方法的好处在于计算次数少,检查数组已满的方式简单,并且可以自定义新随机数的个数.
var generateNewNumbers = function (increasement) {
var num = increasement || 1;
var list = new Array();
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (board[i][j] === 0) {
list.push({
posX: i,
posY: j
});
}
}
};
if (list.length < num) {
for (var i = 0; i < list.length; i++) {
var x = list[i].posX;
var y = list[i].posY;
board[x][y] = 2;
}
} else {
for (var i = 0; i < num; i++) {
var element = Math.floor(Math.random() * list.length);
var x = list[element].posX;
var y = list[element].posY;
board[x][y] = 2;
list.splice(element,1);
}
}
};
这样,两个核心的函数就完成了.
游戏嘛,当然要有失败和胜利.因此我们有两个函数isWon()
和isDead()
.胜利好说,遍历数组如果有2048的话就判定胜利,失败的话则要判定数组中已经没有位置生成新方块,且每个元素的上下左右都没有相同项可以合并.这里我们给出isDead()的实现:
var isDead = function () {
for (var i = 0; i < len; i++) {
for (var j = 0; j < len; j++) {
if (board[i][j] === 0) {
return false;
}
}
}
for (var i = 0; i < len; i++) {
for (var j = 0; j < len; j++) {
if (i - 1 >= 0 && board[i][j] === board[i - 1][j]) {
return false;
} else if (i + 1 < len && board[i][j] === board[i + 1][j]) {
return false;
} else if (j - 1 >= 0 && board[i][j] === board[i][j - 1]) {
return false;
} else if (j + 1 < len && board[i][j] === board[i][j + 1]) {
return false;
}
}
}
return true;
};
这样,代码逻辑方面就完成了.接下来开始进行动画制作.
在开始写2048之前,我以为代码逻辑是最难的...然而在写完代码逻辑之后我发现原来做页面动画才是最痛苦的.动画的关键在于,如何在动画时间与HTML元素变化之间找到平衡,比如结合(Merge()
)会在同一位置产生三个元素,该在什么时候清理冗余元素成了个大问题.
最早的时候我准备用setTimeout()
来进行延迟操作,但是多个setTimeout()会在全部代码执行完毕后再一起执行,造成了逻辑上的混乱,便换了其他方法.
方块移动和结合的动画我放到了同一个函数animateMove()
里,通过向函数传入五个参数:起始i坐标,起始j坐标,偏移量k,是否结合merge,以及移动方向ifVertical来确定移动动画的具体形式.方块移动可以视为把A的坐标改成B的坐标,利用CSS的transition-duration来实现移动动画,结合则通过.append()
方法在HTML元素后添加一个方块,利用@keyframes
来实现新方块的出现动画.
var animateMove = function (fromX, fromY, k, merge, ifVertical) { //k终点坐标
var para = ifVertical || 0 //是否是垂直移动
if (para) { //垂直移动
$(".tile-position-" + fromX + "-" + fromY).removeClass("tile-position-" + fromX + "-" + fromY).addClass("tile-position-" + k + "-" + fromY);
if (merge) {
setTimeout(function () {
animateMerge(k, fromY);
}, 200);
}
} else { //水平移动
$(".tile-position-" + fromX + "-" + fromY).removeClass("tile-position-" + fromX + "-" + fromY).addClass("tile-position-" + fromX + "-" + k);
if (merge) {
setTimeout(function () {
animateMerge(fromX, k);
}, 200);
}
}
};
var animateMerge = function (val1, val2) {
score = score + 5;
$("#score").text(score);
if (board[val1][val2] != 0) {
$(".tile-container").append("<div class='tile tile-merged tile-position-" + val1 + "-" + val2 + "'><div class='inner-val val-" + board[val1][val2] + "'>" + board[val1][val2] + "</div></div>");
$(".tile-position-" + val1 + "-" + val2).not(".tile-merged").remove();
}
};
当然我们不能一味地在结合的过程中添加元素,那样会造成HTML元素的冗余,有无用元素堆积到页面上.而如何清理这些元素也考验技巧.最初我采用的是在某一时刻重新绘制全部方块,不过重新绘制的时机很难掌握,在快速操作方块移动的时候经常会来不及显示完整的动画.因此我在执行合并动画的1s后将含有"tile-merged"类的元素的"tile-merged"类删除.唔,暂时还没发现什么BUG.
当然可以选择点击按钮操作游戏,不过显然使用方向键更为方便.jQuery的.keydown()
方法很好地实现了这个功能,在按键按下时传入按键的ASCII码,通过不同的传入值来执行不同的操作..preventDefault()
则可以防止按方向键时页面发生滚动.
var keyMonitor = function () {
$(document).keydown(function (event) {
var e = event || window.event;
var k = e.keyCode || e.which;
switch (k) {
case 38: //Up
e.preventDefault();
moveUp();
isTriggered();
break;
case 40: //Down
e.preventDefault();
moveDown();
isTriggered();
break;
case 37: //Left
e.preventDefault();
moveLeft();
isTriggered();
break;
case 39: //Right
e.preventDefault();
moveRight();
isTriggered();
break;
}
});
};