自己写一个,开心消消乐!

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的基础知识,巩固知识点的同时,完成了一个小游戏,还是颇有成就感的。

  在贝塔版中,我计划美化一下显示面板,增加一个难度系数选择按钮,增加一个重新开始功能以及增加闯关机制。

  此外,在贝塔版中,我还准备重构部分代码,优化算法和代码逻辑,替换掉一些硬编码,删除部分死代码。

  总之,做自己真正热爱的事情,才会收获到加倍的快乐!

猜你喜欢

转载自www.cnblogs.com/chrischen98/p/10659336.html