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


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

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

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

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

  • 相关阅读:
    Mysql经常使用函数
    ZOJ 3690 Choosing number
    cocos2d-x 多触点监听
    ansible不配ssh连接,用户密码登录
    Ansible Role
    关闭centos自动升级内核
    让外部网络访问K8S service的四种方式
    Hadoop实战:Hadoop分布式集群部署(一)
    Docker:搭建私有仓库(Registry 2.4)
    Docker下的Spring Cloud三部曲之一:极速体验
  • 原文地址:https://www.cnblogs.com/chrischen98/p/10659336.html
Copyright © 2020-2023  润新知