原生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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章