天天看點

使用JS,H5編寫經典遊戲-推箱子

初學JS,使用JS、H5編寫推箱子小遊戲簡易版

推箱子小遊戲是一款多年前很流行的小遊戲(即使現在也有很多人玩),遊戲目的很簡單,就是人推箱子,把所有的箱子推到目的地,就遊戲成功:

使用JS,H5編寫經典遊戲-推箱子

看似跟簡單的邏輯,其實還是有一定難度的,我也是依靠了别人的幫助才完成的,現在開始介紹如何用js,html5編寫該遊戲(友善起見我們把人用一個粉圓形代替):

一.能力要求

JavaScript,HTML畫布,面向對象的基本思想,合理的程式設計邏輯。

二.編寫順序

1.pushBox.html檔案

2.pojo.js檔案(用來存放所有的對象)

3.paint.js檔案(用來寫畫畫的語句)

4.game.js檔案(用來寫運作邏輯部分)

5.allLevels.js檔案(用來存放關卡)

*注:這是我的書寫習慣,按内容和功能将個各類分開,如有更好地寫法歡迎評論

三.開始編寫

1.建立基本的pushBox.html檔案:

内容很簡單,<body>裡面僅僅需要<canvas>标簽,設定id,其後的<audio>檔案在邏輯部分做完後再加進去:

<body>
	<canvas id='can1' width=1536 height=733></canvas>
	<audio id="walk" autoplay></audio>
	<audio id="push" autoplay></audio>
	<audio id="win" autoplay></audio>
</body>
           

<script>有引入其他四個.js檔案,還有一個調用run()方法:

<script type="text/javascript" src="pojo.js"></script>
<script type="text/javascript" src="game.js"></script>
<script type="text/javascript" src="paint.js"></script>
<script type="text/javascript" src="allLevels.js"></script>
<script>
	window.onload = function(){
		run();
	}
</script>
           

2.寫pojo類:

首先我們需要知道總共有這些類:

人,箱子,目标點,磚塊和圍牆,很簡單,所有的類都有color(顔色),size(邊長/半徑),x(橫坐标),y(縱坐标)這些屬性。然後我們要記得,人和磚塊可能和目标點重合,是以在箱子和人的類裡面要加上isOnTarget(是否和目标點重合)屬性,這樣就完成了:

//人類
function Person(x, y){
	this.color = 'pink';
	this.size = 20;
	this.x = x;
	this.y = y;
	//判斷這個人是否在目标點上
	this.isOnTarget = false;
}
//箱子類
function Box(x, y){
	this.color = 'yellow';
	this.size = 40;
	this.x = x;
	this.y = y;
	//判斷某個箱子是否在目标點上
	this.isOnTarget = false;
}
//目标點類
function Target(x, y){
	this.size = 12;
	this.x = x;
	this.y = y;
	this.color = 'lime';
}
//地磚類
function Brick(x, y){
	this.x = x;
	this.y = y;
	this.size = 40;
}
//圍牆類
function Wall(x, y){
	this.x = x;
	this.y = y;
	this.size = 40;
}
           

3.編寫paint.js類

我們需要對剛剛在pojo.js類中寫的所有類寫出畫的方法:

需要注意一點很重要:我們如果使用确定的x,y坐标,比如說要畫箱子:

ctx.fillRect(x, y, size, size);
           

如果如下面的方式調用的話,我們在allLevels裡面這樣畫這個箱子:

ctx.fillRect(500, 500, 40, 40);
           

我們就無法确定這個箱子的旁邊是什麼,不好判斷,于是沒有辦法寫邏輯層。

是以,我們這樣構思:我們用一個二維數組來構造這個關卡,每一個物塊(箱子,人,目标點,磚塊或者牆)都放到這個數組當中,arr1[][],像這樣:

var arr1 = [
	['','wall','wall','wall','wall','wall','',''],
	['','wall','brick','person','wall','wall','wall',''],
	['','wall','brick','box','brick','brick','wall',''],
	['wall','wall','wall','brick','wall','brick','wall','wall'],
	['wall','target','wall','brick','wall','brick','brick','wall'],
	['wall','target','box','brick','brick','wall','brick','wall'],
	['wall','target','brick','brick','brick','box','brick','wall'],
	['wall','wall','wall','wall','wall','wall','wall','wall']
];
           

比如說arr1[0][1]就是牆了,這樣一來就能寫邏輯層了。而且還有一個優點,就是我們在之後建立新關卡的時候很友善,隻需要按照坐标順序在數組裡寫出來即可。

是以,我們在paint.js裡面這樣寫,每個方法裡面都把x和y進行一些運算,使它能正确地在網頁中畫出:

//首先還是清屏
function clearScreen(ctx){
	ctx.clearRect(0,0,1536,750);
}
//畫人
function paintPerson(ctx, x, y, size,color){
	ctx.beginPath();
	ctx.fillStyle = color;
	//我們在内部就寫好該往哪裡畫,傳過來的x,y坐标值就可以直接在裡面計算了
	ctx.arc(530+x*size*2+20, 180+y*size*2+20, size, 0, 2*Math.PI);
	ctx.fill();
}
//畫箱子
function paintBox(ctx, x, y, size,color){
	ctx.beginPath();
	ctx.fillStyle = color;	
	ctx.fillRect(530+x*size, 180+y*size, size, size);
	ctx.strokeStyle = 'black';
	ctx.lineWidth = 2;
	ctx.moveTo(530+x*size, 180+y*size);
	ctx.lineTo(530+x*size+size, 180+y*size+size);
	ctx.stroke();
	ctx.beginPath();
	ctx.moveTo(530+x*size+size, 180+y*size);
	ctx.lineTo(530+x*size, 180+y*size+size);
	ctx.stroke();
	ctx.strokeRect(530+x*size, 180+y*size, size, size);
}
//畫目标點
function paintTarget(ctx, x, y, size,color){
	ctx.beginPath();
	ctx.fillStyle = color;
	ctx.arc(530+x*size*4+20, 180+y*size*4+20, size, 0, 2*Math.PI);
	ctx.fill();
}
//畫地磚,其中用了for循環簡化了代碼量
function paintBrick(ctx, x, y, size){
	ctx.beginPath();
	ctx.fillStyle = 'blue';
	ctx.fillRect(530+x*size, 180+y*size, size, size);
	ctx.strokeStyle = 'lightblue';
	for(var i = 0; i <= 3; i++){
		ctx.beginPath();
		ctx.moveTo(530+x*size, 180+y*size+0.25*(i+1)*size);
		ctx.lineTo(530+x*size+size, 180+y*size+0.25*(i+1)*size);
		ctx.stroke();
	}
	for(var i = 0; i < 4; i++){
		ctx.beginPath();
		if(i%2 == 0){
			ctx.moveTo(530+x*size+0.5*size, 180+y*size+0.25*i*size);
			ctx.lineTo(530+x*size+0.5*size, 180+y*size+0.25*(i+1)*size);
			ctx.stroke();
		}else{
			ctx.moveTo(530+x*size+0.25*size, 180+y*size+0.25*i*size);
			ctx.lineTo(530+x*size+0.25*size, 180+y*size+0.25*(i+1)*size);
			ctx.stroke();
			ctx.beginPath();
			ctx.moveTo(530+x*size+0.75*size, 180+y*size+0.25*i*size);
			ctx.lineTo(530+x*size+0.75*size, 180+y*size+0.25*(i+1)*size);
			ctx.stroke();
		}
	}
}
//畫圍牆
function paintWall(ctx, x, y, size){
	ctx.beginPath();
	ctx.fillStyle = 'gray';
	ctx.fillRect(530+x*size, 180+y*size, size, size);
	ctx.strokeStyle = 'white';
	ctx.beginPath();
	ctx.moveTo(530+x*size+0.5*size, 180+y*size);
	ctx.lineTo(530+x*size+0.5*size, 180+y*size+size);
	ctx.stroke();
	ctx.beginPath();
	ctx.moveTo(530+x*size, 180+y*size+0.5*size);
	ctx.lineTo(530+x*size+size, 180+y*size+0.5*size);
	ctx.stroke();
}
           

然後,我們在調用畫的方法時候這樣調用,按照每個物塊在數組中的位置進行畫畫:

unction getMap(ctx, person){
	clearScreen(ctx);
	
	for(var i = 0; i<arr1.length; i++){
		for(var j = 0; j<arr1[0].length; j++){
			//畫初始圖
			if(arr1[i][j] == 'wall'){
				//這樣我們就把坐标作為參數,引入到那些paint方法裡,經過計算,在網頁中正确顯示了
				paintWall(ctx,j,i,40);
			}else if(arr1[i][j] == 'brick'){
				paintBrick(ctx, j, i,40);
			}else if(arr1[i][j] == 'target'){
				paintBrick(ctx, j, i,40);
				paintTarget(ctx, j, i,10,'lime');
			}else if(arr1[i][j] == 'box'){
				paintBrick(ctx, j, i,40);
				var index = getBoxIndex(boxes,i,j);
				if(boxes[index].isOnTarget){
					paintBox(ctx, j, i,40,'red');
				}else{
					paintBox(ctx, j, i,40,'yellow');
				}
			}else if(arr1[i][j] == 'person'){
				paintBrick(ctx, j, i,40);
				paintPerson(ctx, j, i,20,'pink');
			}
			
		}
	}
	
}
           

在game.js裡面的run()調用getMap()方法後,效果就是這樣:

使用JS,H5編寫經典遊戲-推箱子

4.game.js類:

首先我們還是需要建立人和箱子的對象:

var person = new Person(0,0);
           
var boxLevel1Count = 3;
var boxes = new Array(boxLevel1Count);
for(var i = 0;i<boxLevel1Count;i++){
	boxes[i] = new Box(0,0);
}
           

然後我們要知道人在二維數組中的位置(坐标),以及三個箱子在其中的坐标需要以下兩個方法:

//找到人的坐标
function findPerson(){
	for (var i = 0; i < arr1.length; i++) {
        var tmp = arr1[i];
        for (var j = 0; j < tmp.length; j++) {
            if (arr1[i][j] == 'person') {
		//使用json傳變量
                return {personX:i,personY:j};
            }
        }
    }
}

//找箱子的坐标,并把他們放到數組裡
function findBox(){
	var count = 0;
	for (var i = 0; i < arr1.length; i++) {
        var tmp = arr1[i];
        for (var j = 0; j < tmp.length; j++) {
            if (arr1[i][j] == 'box') {
				boxes[count].x = i;
				boxes[count].y = j;
				count++;
            }
        }
    }
	return boxes;
}
           

找到人的坐标之後,我們要把person對象的x,y和二維數組裡的i,j關聯起來,就是:

//接收person的坐标
	var position = findPerson();
	//i是person的橫坐标
	
	var i = position.personX;
	//j是person的縱坐标
	var j = position.personY;
	//使對象的屬性和人在二維數組的坐标關聯
	person.x = i;
	person.y = j;
           

然後我們就可以開始寫邏輯了,比如說使用者按方向鍵左,要判斷左邊是什麼,如果是箱子的話,還要判斷箱子的左邊是什麼:

正确的邏輯如下所示(我們就拿人往左移動為例,然後上下右都是一樣的):

使用JS,H5編寫經典遊戲-推箱子

具體的文法很簡單:比如說,按照第一個為例,左邊是磚塊,并且人踩的不是目标點:

if(arr1[i][j-1] == 'brick'){
	arr1[i][j-1] = 'person';
	arr1[i][j] = 'brick';
	Audio1.src = '走路emm.wav';
}
           

如果我們下一步,人踩到了目标點,我們就要把person.isOnTarget 設定為true,當人移開時候,我們把這個屬性設定為false

當人左邊是箱子的時候,比較麻煩,首先必須明白有一點:我們到底推的是哪個箱子?之前已經有了一個存放所有箱子的數組了,是以現在需要一個方法,可以讓我們知道我們推的是哪個箱子:

function getBoxIndex(boxes, i,j){
	var index = 0;
	for(var k = 0;k<boxes.length;k++){
		if(boxes[k].x == i && boxes[k].y == j){
			//找到了這個箱子的下标
			index = k;
		}
	}
	return index;
}
           

在實際調用中,裡面的參數(i,j)就寫下一步要走的那個位置,比如說向左走,就是

var index = getBoxIndex(boxes, i,j-1);
           

這個index就是我們要找的第i個箱子了,接下來就很好辦了,我們按照剛才的邏輯一步一步寫,一堆的if、else,隻需注意兩點,當人踩到目标點時,把person.isOnTarget = true,移開之後false;箱子踩到目标點時boxes[index].isOnTarget = true,移開之後false,然後再整理一下,簡化代碼量,就是:

//玩家操作
document.onkeydown = function(ev){
	var oCan = document.getElementById('can1');
	var ctx = oCan.getContext('2d');
	var oEvent = ev || event;
	var Audio1 = document.getElementById('walk');
	var Audio2 = document.getElementById('push');
	
	//接收person的坐标
	var position = findPerson();
	//i是person的橫坐标
	
	var i = position.personX;
	//j是person的縱坐标
	var j = position.personY;
	//使對象的屬性和人在二維數組的坐标關聯
	person.x = i;
	person.y = j;
	if(oEvent.keyCode == 37){
		if(person.isOnTarget){
			if(arr1[i][j-1] == 'brick'){
				arr1[i][j-1] = 'person';
				arr1[i][j] = 'target';
				person.isOnTarget = false;
			}else if(arr1[i][j-1] == 'box' && arr1[i][j-2] != 'wall' && arr1[i][j-2] != 'box'){
				var index = getBoxIndex(boxes, i,j-1);
				if(!boxes[index].isOnTarget){
					if(arr1[i][j-2] == 'brick'){
						arr1[i][j-2] = 'box';
						arr1[i][j-1] = 'person';
						arr1[i][j] = 'target';
						person.isOnTarget = false;
					}else if(arr1[i][j-2] == 'target'){
						arr1[i][j-2] = 'box';
						arr1[i][j-1] = 'person';
						arr1[i][j] = 'target';
						person.isOnTarget = false;					
						boxes[index].isOnTarget = true;
					}
				}else if(boxes[index].isOnTarget){
					if(arr1[i][j-2] == 'brick'){
						arr1[i][j-2] = 'box';
						arr1[i][j-1] = 'person';
						arr1[i][j] = 'target';
						boxes[index].isOnTarget = false;
					}else if(arr1[i][j-2] == 'target'){
						arr1[i][j-2] = 'box';
						arr1[i][j-1] = 'person';
						arr1[i][j] = 'target';		
					}
				}
				boxes[index].y--;
			}else if(arr1[i][j-1] == 'target'){
				arr1[i][j-1] = 'person';
				arr1[i][j] = 'target';
			}
		}else if(!person.isOnTarget){
			if(arr1[i][j-1] == 'brick'){
				arr1[i][j-1] = 'person';
				arr1[i][j] = 'brick';
			}else if(arr1[i][j-1] == 'box' && arr1[i][j-2] != 'wall' && arr1[i][j-2] != 'box'){
				var index = getBoxIndex(boxes, i,j-1);
				//箱子踩的不是目标點
				if(!boxes[index].isOnTarget){
					//箱子左邊是地面
					if(arr1[i][j-2] == 'brick'){
						arr1[i][j-2] = 'box';
						arr1[i][j-1] = 'person';
						arr1[i][j] = 'brick';					
					}//箱子左邊是目标點
					else if(arr1[i][j-2] == 'target'){
						arr1[i][j-2] = 'box';
						arr1[i][j-1] = 'person';
						arr1[i][j] = 'brick';
						boxes[index].isOnTarget = true;
					}
				}else if(boxes[index].isOnTarget){
					if(arr1[i][j-2] == 'brick'){
						arr1[i][j-2] = 'box';
						arr1[i][j-1] = 'person';
						arr1[i][j] = 'brick';
						boxes[index].isOnTarget = false;
						person.isOnTarget = true;
					}else if(arr1[i][j-2] == 'target'){
						arr1[i][j-2] = 'box';
						arr1[i][j-1] = 'person';
						arr1[i][j] = 'brick';
						person.isOnTarget = true;
					}
				}
				boxes[index].y--;
			}else if(arr1[i][j-1] == 'target'){
				arr1[i][j-1] = 'person';
				arr1[i][j] = 'brick';
				person.isOnTarget = true;
			}
		}
	}
           

這樣,向左走的所有邏輯就完成了,然後是上,右,下,接着else if 就行,照貓畫虎,把裡面的i和j一更換就完成了。

所有邏輯寫完,驗證無誤後,我們要判斷通關條件,這個很簡單,當所有的箱子都isOnTarget時候,成功,當然是在每次按完方向鍵之後都要判斷:

function judgeWin(boxes){
	var count = 0;
	for(var p = 0;p<boxes.length;p++){
		if(boxes[p].isOnTarget)
			count++;
	}if(count == boxes.length){
		var Audio3 = document.getElementById('win');
		Audio3.src = '鼓掌.mp3';
		alert('You Win! 一共走了'+countStep+'步');
	}
}
           

通關的畫面如下:

使用JS,H5編寫經典遊戲-推箱子

最後,我們加入音效,主要劃分成以下幾類:人走到磚塊的,人推箱子的,人碰到牆的(和人推箱子碰到牆的),成功後的掌聲。

總結:

1.我開始以為這個很簡單,和我之前做的那個flappy bird 差不多,沒想到這裡面的邏輯其實很複雜,我的flappy bird連結如下:點選打開連結。是以,一定要在剛開始的時候要構思好大局,别越寫越麻煩,容易産生放棄心裡。

2.利用二維數組存放地圖,然後在paint()方法裡面寫畫的位置,大小等,調用的時候paint()裡面就填坐标,這樣有兩個好處:(1)可以知道每個物塊的上下左右都是什麼,易于判斷;(2)建立關卡的時候易于建立,隻需要按照坐标位置,把wall,brick,person,target,box放進去即可

3.在建立人的對象後,我們需要在地圖數組中把人的坐标找出來,然後将對象的x,y屬性和坐标關聯;在建立箱子的對象數組後,我們需要getIndex()方法,找到人到底推的是哪個箱子,才能使這個箱子的isOnTarget改成true或者false,這兩點很關鍵。

注:我隻做了一關,也沒有做關卡的跳轉,有興趣的夥伴們可以繼續把跳轉關卡完成。

最後,希望大家有所收獲。本文為原創,其中若有更好的寫法請大佬留言評論。

音樂,音效提供:愛給網

程式創作時間:2018.3.12

部落格撰寫時間:2018.3.16

繼續閱讀