当前位置:主页 > 查看内容

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

发布时间:2021-10-05 00:00| 位朋友查看

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

引言:

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

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

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

效果

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

算法(网上抄的)

? ? ? ? ? ? 1.将起点作为当前迷宫单元并标记为已访问
?? ??? ??? ?2.当还存在未标记的迷宫单元,进行循环
?? ??? ??? ??? ?1).如果当前迷宫单元有未被访问过的的相邻的迷宫单元
?? ??? ??? ??? ??? ?(1).随机选择一个未访问的相邻迷宫单元
?? ??? ??? ??? ??? ?(2).将当前迷宫单元入栈
?? ??? ??? ??? ??? ?(3).移除当前迷宫单元与相邻迷宫单元的墙
?? ??? ??? ??? ??? ?(4).标记相邻迷宫单元并用它作为当前迷宫单元
?? ??? ??? ??? ?2).如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
?? ??? ??? ??? ??? ?(1).栈顶的迷宫单元出栈
?? ??? ??? ??? ??? ?(2).令其成为当前迷宫单元

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

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

1.创建格子单元对象。

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

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

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

相关图示图

1.每个单元的墙,分为上墙、右墙、下墙、左墙,把这些墙用长度为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();
	}

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

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

源码下载

方式1:少量积分,下载代码

方式2:关注下方公众号,回复?130?下载代码

?

?

;原文链接:https://blog.csdn.net/dkm123456/article/details/116095303
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:关于汽车电子测试工程师 下一篇:没有了

推荐图文


随机推荐