用JavaScript canvas做的走迷宫游戏,肝了一下午,请帮忙点个赞!

4,014 阅读8分钟

引言:

上午女儿跟我去逛超市,在文具区看到一本书,总共有10幅图都是小迷宫游戏,图什么的都挺漂亮,就是有点贵应该是纸比较好,要30多块钱,我就觉得划不来(典型的铁公鸡),我就跟女儿说家里有,买了其他东西就回来了,然后网上查了一下,主要用到的是一个算法,于是吃完午饭就开始写了,这就学马老师来一波回首掏!

有人可能会说你这人真抠门,这点钱都舍不得掏。

我会说:这是钱的问题吗?这是专业,我们程序员的钱有那么好赚吗?我待会就跟我老婆要30块钱,说我买了个迷宫游戏书,我们程序员的钱不就是敲代码来的吗,变现有问题?

效果

图片

刷新就可以换一个通道,不比书香,可以一直玩,一直玩一直爽。

算法(网上抄的)

1.将起点作为当前迷宫单元并标记为已访问

2.当还存在未标记的迷宫单元,进行循环

  1).如果当前迷宫单元有未被访问过的的相邻的迷宫单元 

     (1).随机选择一个未访问的相邻迷宫单元 

     (2).将当前迷宫单元入栈 

     (3).移除当前迷宫单元与相邻迷宫单元的墙 

     (4).标记相邻迷宫单元并用它作为当前迷宫单元

  2).如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空 

     (1).栈顶的迷宫单元出栈 

     (2).令其成为当前迷宫单元

这个算法叫做“深度优先”,简单来说,就是从起点开始走,寻找它的上下左右4个邻居,然后随机一个走,到走不通的时候就返回上一步继续走,直到全部单元都走完。

实现思路(这个自己写的)

1.创建格子单元对象。

2.通过算法将这些格子打通,绘制出迷宫的形状。

3.绘制入口与终点的格子。

4.添加键盘的上、下、左、右移动事件,写好对应的函数,到达终点提示胜利。

相关图示图

.每个单元的墙,分为上墙、右墙、下墙、左墙,把这些墙用长度为4的数组表示,元素的值为true则表示墙存在,否则墙不存在,代码里数组的下标方式来确定墙是否存在。

2.单元是根据行列来创建的,会用到双循环,类似表格,比如第二行用 i 来表示的话就是 1,第3列用 j 来表示就是2,那第二行第3列的元素组合起来就是(1,2)

3.那同理它的上邻居就是(0,2),右邻居(1,3),下邻居(2,2),左邻居(1,1),也就是上下邻居是 i 减加1,左右邻居是 j 减加1。

4.正方形4个点的坐标分别为(x1,y1)(x2,y2)(x3,y3)(x4,y4),计算坐标的公式为:

               //左上角坐标
		x1=j*w;
		y1=i*w;
		//右上角坐标
		x2=(j+1)*w;
		y2=i*w;
		//右下角坐标
		x3=(j+1)*w;
		y3=(i+1)*w;
		//左下角坐标
		x4=j*w;
		y4=(i+1)*w;

计算坐标,假如每个正方形的宽高都是40,那么(1,2)这个单元的坐标如下图:

5.墙的处理,之前说到墙是以一个4个元素的数组来表示的,比如数组为:[true,true,true,true],则图为:

如果数组为[false,true,true,true],则图为:

6.如果要联通右边的邻居要怎么做呢?当前单元去除右墙,右边单元去除左墙,这样就联通了。

去除后就这样,以此类推

代码及讲解

新增构造函数

此构造函数不是直接利用Rect来绘制方形的,而是自己以绘制4条直线的方式来绘制的,既方形的上、右、下、左4条直线。

1.计算坐标,这个上面已经提过。

2.根据墙数组的值来确定是否绘制这条直线,[true,true,true,true]就绘制完整的方形,[false,true,true,true]的话,上边就会缺失。

代码

//用4条直线画方形的构造函数
	function LineRect(o){
		this.x=0,//x坐标
		this.y=0,//y坐标
		
		this.init(o);
		this.axis(this.i,this.j);
	}
	
	LineRect.prototype.init=function(o){
		for(var key in o){
			this[key]=o[key];
		}
		//上右下左4面墙 true就表示要绘制
		this.walls=[true,true,true,true];
	}
	//根据i,j计算出坐标
	LineRect.prototype.axis=function(i,j){
		var w = this.maze.dis;
		//i代表行 j代表列
		//左上角坐标
		this.x1=j*w;
		this.y1=i*w;
		//右上角坐标
		this.x2=(j+1)*w;
		this.y2=i*w;
		//右下角坐标
		this.x3=(j+1)*w;
		this.y3=(i+1)*w;
		//左下角坐标
		this.x4=j*w;
		this.y4=(i+1)*w;
	}
	//绘制函数
	LineRect.prototype.render=function(context){
		this.ctx=context;
		innerRender(this);
			
		function innerRender(obj){
			var ctx=obj.ctx;
			ctx.save()
			ctx.beginPath();
			ctx.translate(obj.x,obj.y);
			
			if(obj.lineWidth){
				ctx.lineWidth=obj.lineWidth;
			}
			
			//判断上、右、下、左 的墙,true的话墙就会有,否则墙就没有
			var top    = obj.walls[0];
			var right  = obj.walls[1];
			var bottom = obj.walls[2];
			var left   = obj.walls[3];
			if(top){
				ctx.moveTo(obj.x1,obj.y1);	
				ctx.lineTo(obj.x2,obj.y2);	
			}
			if(right){
				ctx.moveTo(obj.x2,obj.y2);	
				ctx.lineTo(obj.x3,obj.y3);	
			}
			if(bottom){
				ctx.moveTo(obj.x3,obj.y3);	
				ctx.lineTo(obj.x4,obj.y4);	
			}
			if(left){
				ctx.moveTo(obj.x4,obj.y4);	
				ctx.lineTo(obj.x1,obj.y1);	
			}
			obj.strokeStyle?(ctx.strokeStyle=obj.strokeStyle):null;
			ctx.stroke();
		  	ctx.restore();
		}
	  	return this;
	}

绘制

Maze.prototype.drawGrid=function(){
		this.rows = Math.floor(this.h/this.dis);
		this.cols = Math.floor(this.w/this.dis);
		//根据行数、列数来创建格子
		for(var i=0;i<this.rows;i++){
			for(var j=0;j<this.cols;j++){
				var cell = this.buildCell(i,j);
				this.renderArr.push(cell);
			}
		}
	}
	//创建格子
	Maze.prototype.buildCell=function(i,j){
		var param={i:i,j:j,lineWidth:1,maze:this};
		//创建格子对象
		var cell = new LineRect(param);
		return cell;
	}

根据算法打通墙

给每个单元格对象都增加邻居查找方法

//查找当前单元是否有未被访问的邻居单元
	LineRect.prototype.findNeighbors=function(){
		//邻居分为上下左右
		var maze = this.maze ;
		this.arr = maze.renderArr;
		var res=[];//返回的数组
		var top    = this.getNeighbor('0');
		var right  = this.getNeighbor('1');
		var bottom = this.getNeighbor('2');
		var left   = this.getNeighbor('3');
		
		if(top){
			res.push(top);
		}
		if(right){
			res.push(right);
		}
		if(bottom){
			res.push(bottom);
		}
		if(left){
			res.push(left);
		}
		return res;//返回邻居数组
	}
	//查找邻居
	LineRect.prototype.getNeighbor=function(type,lost_visited){
		var key,neighbor;
		if(type=='0'){
			key = this.assemKey(this.i-1,this.j);
		}else if(type=='1'){
			key = this.assemKey(this.i,this.j+1);
		}else if(type=='2'){
			key = this.assemKey(this.i+1,this.j);
		}else if(type=='3'){
			key = this.assemKey(this.i,this.j-1);
		}
		
		if(key){
			neighbor = this.arr[key];//首先找到这个邻居
			if(neighbor.visited && !lost_visited){//判断是否被访问,如果被访问了返回undefined  lost_visited表示是否忽略访问的情况
				neighbor = undefined;
			}
		}
		return neighbor;
	}
	//根据i,j计算数组单元在数组中的下标值
	LineRect.prototype.assemKey=function(i,j){
		if(i<0 || j<0 || i>=this.maze.rows || j>=this.maze.cols){//超出边界了
			return undefined;
		}
		return i*this.maze.cols+j;//计算出i,j位置单元在数组中的下标
	}

计算

跟着算法来写的代码,唯一要注意的是我设置了一个值unVisitedCount,初始值为所有单元的数量,每当一个单元被标记为已访问后,这个值就递减1,当值为0后就终止循环,结束算法。

             Maze.prototype.computed=function(){
		/*
			1.将起点作为当前迷宫单元并标记为已访问
			2.当还存在未标记的迷宫单元,进行循环
				1).如果当前迷宫单元有未被访问过的的相邻的迷宫单元
					(1).随机选择一个未访问的相邻迷宫单元
					(2).将当前迷宫单元入栈
					(3).移除当前迷宫单元与相邻迷宫单元的墙
					(4).标记相邻迷宫单元并用它作为当前迷宫单元
				2).如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
					(1).栈顶的迷宫单元出栈
					(2).令其成为当前迷宫单元
		*/
		
	
		var stack =	this.stack ; //栈
		var arr = this.renderArr;
		
		var current = arr[0];//取第一个为当前单元
		this.pathArr.push(current);
		
		current.visited=true;//标记为已访问
		var unVisitedCount=arr.length-1;//因为第一个已经设置为访问了
		var neighbors ;
		while(unVisitedCount>0){
			neighbors = current.findNeighbors();//查找邻居集合(未被访问的)
			if(neighbors.length>0){//如果当前迷宫单元有未被访问过的的相邻的迷宫单元
				//随机选择一个未访问的相邻迷宫单元
				var index = _.getRandom(0,neighbors.length); 
				var next = neighbors[index];
				//将当前迷宫单元入栈
				stack.push(current);
				//移除当前迷宫单元与相邻迷宫单元的墙
				this.removeWall(current,next);
				//标记相邻迷宫单元并用它作为当前迷宫单元
				next.visited=true;
				//标记一个为访问,则计数器递减1
				unVisitedCount--;//递减
				current = next;
			}else if(stack.length>0){//如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
				/*
					1.栈顶的迷宫单元出栈
					2.令其成为当前迷宫单元
				*/
				var cell = stack.pop();
				current = cell;
			}
			//推入路线数组
			this.pathArr.push(current);
		}
	}

移除墙

//移除当前迷宫单元与相邻迷宫单元的墙
	Maze.prototype.removeWall=function(a,b){
		if(a.i==b.i){//横向邻居
			if(a.j>b.j){//匹配到的是左边邻居
				//左边邻居的话,要移除自己的左墙和邻居的右墙
				a.walls[3]=false;
				b.walls[1]=false;
			}else{//匹配到的是右边邻居
				//右边邻居的话,要移除自己的右墙和邻居的左墙
				a.walls[1]=false;
				b.walls[3]=false;
			}
		}else if(a.j==b.j){//纵向邻居
			if(a.i>b.i){//匹配到的是上边邻居
				//上边邻居的话,要移除自己的上墙和邻居的下墙
				a.walls[0]=false;
				b.walls[2]=false;
			}else{//匹配到的是下边邻居
				//下边邻居的话,要移除自己的下墙和邻居的上墙
				a.walls[2]=false;
				b.walls[0]=false;
			}
		}
	}

绘制入口出口

//创建起点和终点格子
	Maze.prototype.drawRunCell=function(i,j){
		var end = new _.Rect({
			x:(this.cols-1)*this.dis+this.dis2,
			y:(this.rows-1)*this.dis+this.dis2,
			width:this.dis-2*this.dis2,
			height:this.dis-2*this.dis2,
			fill:true,
			fillStyle:'red'
		});
		end.i=this.rows-1,end.j=this.cols-1;//设定i,j值,判断是否终点
		this.renderArr2.push(end);
		
		var start = new _.Rect({
			x:0+this.dis2,
			y:0+this.dis2,
			width:this.dis-2*this.dis2,
			height:this.dis-2*this.dis2,
			fill:true,
			fillStyle:'blue'
		});
		start.i=0,start.j=0;//设定i,j值,控制移动
		this.renderArr2.push(start);
	}

加入移动监听

//按键的控制
	Maze.prototype.control=function(){
		var that=this;
		global.addEventListener('keydown',function(e){
			console.log(that.endFlag)
			if(that.endFlag) return ;
			var dir;
			switch (e.keyCode){
				case 87://w
				case 38://上
					dir=0;//上移动
					break;
				case 83://s
				case 40://下
					dir=2;//下移动
					break;
				case 65://a
				case 37://左
					dir=3;//左移动
					break;
				case 68://d
				case 39://右
					dir=1;//右移动
					break;
			
			}
			
			that.move(dir);
			//测试用,记得删除
			that.render();
		});
	}

加入移动函数

//移动
	Maze.prototype.move=function(dir){
		var cur = this.renderArr2[1];//当前移动的方块
		var key = this.assemKey(cur);//根据移动方块的i,j计算出key
		var cell =  this.renderArr[key];//得到移动方块对应的单元
		
		var wall = cell.walls[dir];//得到对应的那面墙
		if(!wall){//表示是没有墙能移动
			var neighbor = cell.getNeighbor(dir,1);
			if(!neighbor){
				return ;
			}
			cur.x=neighbor.x1+this.dis2;
			cur.y=neighbor.y1+this.dis2;
			cur.i=neighbor.i;
			cur.j=neighbor.j;
		}
		var end = this.renderArr2[0];
		if(cur.i==end.i && cur.j==end.j ){
			this.endFlag=true;
			console.log('完成');
			this.endShow();
		}
	}
	Maze.prototype.assemKey=function(e){
		return e.i*this.cols+e.j;//计算出i,j位置单元在数组中的下标
	}

加入胜利图

//展示结束的图片(胜利)
	Maze.prototype.endShow=function(){
		var image,img,sx=0,sy=0,sWidth=225,sHeight=108,dx=this.w/2-110,dy=this.h/2-100,dWidth=225,dHeight=108;
		image = this.imgObj['suc'];
		
		img = new _.ImageDraw({image:image,sx:sx,sy:sy,sWidth:sWidth,sHeight:sHeight, dx:dx, dy:dy ,dWidth:dWidth,dHeight:dHeight});
		this.renderArr2.push(img);
		this.render();
	}

写出来也花了不少脑细胞,能看到这里的都是大佬,我去找老婆提现去了。

欢迎各位大佬 点赞+评论+关注,谢谢!

关注公众号【编程界明世隐】,回复【130】下载源码,同时有更多资料、实例代码、面试技巧奉上!