这两天闲着无事,写了几个Web游戏供自己打发时间。其中笔者感觉扫雷这个游戏的实现中涉及到的知识点比较全面,故在此和大家分享一下。
先放效果图:
首先我们要把基本的架子搭建起来:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>原生js实现扫雷小游戏</title>
<style>
*{margin:0;padding:0;}
.container{width:600px;height:600px;border:1px solid #ccc;margin:0 auto;}
.container .block{width:60px;height:60px;background:#abcdef;float:left;border:1px solid #f00;box-sizing:border-box;}
.container .block:active{background:#eee;}
.container .lei{}
/* 雷显示 */
.container .show{background:url('tim.jpg') no-repeat center center/ 100% 100%;cursor:pointer;}
/* 周围有雷的话显示数字 */
.container .number{background:#fff;text-align:center;line-height:60px;}
/* 红旗标记显示 */
.container .biaoji{background:url('qz.jpg') no-repeat center center/ 100% 100%;cursor:pointer;}
.ts0{position:fixed;width:100%;height:100vh;top:0;left:0;z-index:888;background:rgba(255,255,255,0);display:none;}
.ts{position:fixed;width:200px;height:60px;background:rgb(255,255,255);top:0;right:0;left:0;bottom:0;margin:auto;z-index:999;display:none;}
.ts p{text-align:center;line-height:60px;}
</style>
</head>
<body>
<h4>mxc-云风清</h4>
<div class="container">
</div>
<div class="ts0"></div>
<div class="ts">
<p>游戏结束!</p>
</div>
</body>
<script>
</script>
</html>
这并没有什么好说的 —— 事实上,它非常简单: 一个小标题(<h4>
)、一个“游戏区域”(class="container"
)、一个结束时的提示框(class="ts"
)、以及衬托提示框的蒙层(class="ts0"
)。
但你会发现,所谓“游戏区域”仅仅是一个拥有一个div的“空架子”?
这里笔者采用了JS动态渲染div元素(一个一个小方块),因为后面“剧情”的需要,我们要将每个小方块都设置【专属id】(id名):
let container=document.querySelector('.container');
for(let i=0;i<10;i++){
for(let j=0;j<10;j++){
let divObj=document.createElement('div');
divObj.classList.add('block');
divObj.id='a'+i+'_'+j; //设置专属id(id名)
container.appendChild(divObj);
}
}
我们很快便完成了这一代码。这样真的好吗? 现在只是10X10,如果是100X100呢?在大数据量循环中执行appendChild
可不是一个明智的决定!
在笔者关于JS性能优化的文章中提到过一种解决方案:createDocumentFragment()
:
let container=document.querySelector('.container');
let fragment=document.createDocumentFragment();
for(let i=0;i<10;i++){
for(let j=0;j<10;j++){
let divObj=document.createElement('div');
divObj.classList.add('block');
divObj.id='a'+i+'_'+j; //设置专属id(id名)
fragment.appendChild(divObj);
}
}
container.appendChild(fragment);
其实,原生中对元素的很多样式上的操作从性能上考虑最终都可归结到对classname的操作。
现在,我们画面上有了下面的样式:
该进行下一步了。
不过在此之前,我们需要找两张图片:
(我们不必关注他们的大小 —— 在css中改变即可)
做完上面的准备工作,正餐便开始了:
雷的分布:随机,且
上面笔者说:原生中对元素的很多样式上的操作从性能上考虑最终都可归结到对classname的操作,于是我们可以想到:为每个需要变成“雷”的小方块添加一个【特别的类】(.lei):
笔者采用循环实现:
let count=13;
let block=document.querySelectorAll('.block');
do{
let random=Math.floor(Math.random()*block.length);
block[random].classList.add('lei');
}while(document.querySelectorAll('.lei').length<=count);
看!这就体现出了do-while循环的好处了。
然后是处理鼠标事件:这将是最后一步了。
笔者发现身边不少学前端的同学在做事件处理时直接上去就是onclick、oninput、onmouse… 小一点的文件还好说,稍微对性能很高的网页或者说本游戏设置为10000X10000之后,浏览器就“吃不消”了 —— 因为在初始时对函数的加载太过庞大!
函数的优化:我们为什么不能遵循【惰性加载模式】,在触发时再去加载函数呢?
//block是前面获取过的变量——获取的“.block”,指“.container”中的每个小方块
block.forEach(function(item){
item.onclick=function(){ //鼠标左键事件
leftClick(item);
}
item.oncontextmenu=function(e){ //鼠标右键事件
//阻止浏览器默认鼠标右键事件
e.preventDefault();
rightClick(item);
}
})
xxx.oncontextmenu鼠标右键事件注意一下:平时说的像禁止鼠标右键、禁止复制粘贴(其中的一种)、f12弹出控制台都和这个有关
然后便是两个函数了:
鼠标左键的对应函数到没什么——判断点击处是不是雷(有没有.lei这个类)、如果不是,循环判断点击处周围8个小方块中有几个雷并显示出来:
function leftClick(obj){
if(obj.classList.contains('biaoji')){
return "";
}
if(obj.classList.contains('lei')){
let lei=document.querySelectorAll('.lei');
lei.forEach(function(item){
item.classList.add('show');
})
//结束时提示框及蒙层显示
document.querySelector('.ts0').style.display='block';
document.querySelector('.ts').style.display='block';
setTimeout(function(){let yes=prompt("是否刷新刷新进入下一关?");if(yes === "是"){window.location.reload()}},1700);
}else{
obj.classList.add('number');
let ids=obj.id;
//下面三行作用是将【专属id】(id名)拆分开来,以确定位置坐标
let arr=ids.split('_');
let x=Number(arr[0].substr(1));
let y=Number(arr[1]);
let num=0;
for(let i=x-1;i<=x+1;i++){
for(let j=y-1;j<=y+1;j++){
let objs=document.querySelector('#a'+i+'_'+j);
//这里也可以用“es6模板字符串”:let objs=document.querySelector(`#a${i}_${j}`);
if(objs && objs.classList.contains('lei')){
num++;
}
}
}
if(num){
obj.innerHTML=num;
}
if(num===0){
for(let i=x-1;i<=x+1;i++){
for(let j=y-1;j<=y+1;j++){
let objs=document.querySelector('#a'+i+'_'+j);
if(objs && !objs.classList.contains('number')){
leftClick(objs);
}
}
}
}
}
}
倒是鼠标右键:它是插红旗的
这里涉及到一个问题:插红旗和扫到雷不一样,你还可以取消。这问题说大不大,但也是个性能问题。
笔者查阅资料找到了一个函数:toggle()
—— 单次点击时操作一个函数,偶次点击时执行另一个函数(也可以在里面放一个类名,即单次点击时添加类名,偶次点击时取消类名):
function rightClick(obj){
if(!obj.classList.contains('number')){
obj.classList.toggle('biaoji');
}
let biaoji=document.querySelectorAll('.biaoji.lei');
let biaoji2=document.querySelectorAll('.biaoji');
if(biaoji.length===count && count===biaoji2.length){
document.querySelector('.ts0').style.display='block';
document.querySelector('.ts').style.display='block';
}
}
最后,笔者将本文代码上传至了百度网盘,需要者请自行下载:
链接:https://pan.baidu.com/s/1BSUhmvelBGy0FdEvFKe6vA
提取码:v0m8