JavaScript實(shí)現(xiàn)俄羅斯方塊游戲過程分析及源碼分享
觀摩一下《編程之美》:“程序雖然很難寫,卻很美妙。要想把程序?qū)懞茫枰獙懞靡欢ǖ幕A(chǔ)知識,包括編程語言、數(shù)據(jù)結(jié)構(gòu)與算法。程序?qū)懙煤茫枰b密的邏輯思維能力和良好的梳理基礎(chǔ),而且熟悉編程環(huán)境和編程工具。”
學(xué)了幾年的計算機(jī),你有沒有愛上編程。話說,沒有嘗試自己寫過一個游戲,算不上熱愛編程。
俄羅斯方塊曾經(jīng)造成的轟動與造成的經(jīng)濟(jì)價值可以說是游戲史上的一件大事,它看似簡單但卻變化無窮,令人上癮。相信大多數(shù)同學(xué),曾經(jīng)為它癡迷得茶不思飯不想。
游戲規(guī)則
1、一個用于擺放小型正方形的平面虛擬場地,其標(biāo)準(zhǔn)大?。盒袑挒?0,列高為20,以每個小正方形為單位。
2、一組由4個小型正方形組成的規(guī)則圖形,英文稱為Tetromino,中文通稱為方塊共有7種,分別以S、Z、L、J、I、O、T這7個字母的形狀來命名。
I:一次最多消除四層
J(左右):最多消除三層,或消除二層
L:最多消除三層,或消除二層
O:消除一至二層
S(左右):最多二層,容易造成孔洞
Z (左右):最多二層,容易造成孔洞
T:最多二層
方塊會從區(qū)域上方開始緩慢繼續(xù)落下。玩家可以以90度為單位旋轉(zhuǎn)方塊,以格子為單位左右移動方塊,讓方塊加速落下。方塊移到區(qū)域最下方或是著地到其他方塊上無法移動時,就會固定在該處,而新的方塊出現(xiàn)在區(qū)域上方開始落下。當(dāng)區(qū)域中某一列橫向格子全部由方塊填滿,則該列會消失并成為玩家的得分。同時刪除的列數(shù)越多,得分指數(shù)上升。
分析與解法
每塊方塊落下的過程中,我們可以做:
1)旋轉(zhuǎn)到合適的方向
2)水平移動到某一列
3)垂直下落到底部
首先,需要用一個二維數(shù)組,area[18][10]表示18*10的游戲區(qū)域。其中,數(shù)組中值為0表示空,1表示有方塊。
方塊一共7種,每種有4種方向。定義activeBlock[4],在編譯之前這個數(shù)組的值預(yù)定算好,在程序中直接使用。
難點(diǎn)
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)旋轉(zhuǎn), 需要數(shù)理邏輯, 一個點(diǎn)相對另外一個點(diǎn)旋轉(zhuǎn)90度的問題。
3)定時和監(jiān)聽鍵盤事件機(jī)制讓游戲自動運(yùn)行下去。
//開始 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)用戶點(diǎn)開始->構(gòu)造一個活動圖形, 設(shè)置定時器。
//當(dāng)前活動的方塊, 它可以左右下移動, 變型。當(dāng)它觸底后, 將會更新area; var activeBlock; //生產(chǎn)方塊形狀, 有7種基本形狀。 function generateBlock(){ activeBlock = null; activeBlock = new Array(4); //隨機(jī)產(chǎn)生0-6數(shù)組,代表7種形態(tài)。 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; } } //檢查剛生產(chǎn)的四個小方格是否可以放在初始化的位置. 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)完了之后再構(gòu)造一個活動圖形, 再設(shè)置定時器。
效果圖
有待優(yōu)化
1)設(shè)置不同形狀方塊的顏色。
思路:在創(chuàng)建方塊函數(shù)內(nèi),設(shè)定activeBlockColor顏色,七種不同形態(tài)方塊顏色各異(除了修改generateBlock方法之外,還需要修改paintarea方法。因?yàn)橐婚_始考慮不周全,消除一行后,重繪方塊的同時將顏色統(tǒng)一,因此可以考慮移除表格n行,然后在頂部增添n行,以保證沒消除方塊的完整性)。
2)當(dāng)當(dāng)前方塊下落時,可以提前查看下一個方塊。
思路:將generateBlock方法拆分成兩部分,一部分用于隨機(jī)嘗試下一個方塊,一部分用于緩存當(dāng)前所要描繪的方塊。當(dāng)當(dāng)前方塊碰到底部被固定后,下一方塊開始描繪,同時又再次隨機(jī)產(chǎn)生新方塊。如此反復(fù)。
完整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; //游戲狀態(tài) 0: 未開始;1 運(yùn)行; 2 中止; var status = 0; //定時器, 定時器內(nèi)將做moveDown操作 var timer; //分?jǐn)?shù) var score = 0; //area是一個18*10的數(shù)組,也和頁面的table對應(yīng)。初始時都為0, 如果被占據(jù)則為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; } } //當(dāng)前活動的方塊, 它可以左右下移動, 變型。當(dāng)它觸底后, 將會更新area; var activeBlock; //生產(chǎn)方塊形狀, 有7種基本形狀。 function generateBlock(){ activeBlock = null; activeBlock = new Array(4); //隨機(jī)產(chǎn)生0-6數(shù)組,代表7種形態(tài)。 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; } } //檢查剛生產(chǎn)的四個小方格是否可以放在初始化的位置. for(var i=0; i<4; i++){ if(!isCellValid(activeBlock[i].x, activeBlock[i].y)){ return false; } } return true; } //向下移動 function moveDown(){ //檢查底邊界. if(checkBottomBorder()){ //沒有觸底, 則擦除當(dāng)前圖形, erase(); //更新當(dāng)前圖形坐標(biāo) for(var i=0; i<4; i++){ activeBlock[i].x = activeBlock[i].x + 1; } //重畫當(dāng)前圖形 paint(); } //觸底, else{ //停止當(dāng)前的定時器, 也就是停止自動向下移動. clearInterval(timer); //更新area數(shù)組. updatearea(); //消行 var lines = deleteLine(); //如果有消行, 則 if(lines!=0){ //更新分?jǐn)?shù) score = score + lines*10; updateScore(); //擦除整個面板 erasearea(); //重繪面板 paintarea(); } //產(chǎn)生一個新圖形并判斷是否可以放在最初的位置. if(!generateBlock()){ alert("Game over!"); status = 2; return; } paint(); //定時器, 每隔一秒執(zhí)行一次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(); } } //旋轉(zhuǎn), 因?yàn)樾D(zhuǎn)之后可能會有方格覆蓋已有的方格. //先用一個tmpBlock,把a(bǔ)ctiveBlock的內(nèi)容都拷貝到tmpBlock, //對tmpBlock嘗試旋轉(zhuǎn), 如果旋轉(zhuǎn)后檢測發(fā)現(xiàn)沒有方格產(chǎn)生沖突,則 //把旋轉(zhuǎn)后的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; } //先算四個點(diǎn)的中心點(diǎn),則這四個點(diǎn)圍繞中心旋轉(zhuǎn)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); //旋轉(zhuǎn)的主要算法. 可以這樣分解來理解。 //先假設(shè)圍繞源點(diǎn)旋轉(zhuǎn)。然后再加上中心點(diǎn)的坐標(biāo)。 for(var i=0; i<4; i++){ tmpBlock[i].x = cx+cy-activeBlock[i].y; tmpBlock[i].y = cy-cx+activeBlock[i].x; } //檢查旋轉(zhuǎn)后方格是否合法. 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; } //檢查坐標(biāo)為(x,y)的是否在area種已經(jīng)存在, 存在說明這個方格不合法。 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數(shù)組 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"; } } } } //產(chǎn)生一個空白行. function generateBlankLine(){ var line = new Array(10); for(var i=0; i<10; i++){ line[i] = 0; } return line; } //更新分?jǐn)?shù) 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;
相關(guān)文章
js實(shí)現(xiàn)類選擇器和name屬性選擇器的示例步驟
這篇文章主要介紹了js實(shí)現(xiàn)類選擇器和name屬性選擇器的示例步驟,幫助大家更好的理解和使用js,感興趣的朋友可以了解下2021-02-02bootstrap表格內(nèi)容過長時用省略號表示的解決方法
這篇文章主要介紹了bootstrap表格內(nèi)容過長時用省略號表示的解決方法,需要的朋友可以參考下2017-11-11JS實(shí)現(xiàn)單擊輸入框彈出選擇框效果完整實(shí)例
這篇文章主要介紹了JS實(shí)現(xiàn)單擊輸入框彈出選擇框效果的方法,涉及JavaScript響應(yīng)鼠標(biāo)事件動態(tài)操作頁面元素與相關(guān)屬性的實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-12-12JS實(shí)現(xiàn)從頂部下拉顯示的帶動畫QQ客服特效代碼
這篇文章主要介紹了JS實(shí)現(xiàn)從頂部下拉顯示的帶動畫QQ客服特效代碼,可實(shí)現(xiàn)彈性緩沖效果的彈出QQ客服窗口的功能,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10如何解決js函數(shù)防抖、節(jié)流出現(xiàn)的問題
這篇文章主要介紹了如何解決js函數(shù)防抖、節(jié)流出現(xiàn)的問題。SyntheticEvent對象是通過合并得到的。 這意味著在事件回調(diào)被調(diào)用后,SyntheticEvent 對象將被重用并且所有屬性都將被取消。 因此,您無法以異步方式訪問該事件。,需要的朋友可以參考下2019-06-06JS與HTML結(jié)合實(shí)現(xiàn)流程進(jìn)度展示條思路詳解
基于js與html相結(jié)合實(shí)現(xiàn)的流程進(jìn)度展示條,非常實(shí)用,在各大網(wǎng)站都可以用到,下面小編給大家?guī)砹薐S與HTML結(jié)合實(shí)現(xiàn)流程進(jìn)度展示條思路詳解,需要的朋友參考下吧2017-09-09