導言
在一個風和日麗的一天,看完了瘋狂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"; }draw canvas
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 }change width and height
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 }draw block
游戲邏輯
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 }keydown事件監聽
其他的詳細情況可以看源代碼,我就不整理了,
接下來我們看游戲結束時的特效,因為我也不是很懂,所以在這里整理的會比較詳細,當做學習,
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(); }initAnim
上面初始化了一個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 }initCircles
然后再講顯示特效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 }CreateText
跟著代碼的節奏走,我們現在來到了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 }formtext
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函式第一個引數,要做影片的實體,第二個引數,事件,第三個引數,影片改變引數,
Quad.easeInOut()意思是在影片開始和結束時緩動, onComplete影片完成時呼叫的函式,易得,在我們的應用中,我們將開始下一次影片,個人感言
其實剛開始沒想做這么復雜,所以檔案排的比較隨意,然后就導致了后期專案完成時那副雜亂無章的樣子,^_^,以后改,等我等看懂影片效果時在說,現在用的有點半懵半懂,
這篇博客寫得有點亂,新手之作,就先這樣吧,同上,以后改,因為不知道這個專案會不會拿來直接當我們計算機職業實踐的作業,要是的話,我就徹改,連同博客,
以下是源代碼地址,(我還以為csdn的下載要的積分是自己定的,等我下次徹改的時候我傳到github上,現在將就一下)
https://download.csdn.net/download/qq_26136211/12011640
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/24919.html
標籤:HTML5
上一篇:js日期函式
下一篇:2020前端最新面試知識點匯總
