1. 程式人生 > >Js版遊戲打磚塊開發過程詳細

Js版遊戲打磚塊開發過程詳細

最近對js的小遊戲開發來了興趣,前段時間由於回答度娘知道的提問寫了個貪吃蛇,雖然難度不大並不複雜,感覺還挺有意思。感覺小時候玩過的什麼俄羅斯方塊,坦克大戰什麼的都可以試著用js實現下,這天來了興致又想寫一個,其實我小時候最喜歡玩的遊戲就是打磚塊了,當時五年級時在學校上微機課時總是在那偷偷玩打磚塊還有個雪地的保齡球還有個潛艇在深海的遊戲,都忘了名字了,玩兒的不亦樂乎。可能叫法不一樣,就是下圖這種,想必大家都玩兒過,這裡就不廢話了

瞭解需求

大家玩打磚塊都是一關一關過的,每一關(這裡就打算做一關)磚塊碼成一個圖形保持不變,飛球起於擋板彈起按直線遠動,不受重力約束,遇到牆壁則按反射角方向反彈。遇到磚塊則磚塊消失繼續反彈,同時計分,下落回來時需控制下方的擋板左右移動去接住飛球,繼續彈起,磚塊被打完或者飛球沒接住則遊戲結束。再複雜點打掉磚塊時偶爾還會有道具下落,用平臺接住後道具作用生效。還有那個底部的擋板有點特殊,我記得小時候玩的時候如果球打在了擋板的左方,不管球從哪個方向飛回來的都會繼續反彈回左方,越偏左反彈的越厲害,右邊同理。遊戲的執行需求大概就是這些。

這裡用js去寫的話我還真沒寫過,但可以試試,本文算是邊寫程式碼邊記錄部落格,如果覺得廢話太多可以直接看最後的完整程式碼。

開發思路

首先我想到的執行原理大概是磚塊都是一個個的div,飛球是會移動的div,如果飛球的div與磚塊的div有重合,則磚塊消失即消除掉這個html元素,磚塊的座標,飛球的座標應該都是用div的position設為absolute,控制left,top座標來操作的。具體飛球的移動位置應該是用向量去計算。大概就是這個思路,感覺應該不是太難。不知道是否有更好的方法去實現,也查閱過一些其它人的程式碼,基本都是黑色一片,綠色註釋一句沒有,讓人有點費解。乾脆先自己做一個再說。

搭建模型

首先先做個不會動的模型出來,四壁,磚塊,球,擋板,計分板,首先就是四壁,畫個div,這裡div的width,height取值上我是這樣想,既然這個遊戲按座標去執行會涉及到很多的計算。長寬最好取個整數,而不是用百分數。當然不要設的太大,最好開啟網頁就能全部顯示沒有滾動條,這樣方便玩。這裡我設的是600*600居中顯示。程式碼如下:

<body style="background-color:grey;">
	<div align="center">
		<div style="width:600px;height:600px;background-color:#BFEFFF;border:5px groove #87CEFA;position:relative;" id="mainDiv">
			
		</div>
	</div>
<body>
執行一下如下:

然後是磚塊,就是一個個div用固定位置拼上去,但得先想好個形狀。記得小時候玩的時候最大的樂趣就是努力讓飛球能從一個缺口上去在上面自己飛行。想來想去大蓋想到這麼個形狀:

上面是兩個對稱的三角形,中間是兩個豎排,下面是大個的倒三角,有點笑臉的感覺,這樣可以讓玩者努力將飛球從笑臉的嘴角處飛上去,如果幸運點能探入兩個豎排中間來回打擊。再網上飛到眼鏡處就不弄那麼容易了,眼鏡是向下斜坡的飛球不好在上面佔時間太長,不管理論能不能實現就這樣寫吧,依舊使用了貪吃蛇的風格,用js去自己載入這些div.

這些div都是固定的,用left top來固定座標。為了實現對稱居中效果,四壁的寬是600,那左眼處的最上方第一個div中心距離應該是四分之一處,取150.至於磚塊的長寬,我取的是width:28px;height:13px;因為左右上下我都加了1px做縫隙,這樣可以讓每個磚塊能獨立出來顯示分明,有人說給每個div加上border不用行了麼。這裡我試了有的瀏覽器預設border算在寬度裡,即加了border寬度還是28,有的瀏覽器加了border則寬度變為了30.所有這裡就不用border了。這樣加上縫隙的話每個磚塊站位剛好為整數30*15;每下一層比上一層多一個。第一層的左側距離左壁是150 - (30/2)等於145,每下一層靠近左側15。到第九層時靠左側10.剛好留了個空隙。下面的排列基本都是邊寫變推算。不再傲述。

畫完磚塊再畫兩個div,一個擋板,一個飛球。均居中底部顯示,還有一個計分板放最上面居中,座標left,top可以自己算出程式碼如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title></title>
<style>
#mainDiv div{
	width:28px;height:13px;background-color:blue;position:absolute;
}
</style>
<script>
var $ = function (id) {//方便按id提取
	return document.getElementById(id);
};
window.onload=function(){
	var x =150;var y = 15;
	var m = $("mainDiv");
	
	for(var i=1;i<=9;i++){
		
		for(var j=0;j<i;j++){//左眼媚
			var di = document.createElement("div");
			di.style.top = y+(i-1)*15;//每列向下移動一行
			di.style.left= x-i*15+j*30;//每列向左移動15,同行的每一個向右移動一個div寬度30
			m.appendChild(di);//即i控制列的換行,j控制每行的輸出這裡寫了個x,y其實就是從上向下畫圖時的起點,下面加150,加450等其實是換位置畫圖的意思,完全可以直接用算好的數字而不用x,y。只為清楚計算過程
			//右眼媚
			var di1 = document.createElement("div");
			di1.style.top = y+(i-1)*15;
			di1.style.left= x+300-i*15+j*30;
			m.appendChild(di1);
		}
		
	}
	
	for(var i=1;i<=10;i++){//左眼眼睛處不用j去控制每行,因為每行的個數位置是一樣的
			var di = document.createElement("div");
			di.style.top = y+150+(i-1)*15;
			di.style.left= x-15;
			m.appendChild(di);
			//右眼
			var di1 = document.createElement("div");
			di1.style.top = y+150+(i-1)*15;
			di1.style.left= x-15+300;
			m.appendChild(di1);		
	}
	
	for(var i=1;i<=10;i++){//大嘴
		for(var j=0;j<i*2-1;j++){
			var di = document.createElement("div");
			di.style.top = y+450-(i-1)*15;
			di.style.left= x+165-i*30+j*30;
			m.appendChild(di);
		}
	}
}
</script>
</head>
<body style="background-color:grey;">
	<div align="center">
		<div style="width:600px;height:600px;background-color:#BFEFFF;border:5px groove #87CEFA;position:relative;" id="mainDiv">
			<div id="markDiv" style="width:60px;height:25px;top:0;left:270;border:1px dotted blue;background-color:#BFEFFF"></div>
			<div id="qiuDiv" style="width:10px;height:10px;top:580;left:295;background-color:red;"></div>
			<div id="bangDiv" style="width:150px;height:10px;bottom:1;left:225;background-color:black;"></div>
		</div>
	</div>
<body>
執行結果如下


火狐,IE,谷歌,360,顯示結果基本一樣。如果大家有更好的圖形畫法可以自己設計。

加入控制

遊戲開始前是可以先找個好位置出擊的,即飛球飛起前可以隨擋板左右移動,就先做個左右的移動。這裡就用在body上加鍵盤監聽事件了。用左右箭頭控制好了。同時還得有一個boolean值來確定當前遊戲是否已經開始,如果已經開始了飛球就不要隨擋板移動了,程式碼如下:

var bangleft = 225;//取擋板當前的左座標,就是left距離
var qiuleft = 295;//取飛球當前的左座標
var bs = 10;//按一次鍵移動多少,
var kflag =  false;//標記遊戲是否已經開始
//鍵盤處理事件
function keydownEvent(event){		
		var bang = $("bangDiv");//取擋板div
		var qiu = $("qiuDiv");//取飛球的div
		if(event.keyCode==37){//如果是左箭頭
			for(var i = 0;i<bs;i++){
				if(bangleft-1!=0){//0即left為0,即已經到左移動到了牆壁,就不再起作用。
					bangleft-=1;//每一次向左移動1,其實上面做了for迴圈,結果就是每按一次向左移動了bs=10,為什麼要迴圈著去加而不一次性去加,原因很簡單就是為了防止一次就加過了超出了範圍,同時我們可以通過設定bs引數的值來改變擋板移動的快慢
					bang.style.left = bangleft+"px";//改變擋板位置
					if(!kflag){//如果遊戲沒開始
						qiuleft-=1;
						qiu.style.left = qiuleft+"px";//改變飛球位置
					}
				}
			}
		}
		if(event.keyCode==39){//如果是右箭頭同理
			for(var i = 0;i<bs;i++){
				if(bangleft+1!=450){//檢測是否碰右壁
					bangleft+=1;
					bang.style.left = bangleft+"px";
					if(!kflag){
						qiuleft+=1;
						qiu.style.left = qiuleft+"px";
					}
				}
			}
		}
		
	}
body要加上<body style="background-color:grey;" onkeydown="keydownEvent(event)">

飛球的執行

然後先不考慮打磚塊,先讓飛球可以飛起來,無視磚塊自由在四壁內運動,這裡還有一點我們應該都知道,小時候玩的時候飛球飛起時並不是直線飛起的,總是偏左或偏右那麼一點,因為如果設定直線向上的話擋板也保持不動,遊戲開始後飛球就回一直來回上下做運動。

先不管角度問題,先想怎麼移動,這裡要複習一下中學的課程了看下圖:


假設球在x軸y軸的(0,0)為起點,以α角移動,假設每個單位時間內移動了s(速度).就是圖中的斜線距離。到了座標(x1,y1).那已知s的值和α的值就可求出移動的距離x1,y1。賦值給球的座標做為新座標,球就移動了,這樣我們用setTimeout函式不斷去呼叫這個方法球就可以以直線自己前進了。也先不考慮撞不撞強,假設α=30度,s=1;那單位時間內飛球的新座標就是(s*sin30,s*cos30)如果忘了正弦餘弦的用法可以去百度,我確實忘了。。所以飛球按30度角直線執行的方法就是如下程式碼:

var qx = 295;//飛球初始座標left
var qy = 580;//飛球初始座標top
function go(){
		var qiu = $("qiuDiv");
		qx = qx +1*Math.cos((2*Math.PI/360)*30);
		qy = qy -1*Math.sin((2*Math.PI/360)*30);
		qiu.style.left = qx+"px";	
		qiu.style.top = qy+"px";
		
		setTimeout("go()",10)
}
這裡要說明js裡cos,sin的用法,Math.sin(x) 表示的是x 的正弦值。Math.cos(x) 是 x 的餘弦值。但js裡的這個x是弧度而不是角度,假設角度是α,轉化為弧度就是

2*Math.PI/360*α我們這裡假設的是α=30度。直角三角形裡知道一個銳角α和斜邊長s=1,求α的對邊y1的長就是s*sin(α)在此例中即為

1*Math.sin((2*Math.PI/360)*30);
求a的鄰邊x1就是cos....同理

由於我們現在假想的是左上飛,所以是x取加值,y取減。然後迴圈呼叫go(),10是間隔時間,即0.01秒。s=1相當於限制了每次移動的位移,如果s設的過大,則球會變快而且平滑效果很差幾乎是瞬移,所以是s設的小,然後讓間隔時間變短,這樣平滑效果好點,大家可以自己更改除錯。現在大概飛行的原理知道。然後就考慮撞牆了。

一開始我鑽了牛角尖(此段為本人的一段傻瓜笨蛋推理,容易嚴重誤導人,不願看的可以直接看下面的紅字‘最終其實原理是這樣的’),總是考慮要是碰的是左壁,或者右壁,去區分這些,然後去改變角度什麼的,後來一些如果詳細區分的話後面還要考慮磚塊,那就要去考慮碰的是磚塊的上下左右,太麻煩了,就好像那個故事有個皇帝要在每條街鋪上地毯防止腳踩傷,而大臣建議何不讓腳去穿個鞋呢,忘了故事大概了就是這樣。我們應該考慮的是這個飛球是上下左右哪個邊碰壁了。這樣需要考慮的就少了。

如何判斷碰壁呢,這裡不考慮磚塊的情況下很簡單。就是去計算座標罷了。那角度是怎麼改變的呢,原理那提到是反射角,飛球的右邊如下圖:


後來我又繼續畫圖上邊碰壁右邊碰壁畫完我發現


最終其實原理是這樣的其實折騰來折騰去這裡都是四方形,α的值是不會變的,角度的改變都是在圍繞α在做文章。同樣單位時間內的移動距離s也是不會變的,x方向y方向的移動距離的絕對值也是不會邊的,其實撞牆就是改變x,或y位移的正負而已,至於角度我們沒必要去考慮。新增程式碼如下:

var qx = 295;//飛球初始座標left
var qy = 580;//飛球初始座標top
var jiao = 89;//初始飛行角度
var zx = 1;//控制left位移的正負
var zy = -1;//控制top位移的正負
var rp = null;//控制遊戲程序
function go(){
		var qiu = $("qiuDiv");
		
		qx = qx +zx*Math.cos((2*Math.PI/360)*30);
		qy = qy +zy*Math.sin((2*Math.PI/360)*30);
		qiu.style.left = qx+"px";	
		qiu.style.top = qy+"px";
		if(qy>=580){
			//alert(qx)
			//alert(qx<bangleft||qx>bangleft+150)
			if(qx<bangleft||qx>bangleft+150){//判斷是否接住
				clearTimeout(rp);
			}else{
				zy=-1;
				rp = setTimeout("go()",1);
			}
		}else{
			if(qx>=600)zx=-1;
			if(qx<=0)zx=1;
			if(qy<=0)zy=1;
			rp = setTimeout("go()",1);
			}
}


判斷與磚塊碰撞

這樣基本實現了接球並移動。但不管怎麼接球都是按原來的角度走的,執行起來球是一直走一條軌道。這裡就用到開始需求那裡說的。

如果球打在了擋板的左方,不管球從哪個方向飛回來的都會繼續反彈回左方,越偏左反彈的越厲害,右邊同理。而且碰到磚塊雖然不會改變雖然α沒變都軌跡會變。這裡要解決兩個問題了。一個是改變角度,一個是碰撞磚塊。貌似後一個更難點,但剛寫完撞牆順思路可以繼續想如何判斷撞磚塊。

我又一次想到了陣列。就是生成磚塊的同時把每個磚塊放入陣列中去,然後飛球每移動就與所有磚塊進行比較是否碰撞。則開始的載入程式碼改為:

var zdivs = new Array();//用於儲存所有的磚塊。

window.onload=function(){
	var x =150;var y = 15;
	var m = $("mainDiv");
	
	for(var i=1;i<=9;i++){
		
		for(var j=0;j<i;j++){
			var di = document.createElement("div");
			di.style.top = y+(i-1)*15;
			di.style.left= x-i*15+j*30;
			m.appendChild(di);
			
			var di1 = document.createElement("div");
			di1.style.top = y+(i-1)*15;
			di1.style.left= x+300-i*15+j*30;
			m.appendChild(di1);
			
			zdivs[zdivs.length]=di;
			zdivs[zdivs.length]=di1;
		}		
		
		
	}

	for(var i=1;i<=10;i++){
			var di = document.createElement("div");
			di.style.top = y+150+(i-1)*15;
			di.style.left= x-15;
			m.appendChild(di);
			
			var di1 = document.createElement("div");
			di1.style.top = y+150+(i-1)*15;
			di1.style.left= x-15+300;
			m.appendChild(di1);
			
			zdivs[zdivs.length]=di;
			zdivs[zdivs.length]=di1;
	}
	
	for(var i=1;i<=9;i++){
		for(var j=0;j<i*2;j++){
			var di = document.createElement("div");
			di.style.top = y+450-(i-1)*15;
			di.style.left= x+150-i*30+j*30;
			m.appendChild(di);
			
			zdivs[zdivs.length]=di;
		}
	}
	
}

然後在飛球移動的js後面加上迴圈判斷,如果相碰則將此磚塊在陣列中移除同時設定display為none;然後難點就是如何判斷飛球與某一個div相碰,相碰又是以何種方式反彈。想想就這幾種情況,飛球的右邊與磚塊左邊碰,或者下邊與磚塊上邊碰等等

判斷兩個div是否相碰,即有重合點的方法其實是數學上判斷兩個矩形是否重疊。


假設兩個矩形的位置如圖,兩個矩形a和b,分別有4個邊,ax1是左邊的x座標,ay1是上班的y座標,ax2是右邊的x座標,。。。。。我們取a,b的同側邊最大的那一邊組成的新座標sx,sy,如果這個座標同時存在與兩個矩形內就說明兩個矩形重合。即ax1與bx1比較取最大的bx1,ay1與by1取最大的by1。因為這裡是用的top,left所以越靠左left越大,越靠下top越大。這個的具體道理就不好說了,好好看幾何吧。

相碰的問題解決了,那飛球是從哪側碰撞的呢?該反彈回哪邊?這裡我的思路是飛球a,磚塊b,可能的撞擊方式是a左碰b右,a右碰b左,a上碰b下,a下碰b上四種,這裡暫不考慮角碰角原路返回。按四種情況的話a左碰b右的話,a左與b右的left差值,與後面三個a右b左的left差值,a上b下的top差值,a下b上的top差值,這四個差值的絕對值應該是哪個最小判定為是哪種情況相撞。個人是這麼理解,如有異議歡迎提出。所以知道了是那種情況的碰撞,也就明白了該如何去改變方向即zx,zy的值。

然後有了思路則飛球每走一步就去與數組裡的div遍歷去對比是否相碰。繼續寫在上面top<=580後面

function go(){
		var qiu = $("qiuDiv");
		
		qx = qx +zx*Math.cos((2*Math.PI/360)*jiao);
		qy = qy +zy*Math.sin((2*Math.PI/360)*jiao);
		
		if(qy>=580){
			if(qx<bangleft||qx>bangleft+150){//判斷是否接住
				clearTimeout(rp);
			}else{
				zy=-1;
				rp = setTimeout("go()",1);
			}
		}else{
				for(var i=0;i<zdivs.length;i++){
					var io = checkIsP(qx,qy,zdivs[i].offsetLeft,zdivs[i].offsetTop);
					if(io!=0){
						zdivs[i].style.display = "none";
						zdivs.splice(i,1);
						if(io==1){
							zx=1;
						}
						if(io==2){
							zx=-1;
						}
						if(io==3){
							zy=1;
						}
						if(io==4){
							zy=-1;
						}
						break;
					}				
				}
			
			if(qx>=600)zx=-1;
			if(qx<=0)zx=1;
			if(qy<=0)zy=1;
			qiu.style.left = qx+"px";	
			qiu.style.top = qy+"px";
			rp = setTimeout("go()",1);
			}
}

function checkIsP(qx,qy,zx,zy){
	var f = {
		x:qx,
		y:qy,
		x1:qx+10,
		y1:qy+10
	}
	var z = {
		x:zx,
		y:zy,
		x1:zx+30,
		y1:zy+15
	}
	var sx;var sy;
	sx = f.x>=z.x?f.x:z.x;
	sy = f.y>=z.y?f.y:z.y;
	if(sx >= f.x && sx <= f.x1 && sy >= f.y && sy <= f.y1 && sx >= z.x && sx <= z.x1 && sy >= z.y && sy <= z.y1){
	
		return seSmall(Math.abs(f.x-z.x1),Math.abs(f.x1-z.x),Math.abs(f.y-z.y1),Math.abs(f.y1-z.y));
		
	}else{
		return 0;
	}
}

function seSmall(a,b,c,d){
	
	if(a<b&&a<c&&a<d){
		return 1;
	}
	if(b<a&&b<c&&b<d){
		return 2;
	}
	if(c<a&&c<b&&c<d){
		return 3;
	}
	if(d<b&&d<c&&d<a){
		return 4;
	}
}
這樣碰撞問題基本解決了

更改角度

開篇還提到要按擋板的接球位置來更改反彈的角度。其實這個想想就簡單,就按接觸擋板時的距離對角度做百分比就行了與擋板接觸處程式碼改為如下

if(qy>=580){
			if(qx<bangleft||qx>bangleft+150){//判斷是否接住
				clearTimeout(rp);
			}else{
				zy=-1;
				if((qx-bangleft)>(75)){
					jiao = 90-(qx-bangleft+10-75)/75*90;
					zx = 1;
				}else{
					jiao = 90 - (75-(qx-bangleft+10))/75*90;
					zx=-1;
				}
				rp = setTimeout("go()",1);
			}

基本這個遊戲就完成了,感覺還是設計的挺爛的,每次移動都耗那麼多計算,其實在擋板與最下層磚塊間的空白可以省去不用計算,因為這一空間根本沒磚塊,所有在計算for迴圈前加上
else{if(qy<=480)
				for(var i=0;i<zdivs.length;i++){
還有一點也可以優化下,就是磚塊都是一行一行排列的。所有可以對磚塊進行按行分組。當飛球飛到此行的高度時只對比此行的磚塊,還有積分模組也不是很難,這裡就不再寫了,太長了。原始碼見上文,算是拋磚引玉。
(轉載請註明出處http://blog.csdn.net/uucai