JavaScript实现俄罗斯方块游戏过程分析及源码分享
观摩一下《编程之美》:“程序虽然很难写,却很美妙。要想把程序写好,需要写好一定的基础知识,包括编程语言、数据结构与算法。程序写得好,需要缜密的逻辑思维能力和良好的梳理基础,而且熟悉编程环境和编程工具。”
学了几年的计算机,你有没有爱上编程。话说,没有尝试自己写过一个游戏,算不上热爱编程。
俄罗斯方块曾经造成的轰动与造成的经济价值可以说是游戏史上的一件大事,它看似简单但却变化无穷,令人上瘾。相信大多数同学,曾经为它痴迷得茶不思饭不想。
游戏规则
1、一个用于摆放小型正方形的平面虚拟场地,其标准大小:行宽为10,列高为20,以每个小正方形为单位。
2、一组由4个小型正方形组成的规则图形,英文称为Tetromino,中文通称为方块共有7种,分别以S、Z、L、J、I、O、T这7个字母的形状来命名。
I:一次最多消除四层
J(左右):最多消除三层,或消除二层
L:最多消除三层,或消除二层
O:消除一至二层
S(左右):最多二层,容易造成孔洞
Z (左右):最多二层,容易造成孔洞
T:最多二层
方块会从区域上方开始缓慢继续落下。玩家可以以90度为单位旋转方块,以格子为单位左右移动方块,让方块加速落下。方块移到区域最下方或是着地到其他方块上无法移动时,就会固定在该处,而新的方块出现在区域上方开始落下。当区域中某一列横向格子全部由方块填满,则该列会消失并成为玩家的得分。同时删除的列数越多,得分指数上升。
分析与解法
每块方块落下的过程中,我们可以做:
1)旋转到合适的方向
2)水平移动到某一列
3)垂直下落到底部
首先,需要用一个二维数组,area[18][10]表示18*10的游戏区域。其中,数组中值为0表示空,1表示有方块。
方块一共7种,每种有4种方向。定义activeBlock[4],在编译之前这个数组的值预定算好,在程序中直接使用。
难点
1)边界检查。
//检查左边界,尝试着朝左边移动一个,看是否合法。 function checkLeftBorder(){ for(var i=0; i<activeBlock.length; i++){ if(activeBlock[i].y==0){ return false; } if(!isCellValid(activeBlock[i].x, activeBlock[i].y-1)){ return false; } } return true; } //同理,需要检测右边界和底边界
2)旋转, 需要数理逻辑, 一个点相对另外一个点旋转90度的问题。
3)定时和监听键盘事件机制让游戏自动运行下去。
//开始 function begin(e){ e.disabled = true; status = 1; tbl = document.getElementById("area"); if(!generateBlock()){ alert("Game over!"); status = 2; return; } paint(); timer = setInterval(moveDown,1000); } document.onkeydown=keyControl;
程序过程
1)用户点开始->构造一个活动图形, 设置定时器。
//当前活动的方块, 它可以左右下移动, 变型。当它触底后, 将会更新area; var activeBlock; //生产方块形状, 有7种基本形状。 function generateBlock(){ activeBlock = null; activeBlock = new Array(4); //随机产生0-6数组,代表7种形态。 var t = (Math.floor(Math.random()*20)+1)%7; switch(t){ case 0:{ activeBlock[0] = {x:0, y:4}; activeBlock[1] = {x:1, y:4}; activeBlock[2] = {x:0, y:5}; activeBlock[3] = {x:1, y:5}; break; } //省略部分代码.............................. case 6:{ activeBlock[0] = {x:0, y:5}; activeBlock[1] = {x:1, y:4}; activeBlock[2] = {x:1, y:5}; activeBlock[3] = {x:1, y:6}; break; } } //检查刚生产的四个小方格是否可以放在初始化的位置. for(var i=0; i<4; i++){ if(!isCellValid(activeBlock[i].x, activeBlock[i].y)){ return false; } } return true; }
2)每次向下移动后, 都检查是否触底, 如果触底了, 则尝试消行。
//消行 function deleteLine(){ var lines = 0; for(var i=0; i<18; i++){ var j=0; for(; j<10; j++){ if(area[i][j]==0){ break; } } if(j==10){ lines++; if(i!=0){ for(var k=i-1; k>=0; k--){ area[k+1] = area[k]; } } area[0] = generateBlankLine(); } } return lines; }
3)完了之后再构造一个活动图形, 再设置定时器。
效果图
有待优化
1)设置不同形状方块的颜色。
思路:在创建方块函数内,设定activeBlockColor颜色,七种不同形态方块颜色各异(除了修改generateBlock方法之外,还需要修改paintarea方法。因为一开始考虑不周全,消除一行后,重绘方块的同时将颜色统一,因此可以考虑移除表格n行,然后在顶部增添n行,以保证没消除方块的完整性)。
2)当当前方块下落时,可以提前查看下一个方块。
思路:将generateBlock方法拆分成两部分,一部分用于随机尝试下一个方块,一部分用于缓存当前所要描绘的方块。当当前方块碰到底部被固定后,下一方块开始描绘,同时又再次随机产生新方块。如此反复。
完整HTML源码:
<!DOCTYPE> <html> <head> <title>Tetris</title> <meta charset="UTF-8"> <style> *{ font-family: "微软雅黑"; } .tetrisContainer{ width: 230px; height: 400px; position: relative; left: 50%; margin-left: -115px; top: 40%; margin-top: -200px; } #area tr td{ width: 20px; height: 20px; border:1px solid #ccc; } </style> </head> <body> <div class = "tetrisContainer"> <input type="button" value="开始游戏" onclick="begin(this);"/> 得分: <span id="score"> 0</span> <table id="area" cellspacing="0" cellpadding="0" border="1" style="border-collapse:collapse"><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr></table> </div> </body> <script type="text/javascript" src="script/tetris.js"></script> </html>
完整tetris.js源码:
/** * JS俄罗斯方块游戏 v 1.0 */ //表示页面中的table, 这个table就是将要显示游戏的主面板 var tbl; //游戏状态 0: 未开始;1 运行; 2 中止; var status = 0; //定时器, 定时器内将做moveDown操作 var timer; //分数 var score = 0; //area是一个18*10的数组,也和页面的table对应。初始时都为0, 如果被占据则为1 var area = new Array(18); for(var i=0;i<18;i++){ area[i] = new Array(10); } for(var i=0;i<18;i++){ for(var j=0; j<10; j++){ area[i][j] = 0; } } //当前活动的方块, 它可以左右下移动, 变型。当它触底后, 将会更新area; var activeBlock; //生产方块形状, 有7种基本形状。 function generateBlock(){ activeBlock = null; activeBlock = new Array(4); //随机产生0-6数组,代表7种形态。 var t = (Math.floor(Math.random()*20)+1)%7; switch(t){ case 0:{ activeBlock[0] = {x:0, y:4}; activeBlock[1] = {x:1, y:4}; activeBlock[2] = {x:0, y:5}; activeBlock[3] = {x:1, y:5}; break; } case 1:{ activeBlock[0] = {x:0, y:3}; activeBlock[1] = {x:0, y:4}; activeBlock[2] = {x:0, y:5}; activeBlock[3] = {x:0, y:6}; break; } case 2:{ activeBlock[0] = {x:0, y:5}; activeBlock[1] = {x:1, y:4}; activeBlock[2] = {x:1, y:5}; activeBlock[3] = {x:2, y:4}; break; } case 3:{ activeBlock[0] = {x:0, y:4}; activeBlock[1] = {x:1, y:4}; activeBlock[2] = {x:1, y:5}; activeBlock[3] = {x:2, y:5}; break; } case 4:{ activeBlock[0] = {x:0, y:4}; activeBlock[1] = {x:1, y:4}; activeBlock[2] = {x:1, y:5}; activeBlock[3] = {x:1, y:6}; break; } case 5:{ activeBlock[0] = {x:0, y:4}; activeBlock[1] = {x:1, y:4}; activeBlock[2] = {x:2, y:4}; activeBlock[3] = {x:2, y:5}; break; } case 6:{ activeBlock[0] = {x:0, y:5}; activeBlock[1] = {x:1, y:4}; activeBlock[2] = {x:1, y:5}; activeBlock[3] = {x:1, y:6}; break; } } //检查刚生产的四个小方格是否可以放在初始化的位置. for(var i=0; i<4; i++){ if(!isCellValid(activeBlock[i].x, activeBlock[i].y)){ return false; } } return true; } //向下移动 function moveDown(){ //检查底边界. if(checkBottomBorder()){ //没有触底, 则擦除当前图形, erase(); //更新当前图形坐标 for(var i=0; i<4; i++){ activeBlock[i].x = activeBlock[i].x + 1; } //重画当前图形 paint(); } //触底, else{ //停止当前的定时器, 也就是停止自动向下移动. clearInterval(timer); //更新area数组. updatearea(); //消行 var lines = deleteLine(); //如果有消行, 则 if(lines!=0){ //更新分数 score = score + lines*10; updateScore(); //擦除整个面板 erasearea(); //重绘面板 paintarea(); } //产生一个新图形并判断是否可以放在最初的位置. if(!generateBlock()){ alert("Game over!"); status = 2; return; } paint(); //定时器, 每隔一秒执行一次moveDown timer = setInterval(moveDown,1000) } } //左移动 function moveLeft(){ if(checkLeftBorder()){ erase(); for(var i=0; i<4; i++){ activeBlock[i].y = activeBlock[i].y - 1; } paint(); } } //右移动 function moveRight(){ if(checkRightBorder()){ erase(); for(var i=0; i<4; i++){ activeBlock[i].y = activeBlock[i].y + 1; } paint(); } } //旋转, 因为旋转之后可能会有方格覆盖已有的方格. //先用一个tmpBlock,把activeBlock的内容都拷贝到tmpBlock, //对tmpBlock尝试旋转, 如果旋转后检测发现没有方格产生冲突,则 //把旋转后的tmpBlock的值给activeBlock. function rotate(){ var tmpBlock = new Array(4); for(var i=0; i<4; i++){ tmpBlock[i] = {x:0, y:0}; } for(var i=0; i<4; i++){ tmpBlock[i].x = activeBlock[i].x; tmpBlock[i].y = activeBlock[i].y; } //先算四个点的中心点,则这四个点围绕中心旋转90度。 var cx = Math.round((tmpBlock[0].x + tmpBlock[1].x + tmpBlock[2].x + tmpBlock[3].x)/4); var cy = Math.round((tmpBlock[0].y + tmpBlock[1].y + tmpBlock[2].y + tmpBlock[3].y)/4); //旋转的主要算法. 可以这样分解来理解。 //先假设围绕源点旋转。然后再加上中心点的坐标。 for(var i=0; i<4; i++){ tmpBlock[i].x = cx+cy-activeBlock[i].y; tmpBlock[i].y = cy-cx+activeBlock[i].x; } //检查旋转后方格是否合法. for(var i=0; i<4; i++){ if(!isCellValid(tmpBlock[i].x,tmpBlock[i].y)){ return; } } //如果合法, 擦除 erase(); //对activeBlock重新赋值. for(var i=0; i<4; i++){ activeBlock[i].x = tmpBlock[i].x; activeBlock[i].y = tmpBlock[i].y; } //重画. paint(); } //检查左边界,尝试着朝左边移动一个,看是否合法。 function checkLeftBorder(){ for(var i=0; i<activeBlock.length; i++){ if(activeBlock[i].y==0){ return false; } if(!isCellValid(activeBlock[i].x, activeBlock[i].y-1)){ return false; } } return true; } //检查右边界,尝试着朝右边移动一个,看是否合法。 function checkRightBorder(){ for(var i=0; i<activeBlock.length; i++){ if(activeBlock[i].y==9){ return false; } if(!isCellValid(activeBlock[i].x, activeBlock[i].y+1)){ return false; } } return true; } //检查底边界,尝试着朝下边移动一个,看是否合法。 function checkBottomBorder(){ for(var i=0; i<activeBlock.length; i++){ if(activeBlock[i].x==17){ return false; } if(!isCellValid(activeBlock[i].x+1, activeBlock[i].y)){ return false; } } return true; } //检查坐标为(x,y)的是否在area种已经存在, 存在说明这个方格不合法。 function isCellValid(x, y){ if(x>17||x<0||y>9||y<0){ return false; } if(area[x][y]==1){ return false; } return true; } //擦除 function erase(){ for(var i=0; i<4; i++){ tbl.rows[activeBlock[i].x].cells[activeBlock[i].y].style.backgroundColor="white"; } } //绘活动图形 function paint(){ for(var i=0; i<4; i++){ tbl.rows[activeBlock[i].x].cells[activeBlock[i].y].style.backgroundColor="#CC3333"; } } //更新area数组 function updatearea(){ for(var i=0; i<4; i++){ area[activeBlock[i].x][activeBlock[i].y]=1; } } //消行 function deleteLine(){ var lines = 0; for(var i=0; i<18; i++){ var j=0; for(; j<10; j++){ if(area[i][j]==0){ break; } } if(j==10){ lines++; if(i!=0){ for(var k=i-1; k>=0; k--){ area[k+1] = area[k]; } } area[0] = generateBlankLine(); } } return lines; } //擦除整个面板 function erasearea(){ for(var i=0; i<18; i++){ for(var j=0; j<10; j++){ tbl.rows[i].cells[j].style.backgroundColor = "white"; } } } //重绘整个面板 function paintarea(){ for(var i=0;i<18;i++){ for(var j=0; j<10; j++){ if(area[i][j]==1){ tbl.rows[i].cells[j].style.backgroundColor = "#CC3333"; } } } } //产生一个空白行. function generateBlankLine(){ var line = new Array(10); for(var i=0; i<10; i++){ line[i] = 0; } return line; } //更新分数 function updateScore(){ document.getElementById("score").innerText=" " + score; } //键盘控制 function keyControl(){ if(status!=1){ return; } var code = event.keyCode; switch(code){ case 37:{ moveLeft(); break; } case 38:{ rotate(); break; } case 39:{ moveRight(); break; } case 40:{ moveDown(); break; } } } //开始 function begin(e){ e.disabled = true; status = 1; tbl = document.getElementById("area"); if(!generateBlock()){ alert("Game over!"); status = 2; return; } paint(); timer = setInterval(moveDown,1000); } document.onkeydown=keyControl;