Html5 小游戏 俄罗斯方块
导言
在一个风和日丽的一天,看完了疯狂html 5+css 3+javascript讲义,跟着做了书里最后一章的俄罗斯方块小游戏,并做了一些改进,作为自己前端学习的第一站。
游戏效果:
制作思路
因为书里的俄罗斯方块比较普通,太常规了,不是很好看,所以我在网上找了上面那张图片,打算照着它来做。(请无视成品和原图的差距)
然后便是游戏界面和常规的俄罗斯方块游戏逻辑。
接着便是游戏结束界面了。
原本想做个弹出层,但觉得找图片有点麻烦,所以就在网上找了文字特效,套用了一下。
代码实现:
首先是html文件和css文件,主要涉及了布局方面。作为新手,在上面真的是翻来覆去的踩坑。o(╥﹏╥)o
index.html
<!doctype html> <html> <head> <title>俄罗斯方块</title> <meta http-equiv="content-type" content="text/html;charset=utf-8"/> <link rel=stylesheet type="text/css" href="teris.css"> <style type="text/css"> /*导入外部的字体文件*/ @font-face{ font-family:tmb;/*为字体命名为tmb*/ src:url("ds-digib.ttf") format("truetype");/*format为字体文件格式,truetype为ttf*/ } div>span{ font-family:tmb; font-size:18pt; color:green; } </style> </head> <body> <div id="container" class="bg"> <!--ui--> <div class="ui_bg"> <div style="float:left;margin-right:4px;"> 速度:<span id="cur_speed">1</span> </div> <div style="float:left;"> 当前分数:<span id="cur_points">0</span> </div> <div style="float:right;"> 最高分数:<span id="max_points">0</span> </div> </div> <canvas id="text" width="500" height="100" style="position:absolute;"></canvas> <canvas id="stage" width="500" height="100" style="position:absolute;"></canvas> </div> <script src='easepack.min.js'></script> <script src='tweenlite.min.js'></script> <script src='easeljs-0.7.1.min.js'></script> <script src='requestanimationframe.js'></script> <script type="text/javascript" src="jquery-3.4.1.min.js"></script> <script type="text/javascript" src="teris.js"></script> </body> </html>
teris.css
*{ margin:0; padding:0; } html, body{ width:100%; height:100%; } .bg{ font-size:13pt; background-color:rgb(239, 239, 227); /*好看的渐变色*/ background-image:radial-gradient(rgb(239, 239, 227), rgb(230, 220, 212)); /*阴影*/ box-shadow:#cdc8c1 -1px -1px 7px 0px; padding-bottom:4px; } .ui_bg{ border-bottom:1px #a69e9ea3 solid; padding-bottom:2px; overflow:hidden;/*没有这句的话因为子div都设置了float,所以是浮在网页上的,所以父div就没有高度,这句清除了浮动,让父div有了子div的高度*/ }
然后是重头戏,teris.js
游戏变量
//游戏设定 var tetris_rows = 20; var tetris_cols = 14; var cell_size = 24; var no_block=0; var have_block=1; // 定义几种可能出现的方块组合 var blockarr = [ // z [ {x: tetris_cols / 2 - 1 , y:0}, {x: tetris_cols / 2 , y:0}, {x: tetris_cols / 2 , y:1}, {x: tetris_cols / 2 + 1 , y:1} ], // 反z [ {x: tetris_cols / 2 + 1 , y:0}, {x: tetris_cols / 2 , y:0}, {x: tetris_cols / 2 , y:1}, {x: tetris_cols / 2 - 1 , y:1} ], // 田 [ {x: tetris_cols / 2 - 1 , y:0}, {x: tetris_cols / 2 , y:0}, {x: tetris_cols / 2 - 1 , y:1}, {x: tetris_cols / 2 , y:1} ], // l [ {x: tetris_cols / 2 - 1 , y:0}, {x: tetris_cols / 2 - 1, y:1}, {x: tetris_cols / 2 - 1 , y:2}, {x: tetris_cols / 2 , y:2} ], // j [ {x: tetris_cols / 2 , y:0}, {x: tetris_cols / 2 , y:1}, {x: tetris_cols / 2 , y:2}, {x: tetris_cols / 2 - 1, y:2} ], // □□□□ [ {x: tetris_cols / 2 , y:0}, {x: tetris_cols / 2 , y:1}, {x: tetris_cols / 2 , y:2}, {x: tetris_cols / 2 , y:3} ], // ┴ [ {x: tetris_cols / 2 , y:0}, {x: tetris_cols / 2 - 1 , y:1}, {x: tetris_cols / 2 , y:1}, {x: tetris_cols / 2 + 1, y:1} ] ]; // 记录当前积分 var curscore=0; // 记录曾经的最高积分 var maxscore=1; var curspeed=1; //ui元素 var curspeedele=document.getelementbyid("cur_speed"); var curscoreele=document.getelementbyid("cur_points"); var maxscoreele=document.getelementbyid("max_points"); var timer;//方块下落控制 var mycanvas; var canvasctx; var tetris_status;//地图数据 var currentfall;//当前下落的block
游戏界面的完善
//create canvas function createcanvas(){ mycanvas=document.createelement("canvas"); mycanvas.width=tetris_cols*cell_size; mycanvas.height=tetris_rows*cell_size; //绘制背景 canvasctx=mycanvas.getcontext("2d"); canvasctx.beginpath(); //tetris_cos for(let i=1; i<tetris_cols; i++){ canvasctx.moveto(i*cell_size, 0); canvasctx.lineto(i*cell_size, mycanvas.height); } for(let i=1; i<tetris_rows; i++){ canvasctx.moveto(0, i*cell_size); canvasctx.lineto(mycanvas.width, i*cell_size); } canvasctx.closepath(); canvasctx.strokestyle="#b4a79d"; canvasctx.linewidth=0.6; canvasctx.stroke(); //第一行,最后一行,第一列,最后一列粗一点。 canvasctx.beginpath(); canvasctx.moveto(0, 0); canvasctx.lineto(mycanvas.width, 0); canvasctx.moveto(0, mycanvas.height); canvasctx.lineto(mycanvas.width, mycanvas.height); canvasctx.moveto(0, 0); canvasctx.lineto(0, mycanvas.height); canvasctx.moveto(mycanvas.width, 0); canvasctx.lineto(mycanvas.width, mycanvas.height); canvasctx.closepath(); canvasctx.strokestyle="#b4a79d"; canvasctx.linewidth=4; canvasctx.stroke(); //设置绘制block时的style canvasctx.fillstyle="#201a14"; }
1 function changewidthandheight(w, h){ 2 //通过jquery设置css 3 h+=$("ui_bg").css("height")+$("ui_bg").css("margin-rop")+$("ui_bg").css("margin-bottom")+$("ui_bg").css("padding-top")+$("ui_bg").css("padding-bottom"); 4 $(".bg").css({ 5 "width":w, 6 "height":h, 7 "top":0, "bottom":0, "right":0, "left":0, 8 "margin":"auto" 9 }); 10 }
1 //draw blocks 2 function drawblocks(){ 3 //清空地图 4 for(let i=0; i<tetris_rows;i++){ 5 for(let j=0;j<tetris_cols;j++) 6 canvasctx.clearrect(j*cell_size+1, i*cell_size+1, cell_size-2, cell_size-2); 7 } 8 //绘制地图 9 for(let i=0; i<tetris_rows;i++){ 10 for(let j=0;j<tetris_cols;j++){ 11 if(tetris_status[i][j]!=no_block) 12 canvasctx.fillrect(j*cell_size+1, i*cell_size+1, cell_size-2, cell_size-2);//中间留点缝隙 13 } 14 } 15 //绘制currentfall 16 for(let i=0;i<currentfall.length;i++) 17 canvasctx.fillrect(currentfall[i].x*cell_size+1, currentfall[i].y*cell_size+1, cell_size-2,cell_size-2); 18 }
游戏逻辑
1 function rotate(){ 2 // 定义记录能否旋转的旗标 3 var canrotate = true; 4 for (var i = 0 ; i < currentfall.length ; i++) 5 { 6 var prex = currentfall[i].x; 7 var prey = currentfall[i].y; 8 // 始终以第三个方块作为旋转的中心, 9 // i == 2时,说明是旋转的中心 10 if(i != 2) 11 { 12 // 计算方块旋转后的x、y坐标 13 var afterrotatex = currentfall[2].x + prey - currentfall[2].y; 14 var afterrotatey = currentfall[2].y + currentfall[2].x - prex; 15 // 如果旋转后所在位置已有方块,表明不能旋转 16 if(tetris_status[afterrotatey][afterrotatex + 1] != no_block) 17 { 18 canrotate = false; 19 break; 20 } 21 // 如果旋转后的坐标已经超出了最左边边界 22 if(afterrotatex < 0 || tetris_status[afterrotatey - 1][afterrotatex] != no_block) 23 { 24 moveright(); 25 afterrotatex = currentfall[2].x + prey - currentfall[2].y; 26 afterrotatey = currentfall[2].y + currentfall[2].x - prex; 27 break; 28 } 29 if(afterrotatex < 0 || tetris_status[afterrotatey-1][afterrotatex] != no_block) 30 { 31 moveright(); 32 break; 33 } 34 // 如果旋转后的坐标已经超出了最右边边界 35 if(afterrotatex >= tetris_cols - 1 || 36 tetris_status[afterrotatey][afterrotatex+1] != no_block) 37 { 38 moveleft(); 39 afterrotatex = currentfall[2].x + prey - currentfall[2].y; 40 afterrotatey = currentfall[2].y + currentfall[2].x - prex; 41 break; 42 } 43 if(afterrotatex >= tetris_cols - 1 || 44 tetris_status[afterrotatey][afterrotatex+1] != no_block) 45 { 46 moveleft(); 47 break; 48 } 49 } 50 } 51 if(canrotate){ 52 for (var i = 0 ; i < currentfall.length ; i++){ 53 var prex = currentfall[i].x; 54 var prey = currentfall[i].y; 55 if(i != 2){ 56 currentfall[i].x = currentfall[2].x + 57 prey - currentfall[2].y; 58 currentfall[i].y = currentfall[2].y + 59 currentfall[2].x - prex; 60 } 61 } 62 localstorage.setitem("currentfall", json.stringify(currentfall)); 63 } 64 }
1 //按下 下 或 interval到了 2 function next(){ 3 if(movedown()){ 4 //记录block 5 for(let i=0;i<currentfall.length;i++) 6 tetris_status[currentfall[i].y][currentfall[i].x]=have_block; 7 //判断有没有满行的 8 for(let j=0;j<currentfall.length;j++){ 9 for(let i=0;i<tetris_cols; i++){ 10 if(tetris_status[currentfall[j].y][i]==no_block) 11 break; 12 //最后一行满了 13 if(i==tetris_cols-1){ 14 //消除最后一行 15 for(let i=currentfall[j].y; i>0;i--){ 16 for(let j=0;j<tetris_cols;j++) 17 tetris_status[i][j]=tetris_status[i-1][j]; 18 } 19 //分数增加 20 curscore+=5; 21 localstorage.setitem("curscore", curscore); 22 if(curscore>maxscore){ 23 //超越最高分 24 maxscore=curscore; 25 localstorage.setitem("maxscore", maxscore); 26 } 27 //加速 28 curspeed+=0.1; 29 localstorage.setitem("curspeed", curspeed); 30 //ui输出 31 curscoreele.innerhtml=""+curscore; 32 maxscoreele.innerhtml=""+maxscore; 33 curspeedele.innerhtml=curspeed.tofixed(1);//保留两位小数 34 clearinterval(timer); 35 timer=setinterval(function(){ 36 next(); 37 }, 500/curspeed); 38 } 39 } 40 } 41 //判断是否触顶 42 for(let i=0;i<currentfall.length;i++){ 43 if(currentfall[i].y==0){ 44 gameend(); 45 return; 46 } 47 } 48 localstorage.setitem("tetris_status", json.stringify(tetris_status)); 49 //新的block 50 createblock(); 51 localstorage.setitem("currentfall", json.stringify(currentfall)); 52 } 53 drawblocks(); 54 } 55 56 //右移 57 function moveright(){ 58 for(let i=0;i<currentfall.length;i++){ 59 if(currentfall[i].x+1>=tetris_rows || tetris_status[currentfall[i].y][currentfall[i].x+1]!=no_block) 60 return; 61 } 62 for(let i=0;i<currentfall.length;i++) 63 currentfall[i].x++; 64 localstorage.setitem("currentfall", json.stringify(currentfall)); 65 return; 66 } 67 //左移 68 function moveleft(){ 69 for(let i=0;i<currentfall.length;i++){ 70 if(currentfall[i].x-1<0 || tetris_status[currentfall[i].y][currentfall[i].x-1]!=no_block) 71 return; 72 } 73 for(let i=0;i<currentfall.length;i++) 74 currentfall[i].x--; 75 localstorage.setitem("currentfall", json.stringify(currentfall)); 76 return; 77 } 78 //judge can move down and if arrive at end return 1, if touch other blocks return 2, else, return 0 79 function movedown(){ 80 for(let i=0;i<currentfall.length;i++){ 81 if(currentfall[i].y>=tetris_rows-1 || tetris_status[currentfall[i].y+1][currentfall[i].x]!=no_block) 82 return true; 83 } 84 85 for(let i=0;i<currentfall.length;i++) 86 currentfall[i].y+=1; 87 return false; 88 }
1 function gamekeyevent(evt){ 2 switch(evt.keycode){ 3 //向下 4 case 40://↓ 5 case 83://s 6 next(); 7 drawblocks(); 8 break; 9 //向左 10 case 37://← 11 case 65://a 12 moveleft(); 13 drawblocks(); 14 break; 15 //向右 16 case 39://→ 17 case 68://d 18 moveright(); 19 drawblocks(); 20 break; 21 //旋转 22 case 38://↑ 23 case 87://w 24 rotate(); 25 drawblocks(); 26 break; 27 } 28 }
其他的详细情况可以看源代码,我就不整理了。
接下来我们看游戏结束时的特效。因为我也不是很懂,所以在这里整理的会比较详细。当做学习。
1 //game end 2 function gameend(){ 3 clearinterval(timer); 4 //键盘输入监听结束 5 window.onkeydown=function(){ 6 //按任意键重新开始游戏 7 window.onkeydown=gamekeyevent; 8 //初始化游戏数据 9 initdata(); 10 createblock(); 11 localstorage.setitem("currentfall", json.stringify(currentfall)); 12 localstorage.setitem("tetris_status", json.stringify(tetris_status)); 13 localstorage.setitem("curscore", curscore); 14 localstorage.setitem("curspeed", curspeed); 15 //绘制 16 curscoreele.innerhtml=""+curscore; 17 curspeedele.innerhtml=curspeed.tofixed(1);//保留两位小数 18 drawblocks(); 19 timer=setinterval(function(){ 20 next(); 21 }, 500/curspeed); 22 //清除特效 23 this.stage.removeallchildren(); 24 this.textstage.removeallchildren(); 25 }; 26 //特效,游戏结束 27 settimeout(function(){ 28 initanim(); 29 //擦除黑色方块 30 for(let i=0; i<tetris_rows;i++){ 31 for(let j=0;j<tetris_cols;j++) 32 canvasctx.clearrect(j*cell_size+1, i*cell_size+1, cell_size-2, cell_size-2); 33 } 34 }, 200); 35 //推迟显示failed 36 settimeout(function(){ 37 if(textformed) { 38 explode(); 39 settimeout(function() { 40 createtext("failed"); 41 }, 810); 42 } else { 43 createtext("failed"); 44 } 45 }, 800); 46 }
上面代码里的localstorage是html5的本地数据存储。因为不是运用很难,所以具体看代码。
整个特效是运用了createjs插件。要引入几个文件。
easeljs-0.7.1.min.js, easepacj.min.js, requestanimationframe.js和tweenlite.min.js
游戏重新开始就要清除特效。我看api里我第一眼望过去最明显的就是removeallchildren(),所以就选了这个。其他的改进日后再说。
//清除特效 this.stage.removeallchildren(); this.textstage.removeallchildren();
function initanim() { initstages(); inittext(); initcircles(); //在stage下方添加文字——按任意键重新开始游戏. tmp = new createjs.text("t", "12px 'source sans pro'", "#54555c"); tmp.textalign = 'center'; tmp.x = 180; tmp.y=350; tmp.text = "按任意键重新开始游戏"; stage.addchild(tmp); animate(); }
上面初始化了一个stage,用于存放特效,一个textstage,用于形成“failed”的像素图片。还有一个按任意键重新游戏的提示。同时开始每隔一段时间就刷新stage。
根据block的位置来初始化小圆点。
1 function initcircles() { 2 circles = []; 3 var p=[]; 4 var count=0; 5 for(let i=0; i<tetris_rows;i++) 6 for(let j=0;j<tetris_cols;j++) 7 if(tetris_status[i][j]!=no_block) 8 p.push({'x':j*cell_size+2, 'y':i*cell_size+2, 'w':cell_size-3, 'h':cell_size-4}); 9 for(var i=0; i<250; i++) { 10 var circle = new createjs.shape(); 11 var r = 7; 12 //x和y范围限定在黑色block内 13 var x = p[count]['x']+p[count]['w']*math.random(); 14 var y = p[count]['y']+p[count]['h']*math.random(); 15 count++; 16 if(count>=p.length) 17 count=0; 18 var color = colors[math.floor(i%colors.length)]; 19 var alpha = 0.2 + math.random()*0.5; 20 circle.alpha = alpha; 21 circle.radius = r; 22 circle.graphics.beginfill(color).drawcircle(0, 0, r); 23 circle.x = x; 24 circle.y = y; 25 circles.push(circle); 26 stage.addchild(circle); 27 circle.movement = 'float'; 28 tweencircle(circle); 29 } 30 }
然后再讲显示特效failed的createtext()。先将failed的text显示在textstage里,然后ctx.getimagedata.data获取像素数据,并以此来为每个小圆点定义位置。
1 function createtext(t) { 2 curtext=t; 3 var fontsize = 500/(t.length); 4 if (fontsize > 80) fontsize = 80; 5 text.text = t; 6 text.font = "900 "+fontsize+"px 'source sans pro'"; 7 text.textalign = 'center'; 8 text.x = tetris_cols*cell_size/2; 9 text.y = 0; 10 textstage.addchild(text); 11 textstage.update(); 12 13 var ctx = document.getelementbyid('text').getcontext('2d'); 14 var pix = ctx.getimagedata(0,0,600,200).data; 15 textpixels = []; 16 for (var i = pix.length; i >= 0; i -= 4) { 17 if (pix[i] != 0) { 18 var x = (i / 4) % 600; 19 var y = math.floor(math.floor(i/600)/4); 20 if((x && x%8 == 0) && (y && y%8 == 0)) textpixels.push({x: x, y: y}); 21 } 22 } 23 24 formtext(); 25 textstage.clear();//清楚text的显示 26 }
跟着代码的节奏走,我们现在来到了formtext.
1 function formtext() { 2 for(var i= 0, l=textpixels.length; i<l; i++) { 3 circles[i].originx = offsetx + textpixels[i].x; 4 circles[i].originy = offsety + textpixels[i].y; 5 tweencircle(circles[i], 'in'); 6 } 7 textformed = true; 8 if(textpixels.length < circles.length) { 9 for(var j = textpixels.length; j<circles.length; j++) { 10 circles[j].tween = tweenlite.to(circles[j], 0.4, {alpha: 0.1}); 11 } 12 } 13 }
explode()就是讲已组成字的小圆点给重新遣散。
动画实现是使用了tweenlite.
1 function tweencircle(c, dir) { 2 if(c.tween) c.tween.kill(); 3 if(dir == 'in') { 4 /*tweenlite.to 改变c实例的x坐标,y坐标,使用easeinout弹性函数,透明度提到1,改变大小,radius,总用时0.4s*/ 5 c.tween = tweenlite.to(c, 0.4, {x: c.originx, y: c.originy, ease:quad.easeinout, alpha: 1, radius: 5, scalex: 0.4, scaley: 0.4, oncomplete: function() { 6 c.movement = 'jiggle';/*轻摇*/ 7 tweencircle(c); 8 }}); 9 } else if(dir == 'out') { 10 c.tween = tweenlite.to(c, 0.8, {x: window.innerwidth*math.random(), y: window.innerheight*math.random(), ease:quad.easeinout, alpha: 0.2 + math.random()*0.5, scalex: 1, scaley: 1, oncomplete: function() { 11 c.movement = 'float'; 12 tweencircle(c); 13 }}); 14 } else { 15 if(c.movement == 'float') { 16 c.tween = tweenlite.to(c, 5 + math.random()*3.5, {x: c.x + -100+math.random()*200, y: c.y + -100+math.random()*200, ease:quad.easeinout, alpha: 0.2 + math.random()*0.5, 17 oncomplete: function() { 18 tweencircle(c); 19 }}); 20 } else { 21 c.tween = tweenlite.to(c, 0.05, {x: c.originx + math.random()*3, y: c.originy + math.random()*3, ease:quad.easeinout, 22 oncomplete: function() { 23 tweencircle(c); 24 }}); 25 } 26 } 27 }
tweenlite.to函数第一个参数,要做动画的实例,第二个参数,事件,第三个参数,动画改变参数。
个人感言
其实刚开始没想做这么复杂,所以文件排的比较随意,然后就导致了后期项目完成时那副杂乱无章的样子。^_^,以后改。等我等看懂动画效果时在说,现在用的有点半懵半懂。
这篇博客写得有点乱。新手之作,就先这样吧。同上,以后改。因为不知道这个项目会不会拿来直接当我们计算机职业实践的作业。要是的话,我就彻改,连同博客。
以下是源代码地址。
还在审核。明天再说。相信我,我在小番茄里做了记录。
上一篇: 远程访问阿里云服务器jupyter