【javascript小案例】从0开始实现一个俄罗斯方块

写在前面得话:

这篇文章主要记录了我是怎么一步一步写出俄罗斯方块,整个代码用的函数编程,主要是为了让一些不熟悉es6, 面向对象写法得 新手能更容易看明白,全部得代码中都是一些js的基础知识,很容易理解。要说有点麻烦的,那就是游戏过程中的各种检测。但是只要你多思考,你就能理解代码为什么要那样写,你也可以实现这个游戏。(当然也许你有更好的实现方法)。

预览地址:http://blog.cwlserver.top/demo/Tetris.html 

1,先理清游戏逻辑

  • 游戏场景:场景大小为 10*18,
  • 下落时间:初始方块每隔1秒,会下落一格。随着游戏进行时间得增加,方块下落时间间隔会缩短。
  • 操作方法:方向键得 上下左右 分别控制方块得, 变形,加速下落,左移,右移。
  • 方块类型:一共7种类型得方块。每次随机出现一种, 每种方块由数个 1*1大小得小方块组成
  • 方块下落:当方块落到底, 或者下一格已经被占,方块停止下落,然后会有一个新的方块出现
  • 方块左右移动:方块左右移动时,如果左,右是墙或者是已经被占,方块将不能移动。
  • 方块变形:方块逆时针旋转90°,变形时需要判断方块是否可以变形。
  • 游戏会有下一个方块得提示
  • 消行:当一行被填满时,这一行将被消除
  • 计分规则: 消1行得2分,2行4分,3行8分,4行16分
  • 游戏结束: 当方块下落到底,并且方块超出游戏场景时,判定游戏结束

2,分步实现游戏中得功能

html结构

    <div id="box"> <canvas id="canvas" width="300" height="540"></canvas> <div class="scorebox"> <p>游戏已进行: <span id="game-time">00:00:00</span></p><br> <p>当前得分: <span id="score">0</span></p><br> <p>下一个方块:</p><br> <canvas id="next" width="120" height="120"></canvas><br> <p class="btns"><button id="pause">暂停</button><button id="restart">重新开始</button></p> </div> </div> 

构建场景

因为场景大小是10x18,所以我决定用一个 10x18得二维数组来模拟场景,这样方便和方块做碰撞检测。

//定义列数
var ROW = 10; //定义行数 var COL = 18; //游戏得分 var SCORE = 0; //游戏场景 var area = new Array(COL); for(var i=0; i<area.length; i++){ area[i] = new Array(ROW).fill(0); } /* 最终得到得area是这样得 area = [  [0,0,0,0....]  [0,0,0,0....]  [0,0,0,0....]  ... ] */ 

构建小方块

小方块我同样使用二维数组来构建

//定义各种方块得数组, 一共7种不同得方块,数组中的1,2,3,4..这些数字主要是为了每个方块设置不同的颜色
var data = { 'o':[ [1, 1], [1, 1] ], 's':[ [2, 0, 0], [2, 2, 0], [0, 2, 0] ], '5':[ [0, 0, 3], [0, 3, 3], [0, 3, 0] ], 'l':[ [4, 0, 0], [4, 0, 0], [4, 4, 0] ], 't':[ [5, 5, 5], [0, 5, 0], [0, 0, 0] ], 'j':[ [0, 0, 6], [0, 0, 6], [0, 6, 6] ], '|':[ [0, 7, 0, 0], [0, 7, 0, 0], [0, 7, 0, 0], [0, 7, 0, 0] ] }; //定义方块得颜色,每个数字对应一种颜色 var aColor = ['', '#fff', '#0000FF', '#00FF00', '#CC00FF', '#CCFFFF','#FFFF33','#99FFFF']; //将data中得key放到一个字符串中 方便随机调用 var sKey = 'os5ltj|'; //定义当前方块, 当前方块默认null; var cur = null; //因为游戏中会有下一个方块得提示, 所以这里要提前声明一下 var next = null; //定义一个生成方块得函数 function createBox(){ //首先创建提示方块 if(!next){ //从skey中随机取出一个键名 var rnd = Math.floor(Math.random()*sKey.length); //根据key取得方块数组 var box = data[sKey[rnd]]; //每一个方块都有,x, y, box 这三个属性 next = { //方块初始在场景中间位置,方块左移 x--, 右移 x++; x: Math.floor((ROW-box[0].length)/2), //方块在垂直方向得位置,刚好在场景外, y++ 方块下落 y: -box[0].length, //方块得数组 box: box }; } //当前方块不存在时, 创建当前方块 if(!cur){ //直接下一个方块变成当前这个 cur = next; //然后再重新生成下一个 next = { x: Math.floor((ROW-box[0].length)/2), y: -box[0].length, box: data[sKey[Math.floor(Math.random()*sKey.length)]] } } } 

现在想一个问题,有了场景和方块的数据之后,如何把他们联系起来?

我的处理方式是这样的,在方块下落的过程中,方块和场景是分开的,方块的位置和场景是分开刷新的。在下落的过程中我会 检测方块和场景是否发生碰撞,如果发生了碰撞,将当前方块的数组合并到场景的数组中,使方块变成场景的一部分,同时生成一个新的方块。看下代码如何实现

//将当前方块合并到场景
function mergeBoxArea(){
    //循环当前方块 for(var i=0; i<cur.box.length; i++){ //这里的判断是为了当方块的一部分在场景外的时候,将那一部分跳过,只计算在场景中的部分 if(i+cur.y>=0){ for(var j=0; j<cur.box[i].length){ //将当前方块数组中不为0的项,和 场景中当前位置为0的项合并 if(cur.box[i][j] !== 0 && area[i+cur.y][j+cur.x] == 0){ //合并的结果, 将场景中当前位置的值设置为方块对应位置的值 area[i+cur.y][j+cur.x] = cur.box[i][j]; } } } } //将方块合并入场景的同时要尝试 消行 var arr = isRemove(area); if(arr.length !== 0){ for(var i=0; i<arr.length; i++){ area.splice(arr[i], 1) area.unshift(new Array(ROW).fill(0)) } //更新得分 SCORE+=Math.pow(2, arr.length) scoreEle.innerHTML = SCORE; }; } //碰撞检测 //垂直方向的碰撞检测, 需要接受当前方块做为参数, //作用:检测方块下落一格之后和场景的碰撞情况,如果会碰撞返回true,否则返回false; function collide(cur){ var box = cur.box; var len = box.length; var x = box.x; //因为是检测下一个位置,所以要+1; var y = box.y + 1; for(var i=0; i<len; i++){ //做碰撞检测同样需要将场景外的方块部分排除掉 if(i+y>=0){ //方块的数组都是n*n的所以都用len for(var j=0; j<len; j++){ //将方块为0的项不检测 if(box[i][j] !== 0){ //第一种碰撞情况:当i+y大于等于场景的高度时,说明方块出界 //第二种碰撞情况:方块没有出界,但是场景中的这个位置,被占用了 if(i+y>=area.length || (i+y<area.length && area[i+y][j+x] !== 0)){ //碰撞了返回 true return true; } } } } } //代码执行到这里时说明没有碰撞,返回false; return false; } //水平方向的移动限制 //当用键盘控制方块左右移动的时候,需要检测左右是否是墙,或者方块,这里检测的也是下一个位置的碰撞情况 //如果没有墙或者方块(不碰撞),返回true //如果碰撞, 返回 false; //接受参数: 当前方块:cur, 移动方向: dir -1|0|1 function bMove(cur, dir){ //当前位置加上方向 就是 下一个位置 var x = cur.x+dir; for(var i=0; i<cur.box.length; i++){ for(var j=0; j<cur.box[i].length; j++){ if(cur[i][j] !== 0){ //这里发生碰撞的情况有3中 //1.方块在左边出界了, 这时 j+x<0 //2.方块在右边出界了, j+x>= ROW //3.方块没有出界,但是场景中的这个位置被占用 area[i+cur.y][j+x]!==0 // 加上 i+cur.y>=0 && j+x>=0 && area[i+cur.y] 是为了防止报错 if(j+x<0 || j+x==ROW || ( i+cur.y>=0 && j+x>=0 && area[i+cur.y] && area[i+cur.y][j+x]!==0)){ return false; } } } } return true; } 

如何处理方块旋转?

方块的旋转比较容易处理,就把二维数组旋转一下就可以了。但是要注意方块旋转的时候也是需要检测 旋转的合理性, 可以想象一下,一个长条下落的过程中,如果他的左右两边都是方块,这种情况肯定是不能旋转的(其它方块同理)。还有一种情况就是,方块靠墙下落的时候,旋转一下之后,有一部分转到墙里面去了,这种也是不合理的,但是玩游戏的时候,这种情况也能旋转,所以出现这种情况的时候,我们需要修正一下方块的位置。 下面看代码怎么写

//此函数用于检测方块是否能够旋转
/*
参数: 当前方块 cur
返回值: true        //方块可以直接旋转
 false //方块不能旋转,即使是在尝试修正位置之后,就是上面说到的左右都是方块的情况  cur.x //当返回 一个数值的时候,说明 将方块水平移动到这个位置后,可以旋转, 即上面说的修正位置 */ function bRotate(cur){ //在这里复制一个旋转后的方块出来,用于检测 var _cur = {x: cur.x, y:cur.y, box: rotateBox(cur.box)}; //检测方块旋转之后,水平和垂直方向的碰撞情况, 如果在任意方向会发生碰撞 if( collide(_cur) === true || bMove(_cur, 0) === false ){ //尝试水平移动方块,移动方向是分别向左,向右移动2格 for(var i=0; i<2; i++){ //方块靠近左边的时候,尝试向右移动,并且检测移动的合理性 if(_cur.x<4 && bMove(_cur, 1)){ _cur.x++; } //靠近右侧的时候,向左移动,并且检测移动的合理性 if(_cur.x>6 && bMove(_cur, -1)){ _cur.x--; } //移动之后再检测是否碰撞, 如果不会发生碰撞, 返回移动后的位置 if(collide(_cur) === false && bMove(_cur, 0)){ return _cur.x; } } //代码执行到这里的时候说明,移动了之后仍会碰撞 return false; //如果旋转之后不会发生碰撞,直接返回true; }else{ return true; } } //旋转数组的函数 function rotateBox(arr){ var res = []; for(var i=0; i<arr.length; i++){ res.push([]); } //旋转 for(var i=0; i<arr.length; i++){ for(var j=0; j<arr[i].length; j++){ res[arr.length-1-y][x] = arr[x][y]; } } return res; } 

现在开始处理游戏的刷新, 计算游戏的时间

游戏用 requestAnimationFrame 更新

var timer = null; //记录一个旧的时间,这里用于辅助计算, 每次刷新的间隔时间 var oldTime = Date.now(); //n 用于累加 raf 的间隔时间 var n = 0; //游戏运行时间 单位 毫秒 var gameTime = 0; //方块下落的间隔时间 var step = 1000; //游戏是否暂停 var bPause = false; //获取dom元素 //主场景canvas var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); //提示下一个方块 canvas var nextCanvas = document.getElementById('next'); var nextctx = nextCanvas.getContext('2d'); //游戏得分 var scoreEle = document.getElementById('score'); //暂停按钮 var pauseEle = document.getElementById('pause'); //重新开始按钮 var restartEle = document.getElementById('restart') //显示游戏时间 var gameTimeEle = document.getElementById('game-time'); //开启主循环 timer = requestAnimationFrame(animate); //主循环函数 function animate(){ //累加 raf 的间隔时间 n+=Date.now()-oldTime; //累加游戏运行时间 gameTime+=Date.now()-oldTime; oldTime = Date.now(); //方块要开始下落了 if(n>=step){ n = 0; //每秒钟更新一次游戏时间 updateGameTime() //根据游戏进行时间提高游戏难度 changeDifficulty(); //方块下落之前要先检测是否会发生碰撞 //会发生碰撞 if(collide(cur)){ //会碰撞,并且此时,如果方块有一部分在外面,说明游戏结束 if(cur.y<0){ gameover() //正常的碰撞 }else{ //将方块合并入游戏场景 mergeBoxArea() //并将cur 设置为null cur = null; //产生一个新的方块 createBox() } //不会碰撞 }else{ cur.y++; } } //更新游戏场景 drawArea(); //画提示方块 drawNextBox(); timer = requestAnimationFrame(animate); } //更新游戏场景 function drawArea(){ ctx.clearRect(0, 0, 300, 540) ctx.save() ctx.scale(30, 30) //ctx.fillStyle = '#fff'; //画游戏场景 drawcube(ctx, area) //画当前方块 drawcube(ctx, cur.box, cur.x, cur.y) ctx.restore(); } //更新提示 function drawNextBox(){ nextctx.clearRect(0, 0, 120, 120) nextctx.save() nextctx.scale(30, 30) //画下一个方块 next&&drawcube(nextctx, next.box) nextctx.restore(); } //画方块,接受一个ctx对象,一个数组, 数组的偏移值 function drawcube(ctx, arr, x, y){ x = x || 0; y = y || 0; for(var i=0; i<arr.length; i++){ for(var j=0; j<arr[i].length; j++){ if(arr[i][j] !== 0){ //设置方块的颜色 ctx.fillStyle = aColor[arr[i][j]]; ctx.fillRect(j+x, i+y, 1, 1) } } } } 

监听键盘事件,移动方块

//监听键盘事件
document.addEventListener('keydown', function(ev){ if(bPause || !cur){ return false; } var keycode = ev.keyCode; switch(keycode){ //左 case 37: //是否能向左移动 if(bMove(cur, -1)){ cur.x--; } break; //右 case 39: //是否能向右移动 if(bMove(cur, 1)){ cur.x++; } break; //下 case 40: //如果触底或者落到其它方块上面 if(collide(cur)){ if(cur.y<0){ gameover() }else{ mergeBoxArea() cur = null; createBox() } }else{ cur.y++; } break; //上 case 38: //是否能旋转 当n为true时可以直接旋转,当n为数值时需要将方块x位置移动到此处才能旋转 var rotateRes = bRotate(cur); //可以直接旋转 if(rotateRes === true){ cur.box = rotateBox(cur.box); //不能旋转 }else if(rotateRes === false){ console.log('不能旋转') //需要移动之后才能旋转 }else{ cur.x = rotateRes; cur.box = rotateBox(cur.box); } break; } }) 

处理游戏结束, 游戏暂停, 游戏重新开始, 消行, 更新游戏得分, 更新游戏运行时间等等

//点击暂停按钮
pauseEle.addEventListener('click', function(){ var html = this.innerHTML; if(html === '暂停'){ pause(); this.innerHTML = '继续'; }else{ start(); this.innerHTML = '暂停'; } }) //点击重新开始 restartEle.addEventListener('click', function(){ restart(); }) //暂停游戏 function pause(){ cancelAnimationFrame(timer); bPause = true; } //继续 function start(){ timer = requestAnimationFrame(animate); bPause = false; } //重新开始 function restart(){ //重置场景 for(var i=0; i<area.length; i++){ for(var j=0; j<area[i].length; j++){ area[i][j] = 0; } } cancelAnimationFrame(timer); timer = requestAnimationFrame(animate); bPause = false; pauseEle.innerHTML = '暂停'; //重置游戏时间 gameTime = 0; //更新游戏时间 updateGameTime(); cur = null; //创建第一个方块 createBox(); } //游戏结束 function gameover(){ cancelAnimationFrame(timer); alert('游戏结束, 您一共获得:'+SCORE+"分") restart() } //检测是否可以消行,并将可以消除得行 加入结果数组返回出去 function isRemove(area){ var arr = []; for(var i=0; i<area.length; i++){ var remove = true; //如果数组的一行的每一项都不为0说明可以消除 for(var j=0; j<area[i].length; j++){ if(area[i][j] == 0){ remove = false; } } //储存消除行的索引 if(remove){ arr.push(i) } } return arr; } //更新游戏运行时间 function updateGameTime(){ var n = gameTime/1000; var h = Math.floor(n/(60*60)); n%=60*60; var m = Math.floor(n/60); n%=60; var s = Math.floor(n); h = h<9?'0'+h:''+h; m = m<9?'0'+m:''+m; s = s<9?'0'+s:''+s; gameTimeEle.innerHTML = h+':'+m+':'+s; } //根据游戏时间修改难度(方块下落间隔时间) function changeDifficulty(){ //游戏进行5分钟 方块下落间隔为300ms if(gameTime>=1000*60*5){ step = 300; //游戏运行3分钟 方块下落间隔500ms }else if(gameTime>=1000*60*3){ step = 500; //游戏运行2分钟 方块下落间隔700ms }else if(gameTime>=1000*60*2){ step = 700; } } 

到这里整个游戏差不多就算完了,最终的预览demo和现在的代码并某些细节不是完全一样。

作者简介:陈蔚磊(高级前端工程师),公众号“铅笔学园”前端内容合作作者之一,目前在一培训机构任职前端工程师,掌握最前沿的前端技术。

铅笔学园:IT资源分享|知识分享,做初级程序员的指明灯
在这里插入图片描述

猜你喜欢

转载自www.cnblogs.com/qianbixueyuan/p/10560205.html
今日推荐