自己寫一個,開心消消樂!


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的基礎知識,鞏固知識點的同時,完成了一個小游戲,還是頗有成就感的。

  在貝塔版中,我計划美化一下顯示面板,增加一個難度系數選擇按鈕,增加一個重新開始功能以及增加闖關機制。

  此外,在貝塔版中,我還准備重構部分代碼,優化算法和代碼邏輯,替換掉一些硬編碼,刪除部分死代碼。

 

  總之,做自己真正熱愛的事情,才會收獲到加倍的快樂!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM