写在前面
2048是一个不错的练手项目,因为它涵盖了不少js,css的知识,同时也包含了简单的游戏逻辑,这个项目的完成对我来说收获颇丰。他让我见见理解了游戏的构成,和一些看似简单的东西复杂的一面。我会虚心接纳各位的建议,对不足之处进行修改。
HTML的准备:
首先要让我们的游戏看起来具有一定的观赏性,所以前端的页面也是很重要的一部分。我这里采用的是手机打开时显示的页面大小
那么我们应该对自己的目标界面有一个整体的估计,他大概是什么样子,布局是什么样的,这样我们在编写前端页面时会更加得心应手。
我记得最早在高中时和同桌一起在课上玩的那一款2048应该是比较简洁的,而且画面让人看起来很舒服,加上自己的想法大概如图:
其实这也就是2048的全部内容了,关于页面的总体布局我们可以任意选择,但数字盒的div必须要使用绝对定位,这关乎着我们对整个游戏的布局操作。
在开始编写html之前我们至少应该确定的是数字盒子会有4*4的排布,而承接他的框架就应该是接近正方形的(为了美观)那么就会有了如下的页面
为了让我们的游戏看起来更立体我希望每一个格子后面都有一个背景板,这样会让元素的移动变得更生动:这里重复创建了16个白色的背景板重叠在一起,之后我们会用js让他们有序排列起来这样做的目的其实也与数字盒子的摆放位置有联系。
html代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="css/mycss.css">
<script src="js/gamerules.js"></script>
<title></title>
</head>
<body>
<div class="sumblock">
<div class="myheader">
<div class="textfont">2048</div>
<div class="buttonclick" onclick="change()">restart</div>
<div class="textiner">当前分数:</div>
<div class="textiner" id="score">0</div>
</div>
<div class="blackblock"></div>
<div class="mybody">
<div class="mypane" id="pane">
<div class="numberbox" id="box-0-0"></div>
<div class="numberbox" id="box-0-1"></div>
<div class="numberbox" id="box-0-2"></div>
<div class="numberbox" id="box-0-3"></div>
<div class="numberbox" id="box-1-0"></div>
<div class="numberbox" id="box-1-1"></div>
<div class="numberbox" id="box-1-2"></div>
<div class="numberbox" id="box-1-3"></div>
<div class="numberbox" id="box-2-0"></div>
<div class="numberbox" id="box-2-1"></div>
<div class="numberbox" id="box-2-2"></div>
<div class="numberbox" id="box-2-3"></div>
<div class="numberbox" id="box-3-0"></div>
<div class="numberbox" id="box-3-1"></div>
<div class="numberbox" id="box-3-2"></div>
<div class="numberbox" id="box-3-3"></div>
</div>
<div class="over" id="gameover">you lose</div>
</div>
</div>
</body>
</html>
css代码如下:
.sumblock{
background-color: #ffffff;
width: 980px;
height: 2040px;
display: flex;
flex-direction: column;
}
.blackblock{
height: 5px;
width: 980px;
background-color: #CCCCCC;
}
.myheader{
width: 980px;
height: 1023px;
background-color: #FEFFF5;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
.mybody{
width: 980px;
height:1010px;
background-color: #E0E0E0;
display: flex;
justify-content: space-around;
}
.buttonclick{
width: 250px;
border-radius: 9px;
height: 120px;
left: 700px;
top: 470px;
background: #F7722A;
opacity: 0.8;
font-size: 4.5em;
text-align: center;
align-items: center;
}
.textiner{
font-size: 2.5em;
font-weight: normal;
}
.textfont{
display: flex;
align-items: center;
text-align: center;
font-size: 12em;
line-height: ;
}
.mypane{
width: 966px;
height: 1000px;
background: #E8E8E8;
border-radius: 24px;
position:relative;
}
.numberbox{
width: 180px;
height: 180px;
background-color: #f7f7f7;
border-radius: 12px;
position: absolute;
}
.over{
background-color:black;
color: white;
font-family:Arial, Helvetica, sans-serif;
font-size:60px;
position: absolute;
top:45%;
left:42%;
width:250px;
text-align:center;
display: none;
}
.realnumberbox{
width: 180px;
height: 180px;
border-radius: 12px;
font-size: 5.5em;
font-weight: bold;
text-align: center;
align-items: center;
line-height: 180px;
position: absolute;
}
接下来是js部分:
我从这个小项目上学到的很重要的一点便是面向对象编程的一个重点,那便是许多功能事先已经编写好了,在使用时,我们只需要关注功能的运用,而不需要这个功能的具体实现过程。我们通过获得第一块砖所在位置的Left值与Top值来计算其他所有砖块的位置
从图中不难看出来如果想要排列好每一个砖块只需要通过序号将偏移量与坐标相乘得到:
如下:
同时利用循环获取16个背景砖并排列:
便产生如下效果:
接下来我们来做一些准备工作,让数字随机生成在棋盘上:
关于这部分的思路大家都可以想的明白,我们实现一个2048游戏需要用到的数据结构便是二维数组,很直观的表现在游戏上。一个用来存放数字,另一个用来查看当前物体的移动方向上的状况。
而在每一次移动数字盒子时,为了实时更新页面的状况使用了自定义的updateCss方法进行更新
每次移动时都会进行判断是否可达
js部分:
var content = new Array();
var sum = new Array()
var score = 0;
function change() {
start()
score=0
}
window.onload = function() {
initialize()
randomSpace()
randomSpace()
slider()
}
function start(){
initialize()
randomSpace()
randomSpace()
}
function getScore() {
var sc = document.getElementById("score")
sc.textContent = score;
}
function setBackground(){
//排列背景砖
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
var cell = document.getElementById("box-" + i + "-" + j)
cell.style.left = plefts(i, j)+"px"
cell.style.top = ptops(i, j)+"px"
}
}
}
//初始化函数
function initialize() {
//移除游戏结束状态
document.getElementById("gameover").style.display = 'none'
//排列背景方块
setBackground();
//初始化数组
for (var i = 0; i < 4; i++) {
content[i] = new Array()
for (var j = 0; j < 4; j++) {
content[i][j] = 0;
}
}
for (var i = 0; i < 4; i++) {
sum[i] = new Array()
for (var j = 0; j < 4; j++) {
sum[i][j] = 0;
}
}
updateCss()
}
function updateCss() {
//移除原有的number盒子
var child = document.getElementsByClassName("realnumberbox")
child.removeNode = [];
if (child.length != undefined) {
var len = child.length;
for (var i = 0; i < len; i++) {
child.removeNode.push({
parent: child[i].parentNode,
inner: child[i].outerHTML,
next: child[i].nextSibling
});
}
for (var i = 0; i < len; i++) {
child[0].parentNode.removeChild(child[0]);
}
} else {
child.removeNode.push({
parent: child.parentNode,
inner: child.outerHTML,
next: child.nextSibling
});
child.parentNode.removeChild(child);
}
//前端更新页面,将数字盒子填满16格
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
var numbercell = document.getElementById("pane")
var node = document.createElement("div")
node.className = "realnumberbox"
node.id = "numberbox-" + i + "-" + j
if (content[i][j] == 0) {
node.style.height = "0px";
node.style.width = "0px";
node.style.left = plefts(i, j)+90+"px"
node.style.top = ptops(i, j)+90+"px"
} else {
node.style.height = "180px"
node.style.weight = "180px"
node.style.backgroundColor = jamColor(content[i][j])
var textnode = document.createTextNode(content[i][j])
node.appendChild(textnode)
node.style.left = plefts(i, j)+"px"
node.style.top = ptops(i, j)+"px"
}
numbercell.appendChild(node)
}
}
}
//随机位置生成2或4
function randomSpace() {
if (nospace(content))
return false;
var randomx = Math.floor(Math.random() * 4)
var randomy = Math.floor(Math.random() * 4)
while (true) {
//如果选中位为零则退出循环
if (content[randomx][randomy] == 0)
break;
//否则继续选取
randomx = Math.floor(Math.random() * 4)
randomy = Math.floor(Math.random() * 4)
}
var num = Math.random() < 0.5 ? 2 : 4;
content[randomx][randomy] = num;
printRealNumber(randomx, randomy, num)
return true;
}
function printRealNumber(x, y, realnumber) {
var numberbox = document.getElementById("numberbox-" + x + "-" + y)
numberbox.style.backgroundColor = jamColor(realnumber)
numberbox.textContent = realnumber + ""
//生成数字动画
clearInterval(numberbox.timer);
numberbox.timer = setInterval(function() {
numberbox.style.width = numberbox.offsetWidth + Math.ceil(180 / 10) + "px"
numberbox.style.height = numberbox.offsetHeight + Math.ceil(180 / 10) + "px"
numberbox.style.top = numberbox.offsetTop- Math.ceil(90/10)+"px"
numberbox.style.left = numberbox.offsetLeft -Math.ceil(90/10)+"px"
if (numberbox.offsetHeight == 180 && numberbox.offsetWidth == 180 && numberbox.offsetTop==ptops(x,y) && numberbox.offsetLeft == plefts(x,y)) { //如果到了就清除计时器
clearInterval(numberbox.timer);
}
}, 24);
}
//旗帜二维数组的重置
function sumArray() {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
sum[i][j] = 0
}
}
}
//判断是否可以左移
function judgeL(content) {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (content[i][j] != 0 && j != 0) {
if (content[i][j - 1] == 0 || content[i][j - 1] == content[i][j])
return true
}
}
}
return false;
}
//判断是否可以右移
function judgeR(content) {
for (var i = 0; i < 4; i++)
for (var j = 0; j < 4; j++)
if (content[i][j] != 0 && j != 3)
if (content[i][j + 1] == 0 || content[i][j + 1] == content[i][j])
return true;
return false;
}
//判断是否可以上移
function judgeU(content) {
for (var i = 0; i < 4; i++)
for (var j = 0; j < 4; j++)
if (content[i][j] != 0 && i != 0)
if (content[i - 1][j] == 0 || content[i - 1][j] == content[i][j])
return true;
return false;
}
//判断是否可以下移
function judgeD(content) {
for (var i = 0; i < 4; i++)
for (var j = 0; j < 4; j++)
if (content[i][j] != 0 && i != 3)
if (content[i + 1][j] == 0 || content[i + 1][j] == content[i][j])
return true;
return false;
}
//判断水平方向是否有障碍物
function haveStoneX(row, col1, col2, content) {
for (var i = col1 + 1; i < col2; i++) {
if (content[row][i] != 0) {
return false;
}
}
return true;
}
//判断垂直方向是否有障碍物
function haveStoneY(col, row1, row2, content) {
for (var i = row1 + 1; i < row2; i++) {
if (content[i][col] != 0) {
return false
}
}
return true
}
//实现方块移动动画
function animation(x1, y1, x2, y2) {
var numberbox = document.getElementById("numberbox-" + x1 + "-" + y1)
//移动只有垂直与水平移动
if (x1 == x2) {
//水平移动时分向左和向右
//判断方向
var direction = y1 > y2 ? -1 : 1
clearInterval(numberbox.timer)
numberbox.timer = setInterval(function() {
numberbox.style.left = numberbox.offsetLeft + direction * Math.ceil(Math.abs((numberbox.offsetLeft -
plefts(x2, y2)) / 10)) + "px"
if (numberbox.offsetLeft == pleft(x2, y2)) { //如果到了就清除计时器
clearInterval(numberbox.timer);
}
}, 24);
} else {
//判断方向
var direction = x1 > x2 ? -1 : 1
clearInterval(numberbox.timer)
numberbox.timer = setInterval(function() {
numberbox.style.top = numberbox.offsetTop + direction * Math.ceil(Math.abs((numberbox.offsetTop -
ptops(x2, y2)) / 10)) + "px"
if (numberbox.offsetTop == ptop(x2, y2)) { //如果到了就清除计时器
clearInterval(numberbox.timer);
}
}, 24);
}
}
//左移动作
function moveLeft() {
//首先判断是否可以进行左移
if (!judgeL(content))
return false;
sumArray();
//其次进行循环遍历可以移动的格子
for (var i = 0; i < 4; i++) {
for (var j = 1; j < 4; j++) {
if (content[i][j] != 0) {
for (var k = 0; k < j; k++) {
//左侧为零且没有障碍物的格子移动方式
if (content[i][k] == 0 && haveStoneX(i, k, j, content)) {
animation(i, j, i, k)
content[i][k] = content[i][j]
content[i][j] = 0;
continue;
} else if (content[i][k] == content[i][j] && haveStoneX(i, k, j,
content)) { //左侧与定位元素相同且中间没有障碍物的格子移动方式
animation(i, j, i, k);
if (sum[i][k] != 0) {
content[i][k + 1] = content[i][j]
content[i][j] = 0
} else { //可以合并
content[i][k] += content[i][j]
content[i][j] = 0;
sum[i][k] = 1;
score += content[i][k]
}
continue
}
}
}
}
}
setTimeout("updateCss()", 200)
return true;
}
//右移动作
function moveRight() {
if (!judgeR(content))
return false;
sumArray()
for (var i = 0; i < 4; i++) {
for (var j = 3; j >= 0; j--) {
if (content[i][j] != 0) {
for (var k = 3; k > j; k--) {
if (content[i][k] == 0 && haveStoneX(i, j, k, content)) {
animation(i, j, i, k)
content[i][k] = content[i][j]
content[i][j] = 0;
continue;
} else if (content[i][k] == content[i][j] && haveStoneX(i, j, k, content)) {
animation(i, j, i, k);
if (sum[i][k] != 0) {
content[i][k - 1] = content[i][j]
content[i][j] = 0
} else {
content[i][k] += content[i][j]
content[i][j] = 0;
sum[i][k] = 1;
score += content[i][k]
}
continue
}
}
}
}
}
setTimeout("updateCss()", 200)
return true;
}
//上移动作
function moveUp() {
if (!judgeU(content))
return false
sumArray()
for (var i = 1; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (content[i][j] != 0) {
for (var k = 0; k < i; k++) {
if (content[k][j] == 0 && haveStoneY(j, k, i, content)) {
animation(i, j, k, j)
content[k][j] = content[i][j]
content[i][j] = 0
continue
} else if (content[k][j] == content[i][j] && haveStoneY(j, k, i, content)) {
animation(i, j, k, j)
if (sum[k][j] != 0) {
content[k + 1][j] = content[i][j]
content[i][j] = 0
} else {
content[k][j] += content[i][j]
content[i][j] = 0;
sum[k][j] = 1;
score += content[k][j]
}
continue
}
}
}
}
}
//更新样式
setTimeout("updateCss()", 200)
return true;
}
//下移动作
function moveDown() {
if (!judgeD(content))
return false
sumArray()
for (var i = 2; i >= 0; i--) {
for (var j = 0; j < 4; j++) {
if (content[i][j] != 0) {
for (var k = 3; k > i; k--) {
if (content[k][j] == 0 && haveStoneY(j, i, k, content)) {
animation(i, j, k, j)
content[k][j] = content[i][j]
content[i][j] = 0
continue
} else if (content[k][j] == content[i][j] && haveStoneY(j, i, k, content)) {
animation(i, j, k, j)
if (sum[k][j] != 0) {
content[k - 1][j] = content[i][j]
content[i][j] = 0
} else {
content[k][j] += content[i][j]
content[i][j] = 0;
sum[k][j] = 1;
score += content[k][j]
}
continue
}
}
}
}
}
//更新样式
setTimeout("updateCss()", 200)
return true;
}
//是否可以进行移动
function cantMove(content) {
if (judgeL(content) || judgeR(content) || judgeU(content) || judgeD(content))
return false
return true;
}
//游戏结束
document.addEventListener("keydown", keydown);
//参数1:表示事件,keydown:键盘向下按;参数2:表示要触发的事件
function keydown(event) {
//表示键盘监听所触发的事件,同时传递参数event
switch (event.keyCode) {
case 37: //Left
if (moveLeft()) {
getScore()
randomSpace()
setTimeout("isgameover()", 400);
}
break;
case 38: //Up
if (moveUp()) {
getScore()
randomSpace()
setTimeout("isgameover()", 400);
}
break;
case 39: //Right
if (moveRight()) {
getScore()
randomSpace()
setTimeout("isgameover()", 400);
}
break;
case 40: //Down
if (moveDown()) {
getScore()
randomSpace()
setTimeout("isgameover()", 400);
}
break;
}
}
//判断游戏结束
function isgameover() {
if (nospace(content) && cantMove(content))
gameover();
}
//游戏结束弹窗
function gameover() {
$("#gameover").css('display', 'block');
}
//返回当前元素所在的left值
function pleft(i, j) {
var a = 35+ j * 240;
return a + "px"
}
//返回当前元素所在的top值
function plefts(i, j) {
var a = 35 + j * 240;
return a
}
//返回当前元素所在的right值(数值型)
function ptop(i, j) {
var a = 40 + i * 240;
return a + "px"
}
//返回当前元素所在的top值(数值型)
function ptops(i, j) {
var a = 40 + i * 240;
return a
}
//判断是否存在空格子
function nospace(content) {
for (var i = 0; i < 4; i++)
for (var j = 0; j < 4; j++)
if (content[i][j] == 0)
return false;
return true;
}
//为每个数字配置固定颜色
function jamColor(number) {
var color = null;
switch (number) {
case 2:
color = "#cbced8"
break;
case 4:
color = "#50ba46"
break;
case 8:
color = "#5071ec"
break;
case 16:
color = "#a14ddc"
break;
case 32:
color = "#ee7e0e"
break;
case 64:
color = "#eb351d"
break;
case 128:
color = "#ee9495"
break;
case 256:
color = "#8d7fdd"
break;
case 512:
color = "#79dcc7"
break;
case 1024:
color = "#b8ee8b"
break;
case 2048:
color = "#e23c09"
}
return color
}
其中让我印象深刻的是关于方块移动的动画
关于这部分我认为是动画很基础的一部分,但是值得思考。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<title>动画测试</title>
</head>
<body>
<div id='block' style="width: 180px; height: 180px; background-color: cornflowerblue;position:relative;"></div>
<input type="button" value="click" onclick="move()">
<script>
function move(){
var tr=document.getElementById("block")
var target=500
clearInterval(tr.timer); //保证元素此动画计时器不重复
var dir=tr.offsetLeft<target ? 1 : -1; //确定运动方向
tr.timer=setInterval(function(){
tr.style.left=tr.offsetLeft+dir*Math.ceil(Math.abs(tr.offsetLeft-target)/10)+"px"; //offset为整数,移动时偏移量会小于1,所以当偏移量小于1时用Math.ceil将其改为1,直至达到目标
if(tr.offsetheight==380 && tr.offsetWidth==380){ //如果到了就清除计时器
clearInterval(tr.timer);
}
},24);
}
</script>
</body>
</html>
动画的本质就是像素的移动和变换,而做到2048内的滑块移动并不困难,只要让物体在一定时间,每个时间段进行一段移动就可以做到,而关于本次项目的移动我选择了匀速运动,因为匀减速运动会让你的物块移动起来有些别扭,这两种方法的实现大同小异。由于我们使用了一个时间计时器,每隔一定时间就会运行一次,所以时间是固定的,根据公式V=x/t,如果要让我们的物块运动起来是匀速,那么每次的移动距离就需要是常量。而匀加速和匀减速就是根据需要递增和递减位移的偏移量。最后在到达目的点删除动画。
关于JS实现触摸滑动,借鉴了一些网上的代码,本质上很好理解,在body上注册一个事件检测触摸事件,当触摸一个方向达到阈值时即判定触发。
function slider(){
let box = document.querySelector('body')// 监听对象
let startTime = '' // 触摸开始时间
let startDistanceX = '' // 触摸开始X轴位置
let startDistanceY = '' // 触摸开始Y轴位置
let endTime = '' // 触摸结束时间
let endDistanceX = '' // 触摸结束X轴位置
let endDistanceY = '' // 触摸结束Y轴位置
let moveTime = '' // 触摸时间
let moveDistanceX = '' // 触摸移动X轴距离
let moveDistanceY = '' // 触摸移动Y轴距离
box.addEventListener("touchstart", (e) => {
startTime = new Date().getTime()
startDistanceX = e.touches[0].screenX
startDistanceY = e.touches[0].screenY
})
box.addEventListener("touchend", (e) => {
endTime = new Date().getTime()
endDistanceX = e.changedTouches[0].screenX
endDistanceY = e.changedTouches[0].screenY
moveTime = endTime - startTime
moveDistanceX = startDistanceX - endDistanceX
moveDistanceY = startDistanceY - endDistanceY
// 判断滑动距离超过40 且 时间小于500毫秒
if ((Math.abs(moveDistanceX) > 40 || Math.abs(moveDistanceY) > 40) && moveTime < 500) {
// 判断X轴移动的距离是否大于Y轴移动的距离
if (Math.abs(moveDistanceX) > Math.abs(moveDistanceY)) {
// 左右
if(moveDistanceX > 0) {
if (moveLeft()) {
getScore()
randomSpace()
setTimeout("isgameover()", 400);
}
} else{
if (moveRight()) {
getScore()
randomSpace()
setTimeout("isgameover()", 400);
}
}
} else {
// 上下
if(moveDistanceY > 0){
if (moveUp()) {
getScore()
randomSpace()
setTimeout("isgameover()", 400);
}
}else{
if (moveDown()) {
getScore()
randomSpace()
setTimeout("isgameover()", 400);
}
}
}
}
})
}