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的基础知识,巩固知识点的同时,完成了一个小游戏,还是颇有成就感的。
在贝塔版中,我计划美化一下显示面板,增加一个难度系数选择按钮,增加一个重新开始功能以及增加闯关机制。
此外,在贝塔版中,我还准备重构部分代码,优化算法和代码逻辑,替换掉一些硬编码,删除部分死代码。
总之,做自己真正热爱的事情,才会收获到加倍的快乐!