原生JS小游戏:从0实现一个扫雷游戏

这两天闲着无事,写了几个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

发布了200 篇原创文章 · 获赞 417 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/qq_43624878/article/details/104568753