0 寫在前面
4月第一周,我並沒有被善待~~上個周末才進行了一次馮如杯四審答辯,這周3馬上又跟了一次五審答辯…
從上個周五就開始准備四審,答完之后以為可以松口氣了,誰料五審來的如此突然。而且更令人煩惱的是,負責我自己那個項目的指導學姐,讓我反復修改答辯的PPT和講稿。說實話,我認為項目的成功,關鍵還在於完成的質量,和核心科技含量,而不是在展示上徒增一些花里胡哨卻無關緊要的噱頭。好在北航的老師水平還是高超的,給了我不少寶貴的修改建議,還親自幫我修改了PPT,這點我是非常感動的。既然我們無論如何努力,都無法讓所有人都能滿意,那么,我們就不妨舍棄掉那些不專業的人所給出不專業的建議就好了,聰明的人,內心自要對形勢有着清晰的判斷。
扯遠了,為自己這一周沒怎么寫東西找點借口。
好在終於迎來了幾天假期,可以好好規划一下自己的生活,學點自己喜歡的知識。
昨晚看到一個很好玩的游戲--開心消消樂。實現的邏輯非常的清晰簡潔,采用純原生JS打造,我就非常想自己也實現一個。通過自己編寫這樣一個好玩的小游戲,主要鞏固練習了以下幾個方面的知識點:
- 鼠標事件的響應
- 連通圖算法
感興趣的朋友可以點擊博客右上角進入我的github。
也可以點擊這里下載源代碼進行試玩。
1 需求分析
1-1 初始化
在初始化階段,我們需要初始化以下內容:
- 初始化背景
- 初始化星星小方塊
- 初始化分數等顯示面板
1-2 鼠標移入事件
完成初始化后,當如表移入星星區域時,需要利用連通圖算法判斷當前鼠標位置處的星星連通情況:
- 取消原有動畫效果
- 判斷連通情況
- 連通區域星星閃爍
- 計算分數並顯示
1-3 鼠標點擊事件
當用戶點擊可消星星時,需要響應該點擊事件:
- 連通的星星被消除
- 下落或左移以補充空缺
- 分數累加
1-4 游戲結束的判斷
- 當無連通的星星時,游戲結束
- 當分數超過了目標分數,顯示闖關成功
2 實現過程
2-1 初始化
2-1-1 初始化背景
這里就是一些簡單的html與css寫法。
練習一個屬性 background-size:
background-size:cover; 會按照圖片原有比例去覆蓋區域,超出部分可能被裁掉,因此不一定能看到完整圖像,這里我們采用的是cover。
而background-size:100%; 則會將圖片撐滿整個區域,圖像完整性得以保持,但是圖像比例可能發生改變。
【html代碼】
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <meta http-equiv="X-UA-Compatible" content="ie=edge"> 7 <title>Get Those Stars!</title> 8 <link rel="stylesheet" href="index.css"> 9 <script src="index.js"></script> 10 </head> 11 <body> 12 <div id="pop_star"> 13 <div id="targetScore">Target Score : 2000</div> 14 <div id="nowScore">Current Score : 0</div> 15 <div id="selectScore">0 blocks 0 scores</div> 16 </div> 17 </body> 18 </html>
【css代碼】
1 *{ 2 margin:0; 3 padding:0; 4 } 5 html,body{ 6 height: 100%; 7 width: 100%; 8 } 9 /* 以上為常用頁面初始化 */ 10 #pop_star{ 11 height: 100%; 12 width: 500px; 13 margin: 0 auto; 14 background: url("./pic/background.png"); 15 position: relative; /*父元素,為了使之后的子元素都相對於他進行定位,此處設為relative*/ 16 color:white; 17 background-size: cover; /*使背景圖片保持比例覆蓋整個背景區域*/ 18 } 19 /* 以下三個元素為現實面板,其樣式相同 */ 20 #targetScore{ 21 width: 100%; 22 height: 50px; 23 position: relative; 24 line-height: 50px; 25 text-align: center; 26 font-size: 20px; 27 background-size: cover; 28 } 29 30 #nowScore{ 31 width: 100%; 32 height: 50px; 33 position: relative; 34 line-height: 50px; 35 text-align: center; 36 font-size: 20px; 37 background-size: cover; 38 } 39 40 #selectScore{ 41 width: 100%; 42 height: 50px; 43 position: relative; 44 line-height: 50px; 45 text-align: center; 46 font-size: 20px; 47 background-size: cover; 48 opacity:0; 49 }
2-1-2 初始化星星
在初始化部分,我采用JS來編寫樣式。
這里練習了一個比較有技巧的樣式:boxSizing:border-box;
boxSizing:border-box;實現了將border限制在元素區域內,不會溢出覆蓋到周圍其他元素。
【初始化部分代碼】
1 var table; //游戲桌面 2 var squareWidth = 50; //方塊寬高 3 var boardWidth = 10; //行列數 4 var squareSet = []; //方塊信息集合(二維數組)每個元素保存該方塊的全部信息 5 var baseScore = 5; //第一塊的分數 6 var stepScore = 10; //每多一塊的累加分數 7 var totalScore = 0; //當前總分 8 var targetScore = 1500; //目標分 9 10 function refresh(){ //重繪畫板,每次鼠標點擊后刷新 11 for(var i = 0 ; i < squareSet.length ; i ++){ 12 for(var j = 0 ; j < squareSet[i].length ; j ++){ 13 if(squareSet[i][j] == null) continue; // 點擊后數組中可能有空值需要跳過 14 squareSet[i][j].row = i; //更新當前的行列數 15 squareSet[i][j].col = j; 16 squareSet[i][j].style.backgroundImage = "url(./pic/" + squareSet[i][j].num + ".png)" 17 squareSet[i][j].style.backgroundSize = "cover"; //占滿范圍 18 squareSet[i][j].style.transform = "scale(0.95)"; //美觀效果讓不同星星之間留出空隙(縮小至0.95倍大小) 19 squareSet[i][j].style.left = squareSet[i][j].col * squareWidth + "px"; // 別忘了加"px" 20 squareSet[i][j].style.bottom = squareSet[i][j].row * squareWidth + "px"; 21 } 22 } 23 } 24 25 function createSquare(value,row,col){ //創建小方塊,傳入參數為顏色、行、列,初始化時使用。 26 var temp = document.createElement('div'); //創建div dom對象 27 temp.style.height = squareWidth + "px"; 28 temp.style.width = squareWidth + "px"; 29 temp.style.display = "inline-block"; //需要讓對象元素能排列一排 30 temp.style.position = "absolute"; //相對於背景絕對定位 31 temp.style.boxSizing = "border-box"; //重要:不會使增加的邊框溢出覆蓋到旁邊的元素 32 temp.style.borderRadius = "12px"; 33 temp.num = value; 34 temp.col = col; 35 temp.row = row; 36 return temp; //返回這個創建出來的對象 37 } 38 39 function init(){ // JS調用入口 40 table = document.getElementById('pop_star'); // 獲取到最外層的父元素作為桌面 41 document.getElementById('targetScore').innerHTML = "Target Score : " + targetScore; //顯示目標分數用innerHTML 42 // 循環初始化星星區域 43 for(var i = 0 ; i < boardWidth ; i ++){ 44 squareSet[i] = new Array(); //二維數組的創建,對每一個元素new Array()創建新數組 45 for(var j = 0 ; j < boardWidth ; j ++){ 46 var square = createSquare(Math.floor(Math.random() * 5) , i , j); 47 48 squareSet[i][j] = square; //必須將新創建的方塊放回到數組中 49 table.appendChild(square); //需要將創建的新元素添加到桌面上 50 } 51 52 } 53 refresh(); //每次頁面內容發生變化需要重繪頁面 54 } 55 56 window.onload = function(){ 57 init(); 58 } // window.onload 保證了在頁面全部加載完畢后再執行JS代碼
2-1-3 效果
2-2 鼠標移入事件
2-2-1 實現細節
首先,在init函數中雙層循環的內層產生完小方塊后,即可添加移入和點擊兩個事件的調用函數了。
1 square.onmouseover = function(){ 2 mouseOver(this); 3 }
隨后,按照思路,逐層編寫函數。
先寫出mouseOver函數的整體邏輯框架,發現需要:還原樣式、判斷相鄰、閃爍和顯示分數四個部分。於是緊接着按序編寫這四個部分的函數。
重點是在這里,我練習了連通圖的判定算法,這里采用了遞歸實現。
此外在閃爍方法中,運用一個數學技巧,即scale(0.9+-0.05)的方式實現了大小交替變換的閃爍效果。
另外練習了定時器setInterval(function(){},time);
以及setTimeout(function(){},time);兩個方法的用法,注意體會。
1 var choose = []; //選中的連通小方塊 2 var timer = null; //閃爍定時器 3 var flag = true; //鎖,防止點擊事件中響應其他點擊或移入時間 4 var tempSquare = null; //臨時方塊 5 6 function goBack(){ //還原樣式 7 if(timer != null){ //清空計時器 8 clearInterval(timer); 9 } 10 for(var i = 0 ; i < squareSet.length ; i ++){ 11 for(var j = 0 ; j < squareSet[i].length ; j ++){ 12 if(squareSet[i][j] == null) continue; 13 squareSet[i][j].style.border = "0px solid white"; 14 squareSet[i][j].style.transform = "scale(0.95)"; 15 } 16 } 17 } 18 19 function checkLinked(square , arr){ // 遞歸連通圖算法 20 if(square == null) return; // 遞歸邊界 21 arr.push(square); // 將當前方塊放入選中數組中 22 // check left 23 if( square.col > 0 && //未到邊界 24 squareSet[square.row][square.col - 1] && //左側有塊 25 squareSet[square.row][square.col - 1].num == square.num && //顏色相同 26 arr.indexOf(squareSet[square.row][square.col - 1]) == -1) { //不在choose中,避免循環判斷 27 checkLinked(squareSet[square.row][square.col - 1] , arr); 28 } 29 // check right 30 if( square.col < boardWidth - 1 && 31 squareSet[square.row][square.col + 1] && 32 squareSet[square.row][square.col + 1].num == square.num && 33 arr.indexOf(squareSet[square.row][square.col + 1]) == -1) { 34 checkLinked(squareSet[square.row][square.col + 1] , arr); 35 } 36 // check up 37 if( square.row < boardWidth - 1 && 38 squareSet[square.row + 1][square.col] && 39 squareSet[square.row + 1][square.col].num == square.num && 40 arr.indexOf(squareSet[square.row + 1][square.col]) == -1) { 41 checkLinked(squareSet[square.row + 1][square.col] , arr); 42 } 43 // check down 44 if( square.row > 0 && 45 squareSet[square.row - 1][square.col] && 46 squareSet[square.row - 1][square.col].num == square.num && 47 arr.indexOf(squareSet[square.row - 1][square.col]) == -1) { 48 checkLinked(squareSet[square.row - 1][square.col] , arr); 49 } 50 } 51 52 function flicker(arr){ // 選中連通的小方塊可以閃爍 53 var num = 0; 54 timer = setInterval(function(){ 55 for(var i = 0 ; i < arr.length ; i ++){ 56 arr[i].style.border = "3px solid #BFEFFF"; 57 arr[i].style.transform = "scale(" + (0.9 + (0.05 * Math.pow(-1 , num))) + ")"; 58 } 59 num ++; // 注意這里所采用的數學技巧,仍然使用transform:scale(val)來進行縮放。 60 },300); 61 } 62 63 function selectScore(){ //可以顯示當前選中小方塊的得分 64 var score = 0; 65 for(var i = 0 ; i < choose.length ; i ++){ 66 score += (baseScore + i * stepScore); 67 } 68 if(score == 0) return; 69 document.getElementById('selectScore').innerHTML = choose.length + " blocks " + score + " points"; 70 document.getElementById('selectScore').style.opacity = 1; 71 document.getElementById('selectScore').style.transition = null; 72 // 設置時間間隔1秒后顯示消失的過渡動畫 73 setTimeout(function(){ 74 document.getElementById('selectScore').style.opacity = 0; 75 document.getElementById('selectScore').style.transition = "opacity 1s"; 76 },1000); 77 } 78 79 function mouseOver(obj){ 80 // 加鎖,點擊事件過程中不允許其他點擊事件與移入事件 81 if(!flag){ 82 tempSquare = obj; 83 return; 84 } 85 // 還原所有樣式 86 goBack(); 87 // 檢查相鄰 88 choose = []; 89 checkLinked(obj , choose); 90 if(choose.length <= 1){ 91 choose = []; 92 return; 93 } 94 // 閃爍 95 flicker(choose); 96 // 顯示分數 97 selectScore(); 98 }
2-2-2 效果
2-3 鼠標點擊事件
2-3-1 實現細節
點擊響應時,需要先對鎖進行判斷與控制。
若已鎖,則直接返回,否則,可以繼續完成更新分數、完成星星消除、消除后的移動以及游戲結束的判斷。
為了給星星消除增加一個延遲動畫,這里采用循環設置定時器,但由於產生閉包,導致定時器不能按間隔變化,只能取到循環最終的值。
因此為了消除閉包,需要采用立即執行函數。
控制代碼如下:
1 // 鼠標點擊事件 2 square.onclick = function(){ 3 //對鎖進行控制 4 if(!flag || choose.length == null){ 5 return; 6 } 7 flag = false; 8 tempSquare = null; 9 //更新分數 10 var score = 0; 11 for(var i = 0 ; i < choose.length ; i ++){ 12 score += (baseScore + i * stepScore); 13 } 14 totalScore += score; 15 document.getElementById('nowScore').innerHTML = "Current Score : " + totalScore; 16 //為移除增加一個延遲動畫,為了防止閉包,這里采用立即執行函數 17 for(var i = 0 ; i < choose.length ; i ++){ 18 (function(i){ 19 setTimeout(function(){ 20 squareSet[choose[i].row][choose[i].col] = null; //為狀態數組置空 21 table.removeChild(choose[i]); //將其從桌面上移除 22 } , i * 100); 23 })(i); 24 } 25 //需要等星星消除完畢后再移動,故需增加一個延遲 26 setTimeout(function(){ 27 move(); //調用移動函數 28 },choose.length * 100); 29 }
為了對星星的下落移動進行控制,這里采用快慢指針算法。
橫向移動在循環遍歷時采用了一個技巧:
只判斷最底層是否有元素為null即可。
此外這里練習了splice的用法:
Array.splice(index,num);表示在數組Array中,刪除從index開始的num個元素。
一定要注意橫向移動循環結束條件的判斷!因為刪除元素后數組長度是變化的。
最后,別忘了重繪桌面調用refresh();
1 function move(){ 2 //縱向下落,采用快慢指針算法 3 for(var i = 0 ; i < boardWidth ; i ++){ 4 var pointer = 0; //慢指針 5 for(var j = 0 ; j < boardWidth ; j ++){ 6 if(squareSet[j][i] != null){ //按行遍歷 7 if(pointer != j){ //快慢指針不同步說明中間有空元素 8 squareSet[pointer][i] = squareSet[j][i]; //慢指針設成快指針元素 9 squareSet[j][i] = null; //快指針處置空 10 } 11 pointer ++; //該行非空時慢指針增加 12 } 13 } 14 } 15 // 橫向移動(當出現一列為空時) 16 for(var i = 0 ; i < squareSet[0].length ;){ //必須注意循環結束條件的判斷 17 if(squareSet[0][i] == null){ //邏輯:只需判斷最低層為空,該行則全為空 18 for(var j = 0 ; j < boardWidth ; j ++){ 19 squareSet[j].splice(i , 1); //splice刪除數組squareSet[j]中從i開始的1個元素 20 } 21 continue;//注意移動后i不應改變了 22 } 23 i ++; 24 } 25 refresh(); 26 }
2-3-2 效果
2-4 游戲結束的判斷
2-4-1 實現細節
結束時調用結束判斷函數,若結束,則返回勝負判斷結果,否則對鎖和連通數組重置,並處理潛在沖突。
1 //需要等星星消除完畢后再移動,故需增加一個延遲 2 setTimeout(function(){ 3 move(); //調用移動函數 4 setTimeout(function(){ 5 var judge = isFinish(); 6 if(judge){ //游戲達到結束條件 7 if(totalScore > targetScore){ 8 alert('Congratulations! You win!'); 9 } 10 else{ 11 alert('Mission Failed!'); 12 } 13 } 14 else{ 15 flag = true; 16 choose = []; 17 mouseOver(tempSquare); //處理可能存在的沖突 18 } 19 },300 + choose.length * 75); //需要一個判斷延遲 20 },choose.length * 50);
判斷結束函數,重要:必須解除鎖
以便后續鼠標事件可以被響應。
1 function isFinish(){ //判斷游戲結束 2 flag = true; //重要:需要先解鎖,保證后續鼠標事件可以被響應 3 for(var i = 0 ; i < squareSet.length ; i ++){ 4 for(var j = 0 ; j < squareSet[i].length ; j ++){ 5 if(squareSet[i][j] == null) continue; //遍歷每一元素判斷連通 6 var temp = []; 7 checkLinked(squareSet[i][j] , temp); 8 if(temp.length > 1) return false; 9 } 10 } 11 return flag; 12 }
2-4-2 效果
3 后記
完成這個開心消消樂用掉了一天的時間,其中遇到了許多困難,但是經過一步步調試,最后還是成功完成了阿爾法版。
在這里面練習到了許多js和css3的基礎知識,鞏固知識點的同時,完成了一個小游戲,還是頗有成就感的。
在貝塔版中,我計划美化一下顯示面板,增加一個難度系數選擇按鈕,增加一個重新開始功能以及增加闖關機制。
此外,在貝塔版中,我還准備重構部分代碼,優化算法和代碼邏輯,替換掉一些硬編碼,刪除部分死代碼。
總之,做自己真正熱愛的事情,才會收獲到加倍的快樂!