这周断断续续完成了2048小游戏的网页版,这里主要讲这款游戏的逻辑和代码实现。
参考:慕课网2048私人订制 https://www.imooc.com/learn/76
对着视频把源码敲了一遍,这里作一个思路整理。
一、整体思路
1、游戏页面结构及样式
2、2048游戏逻辑
3、手机端优化
思路可以划分为上三步,在第一步中,首先规划好整体的排版。
页面内容包括2048header、游戏开始button、得分字段
、整个4*4方格grid-container。在container中每一个cell后期都要绑定一个number。
二、游戏页面结构及样式
2048,开始、得分
设置text-align:center;margin左右auto居中,字体背景颜色等;
<header>
<h1>2048</h1>
<a href="javascrpit:newgame();">开始</a>
<p>得分</p>
</header>
4*4方格,盒子模型
<div id="grid-container">
<div id="grid-cell-0-0>
</div>
</div>
//grid-cell-0-0到3-3
container设置width、height、background,bord-radius边框,padding,margin、position(relative相对定位,相当于浮动)
grid-cell设置width,height、background-color、position(absolute绝对定位,可以被覆盖)
number-cell覆盖每个grid-cell,,其中的数字为border-number,初使为0
三、2048游戏逻辑
1、newgame包括:init初始化游戏,生成gridcell4*4,number-cell清零,随机两个位置,generateOneNumber两次。
(0)生成页面结构后,加载DOM(文档对象模型)。通过$(document).ready()完成,ready事件在生成html标签、元素,显示CSS样式后实现。在ready()事件中执行newgame函数,该函数链接到《开始》按钮。
(1)init:通过类选择器number-cell $(.number-cell).remove()清零,对grid-container中添加number-cell,number-cell中的数字都存在board【】【】二维数组中。
(2)generateOneNumber中随机生成两个位置,和随机2或4,
board[randx][randy]=randNumber;
2、moveLeft、moveRight、moveUp、moveDown键盘操作。moveLeft发生,所有数字左移,相同数字求和。
(0)按下键盘的↑↓←→由KaTeX parse error: Expected 'EOF', got '&' at position 171: …,board[i][k]!=0&̲&在左移的这段(k~j)距离,…(’#score’).text(score)使得在得分:0处显示分数。
4、game over,判断所有border-number不为零(没有空格),且canMoveLeft等四个函数return false(不能移动)。
四、手机端优化
(0)在手机上与pc端的主要区别:页面大小、控制方式(按键盘和触控)
(1)为了适应手机和pc,加一个获取屏幕大小的函数documentWidth=window.screen.availWidth,if 屏幕宽度大于500像素,认为是PC,grid-container按500像素显示,小于500,按手机来自适应。width和height按%百分比显示。
(2)触控用document.addEventListener监听,包括touchstart、touchmove和touchend。
(3)touchstart获取手指初始位置;touchmove是优化,使得手指点击无效;touchend获取手指离开屏幕的位置,通过xy坐标和滑动产生的位移来判断,执行moveLeft等上下左右四个函数。
注:bug修改
在一排四个格子出现2248这样的情况下,左移结果应该是4480,但是在三、2、(2)中,结果是16000,应该添加hasConflicted二维数组,判断是否加过,保证只加一次。
最后,附上慕课中给的源代码,仅作少量修改,侵删
整体结构:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="
width=device_value,
height=device_value,
initial-scale=1.0,
minimum-scale=0.9,
maxmum-scale=1.0,
<!-- //用户可以缩小或放大屏幕尺寸的最小最大值-->
user-scalable = yes"
/>
<title>Title</title>
<link rel="stylesheet" type="text/css" href="2048.css">
<!-- <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.min.js"></script>-->
<!-- <script type="text/javascript" src="http://libs.baidu.com/jquery/1.9.0/jquery.min.js"></script>-->
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="main2048.js"></script>
<script type="text/javascript" src="showanimation.js"></script>
<script type="text/javascript" src="support2048.js"></script>
</head>
<body>
<header>
<h1>2048</h1>
<a href="javascript:newgame();" id="newgamebutton">开始</a>
<p>得分: <span id="score">0</span> </p>
</header>
<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>
</body>
</html>
<!--MVC结构:Model模型、visual界面UI、Control逻辑控制-->
<!--待优化:-->
<!--1、game over-->
<!--2、最高得分-->
<!--3、加分动画-->
<!--可以通过phoneGap将web APP转换为native app-->
<!--网站发布:dropBox&dropPages-->
<!--或者发布方式:nginx+花生壳,存在nginx中即可。-->
2048.css
header{
display: block;
margin: 0 auto;
width:100%;
text-align: center;
}
header h1{
font-family: Arial;
font-size: 40px;
font-weight:bold ;
}
header #newgamebutton{
display: block;
margin: 20px auto;
width: 100px;
padding:10px 10px;
/*background-color: #9efff4;*/
background-color: chocolate;
font-family: Arial;
color: white;
border-radius: 10px;
text-decoration: none;
}
header #newgamebutton:hover{
background-color: cyan;
}
header p{
font-family:Arial;
font-size: 25px;
margin: 20px auto;
}
#grid-container{
width:460px;
height: 460px;
padding:20px;
/*上下左右各自向外20px,盒子为500*500*/
margin: 50px auto;
/*background-color: #bbada0;*/
background-color: #9efff4;
background-color: rgba(22,27,255,0.9);
border-radius: 10px;
position: relative;
}
.grid-cell{
width: 100px;
height: 100px;
border-radius: 6px;
/*background-color: #ccc0b3;*/
background-color: yellowgreen;
position: absolute;
}
.number-cell{
border-radius:6px;
font-family: Arial;
font-size: 60px;
font-weight: bold;
font-height:100px;
text-align:center;
position:absolute;
}
main2048.js
var board = new Array();
var score =0;
var hasConflicted = new Array();
//用于判断每个小格子是否发生了变化
//游戏初始化---------------------------------
$(document).ready(function(){
prepareForMobile();
newgame();
});
function prepareForMobile() {
if( documentWidth > 500 ){
gridContainerWidth = 500;
cellSpace = 20;
cellSideLength = 100;
}
$('#grid-container').css('width',gridContainerWidth-2*cellSpace);
$('#grid-container').css('height',gridContainerWidth-2*cellSpace);
$('#grid-container').css('padding',cellSpace);
$('#grid-container').css('border-radius',0.02*gridContainerWidth)
$('.grid-cell').css('width',cellSideLength);
$('.grid-cell').css('height',cellSideLength);
$('.grid-cell').css('border-radius',0.02*cellSideLength);
}
function newgame() {
//初始化棋盘格
init();
//在随机两个格子生成数字
generateOneNumber();
generateOneNumber();
}
function init() {
for (var i=0; i<4;i++)
for (var j=0;j<4;j++){
var gridCell=$('#grid-cell-'+i+"-"+j);
gridCell.css('top', getPosTop(i, j));
gridCell.css('left', getPosLeft(i, j));
//每个方格的位置由这两个函数决定,在support2048中
}
for(var i = 0;i<4;i++){
board[i]=new Array();
hasConflicted[i] = new Array();
for(var j=0;j<4;j++){
board[i][j]=0;
hasConflicted[i][j]= false;
}
}
updateBoardView();
score = 0;
}
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+'"></div>');
var theNumberCell = $('#number-cell-'+i+'-'+j);
if (board[i][j]==0){
theNumberCell.css('width','0px');
theNumberCell.css('height','0px');
theNumberCell.css('top',getPosTop(i, j)+cellSideLength/2);
theNumberCell.css('left',getPosLeft(i, j)+cellSideLength/2);
}else{
// 不为零时用number-cell代替gridcell
theNumberCell.css('width',cellSideLength);
theNumberCell.css('height',cellSideLength);
theNumberCell.css('top',getPosTop(i, j));
theNumberCell.css('left',getPosLeft(i, j));
theNumberCell.css('background-color',getNumberBackgroudColor(board[i][j]));
theNumberCell.css('color',getNumberColor(board[i][j]));
// 背景色和前景色
theNumberCell.text(board[i][j]);
}
hasConflicted[i][j]= false;
}
$('.number-cell').css('line-height',cellSideLength+'px');
$('.number-cell').css('font-size',0.6*cellSideLength+'px');
}
function generateOneNumber(){//先随机找一个位置,再在这个位置上赋值
if (nospace(board))
return false;
//随机一个位置
var randx=parseInt(Math.floor(Math.random()*4));
var randy=parseInt(Math.floor(Math.random()*4));
//产生0-4直接的浮点数,floor向下取整0123,parseInt强制类型转换为int
var time=0;
while(time<30){ //===========================================================不懂
if (board[randx][randy]==0)
break;
randx=parseInt(Math.floor(Math.random()*4));
randy=parseInt(Math.floor(Math.random()*4));
time++;
}
if(time>30){
for(var i=0;i<4;i++)
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,大于0.5为4
//显示
board[randx][randy]=randNumber;
showNumberWithAnimation(randx, randy, randNumber);
return true;
}
$(document).keydown(function (event) {
event.preventDefault();//阻挡按键本来会产生的效果,这里防止屏幕滚动条上下移动
switch (event.keyCode) {
case 37: //left,向左移动,生成新的数,判断游戏是否结束
if (moveLeft()){
setTimeout("generateOneNumber()",200);
setTimeout("isgameover();",200);
//设置延时,让alert有出现 的效果
}
break;
case 38://up
if (moveUp()){
setTimeout("generateOneNumber()",200);
setTimeout("isgameover();",200);
}
break;
case 39://right
if (moveRight()){
setTimeout("generateOneNumber()",200);
setTimeout("isgameover();",200);
}
break;
case 40://down
if (moveDown()){
setTimeout("generateOneNumber()",200);
setTimeout("isgameover();",200);
}
break;
default:
break;
}
})
document.addEventListener('touchstart',function (event) {
startx=event.touches[0].pageX;//event.touches获取多个手指触碰屏幕的信息【0】为一个手指
starty=event.touches[0].pageY;
});
document.addEventListener('touchmove',function (event) {
event.preventDefault();
});
document.addEventListener('touchend',function (event) {
endx=event.changedTouches[0].pageX;
endy=event.changedTouches[0].pageY;
var deltax=endx-startx;
var deltay=endy-starty;
if(Math.abs(deltax)<0.3*documentWidth && Math.abs(deltay)<0.3*documentWidth)
return;
//x
if(Math.abs(deltax)>=Math.abs(deltay)){
if(deltax>0){
//向右
if (moveRight()){
setTimeout("generateOneNumber()",200);
setTimeout("isgameover();",200);
}
}else{
//向左
if (moveLeft()){
setTimeout("generateOneNumber()",200);
setTimeout("isgameover();",200);
//设置延时,让alert有出现 的效果
}
}
}
else{
//y
if(deltay>0){
//向下
if (moveDown()){
setTimeout("generateOneNumber()",200);
setTimeout("isgameover();",200);
}
}else{
//向上
if (moveUp()){
setTimeout("generateOneNumber()",200);
setTimeout("isgameover();",200);
}
}
}
});
function isgameover() {
if (nospace(board)&&nomove(board)){
gameover();
}
}
function gameover() {
alert('Game Over');
}
//--------------------------------------------------------Left-------------------------------------
function moveLeft() {
if (!canMoveLeft(board)){
return false;
}
//moveLeft
for (var i=0;i<4;i++)
for (var j=1;j<4;j++){
if (board[i][j]!=0){//不为0可以向左移动,移动要判断该行左侧是否空(或相等)且没有障碍物
for (var k=0;k<j;k++){
if (board[i][k]==0&&noBlockHorizontal(i, k, j, board)){
//move
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]){
// else if (board[i][k]==board[i][j] &&noBlockHorizontal(i, k, j, board)){
//hasConflicted[i][k]没变化过,即没有发生add
//move
showMoveAnimation(i, j,i, k);
//add
board[i][k] +=board[i][j];
board[i][j]=0;
score+=board[i][k];
updateScore(score);
hasConflicted[i][k]=true;//只add一次
continue;
}
}
}
}
setTimeout("updateBoardView()",200);//设置更新在200ms后发生,move动画就可以显示
return true;
}
//--------------------------------------------------------Right-------------------------------------
function moveRight(){
if( !canMoveRight( board ) )
return false;
//moveRight
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] ){
// else if( board[i][k] == board[i][j] && noBlockHorizontal( i , j , k , board )){
showMoveAnimation( i , j , i , k);
board[i][k] *= 2;
board[i][j] = 0;
score+=board[i][k];
updateScore(score);
hasConflicted[i][k]=true;
continue;
}
}
}
}
setTimeout("updateBoardView()",200);
return true;
}
function moveUp() {
if (!canMoveUp(board)){
return false;
}
//
for(var j=0;j<4;j++)
for (var i=1;i<4;i++)
if (board[i][j]!=0)
for (var k=0;k<i;k++)
if (board[k][j]==0 && noBlockupdown(j, k, i, board)) {
//move
showMoveAnimation(i, j, k, j);
board[k][j] = board[i][j];
board[i][j] = 0;
continue;
}
// else if (board[k][j]==board[i][j]&&noBlockupdown(j, k, i, board)&& !hasConflicted[k][j]) {
else if (board[k][j]==board[i][j]&&noBlockupdown(j, k, i, board)) {
//move
showMoveAnimation(i,j, k, j);
//add
board[k][j]+=board[i][j];
board[i][j]=0;
score+=board[k][j];
updateScore(score);
// hasConflicted[k][j]=true;
continue;
}
setTimeout("updateBoardView()",200);
return true;
}
function moveDown() {
if (!canMoveDown(board)){
return false;
}
//
for (var j=0;j<4;j++)
for (var i=2;i>=0;i--)
if(board[i][j]!=0){
for (var k=3;k>i;k--)
if (board[k][j]==0&&noBlockupdown(j, i, k, board)){
//move
showMoveAnimation(i, j, k, j);
board[k][j]=board[i][j];
board[i][j]=0;
}
// else if (board[i][j]==board[k][j]&&noBlockupdown(j, i, k, board) && !hasConflicted[k][j]){
else if (board[i][j]==board[k][j]&&noBlockupdown(j, i, k, board)){
//move,add
showMoveAnimation(i, j, k, j)
board[k][j]+=board[i][j];
board[i][j]=0;
score+=board[k][j];
updateScore(score);
// hasConflicted[k][j]=true;
}
}
setTimeout("updateBoardView()",200);
return true;
}
support2048.js
documentWidth=window.screen.availWidth;
gridContainerWidth=0.92*documentWidth;
cellSideLength=0.18*documentWidth;
cellSpace=0.04*documentWidth;
function getPosTop(i,j) {
return cellSpace + (cellSpace+cellSideLength)*i;
}
function getPosLeft(i,j) {
return cellSpace + (cellSpace+cellSideLength)*j;
}
function getNumberBackgroudColor(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";
return "white";
}
function nospace(board) {
for (var i=0;i<4;i++)
for (var j=0;j<4;j++)
if (board[i][j]==0)//默认初始值为0,为0说明还有空格
return false;
return true;
}
function noBlockHorizontal(row, col1,col2,board) {
for (var i=col1+1;i<col2;i++)
if (board[row][i]!=0)
return false;
return true;
}
function noBlockupdown(col, row1,row2,board) {
for (var i=row1+1;i<row2;i++)
if (board[i][col]!=0)
return false;
return true;
}
function canMoveLeft(board) {
for (var i=0;i<4;i++)
for (var j=1;j<4;j++){
if (board[i][j]!=0)//这个点不为0判断要向左移动
if (board[i][j-1]==0 || board[i][j-1]==board[i][j])//左侧是空的或者左右相等
return true;
}
return false;
}
function canMoveRight(board){
for (var i=0;i<4;i++)
for (var j=2;j>=0;j--)
if (board[i][j]!=0)
if (board[i][j+1]==board[i][j] || board[i][j+1]==0)
return true;
return false;
}
function canMoveUp(board){
for (var j=0;j<4;j++)
for (var i=1;i<4;i++)
if (board[i][j]!=0){
if (board[i-1][j]==0 || board[i][j]==board[i-1][j]){
return true;
}
}
return false;
}
function canMoveDown(board) {
for (var j=0;j<4;j++)
for (var i=2;i>=0;i--)
if(board[i][j]!=0){
if (board[i+1][j]==board[i][j] || board[i+1][j]==0){
return true;
}
}
return false;
}
function nomove(board) {
if (canMoveLeft(board) || canMoveRight(board) || canMoveUp(board) || canMoveDown(board)){
return false;
}
return true;
}
showanimation.js 移动的动画效果
function showNumberWithAnimation(i, j, randNumber) {
var numberCell=$('#number-cell-'+i+"-"+j);
numberCell.css('background-color', getNumberBackgroudColor(randNumber));
numberCell.css('color', getNumberColor(randNumber));
numberCell.text(randNumber);
numberCell.animate({
// animate函数是jQUERY中的动画函数,动画渐变50ms
width:cellSideLength,
height:cellSideLength,
top:getPosTop(i, j),
left:getPosLeft(i, j)
},50);
}
function showMoveAnimation(fromx, fromy,tox, toy){
var numberCell = $('#number-cell-'+fromx+'-'+fromy);
numberCell.animate({
top:getPosTop(tox, toy),
left:getPosLeft(tox, toy)
},200);
}
function updateScore(score) {
$('#score').text(score);
}