引言:
上午女儿跟我去逛超市,在文具区看到一本书,总共有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();
}
写出来也花了不少脑细胞,能看到这里的都是大佬,我去找老婆提现去了。