这个系列分为两部分,第一部分为迷宫的生成及操作,第二部分为自动寻路算法。
我们先看效果:点击查看
我们直入正题,先说一说生成迷宫的思路。
整个思路十分简单:
首先我们将迷宫视为一个 m 行 n 列的单元格组合,每一个单元格便可以表示为 maze[i][j]。接下来迷宫与 m * n 单元格的区别是什么呢?对,迷宫就是相当于不同单元格以某种规律相互连通,也就相当于我们把相邻的两个单元格之间的重合线给去掉,然后按照某种规律循环,便可生成一个迷宫。
我们假定从左上角开始出发,遍历每一个单元格,如果该单元格未被访问过,则查看其相邻元素(上,下,左,右)是否有未访问的单元格,如果有则随机取出一个相邻元素并打通他们之间的重合线,如果没有则回退到上一个单元格。
上代码:
首先我们创建一个构造函数:
function Maze(obj,col,row){
this.col = col || 10;
this.row = row || 10;
this.canvas = obj.getContext('2d');
this.init();}
在这个构造函数中,我们接收三个参数,分别为 canvas 元素,迷宫的行数与列数,并直接调用 Maze 的 init 方法。
init : function(){this.cell = (width - 2) / this.col;
for(var i = 0 ; i < this.row ; i++){maze_cells[i] = [];
for(var j = 0; j < this.col ; j++){maze_cells[i].push({
'x' : j,
'y' : i,
'top' : false,
'bottom' : false,
'left' : false,
'right' : false,
'isVisited' : false,
'g' : 0,
'h' : 0,
'f' : 0
})
}
}
start_cell = {'x' : 0, 'y' : 0};
start_row = start_cell.x;
start_col = start_cell.y;
visitRooms.push(start_cell)
roomsLine.push(start_cell)
maze_cells[0][0].isVisited = true;
maze_cells[0][0].top = true;
maze_cells[this.row-1][this.col-1].bottom = true;
this.calcCells(0,0,maze_cells);
this.drawCells();
maze_cells[0][0].top = false;
maze_cells[this.row-1][this.col-1].bottom = false;
this.drawRect(start_col,start_row);
this.bindEvent();},
在 init 方法中,我们首先根据传入的列数 col 来计算单元格的宽度,然后构建一个 maze_cells 对象,其中每一行为一个数组,每个单元格包含的值分别代表 x,y 坐标,上下左右 4 个方向是否可以通行,是否访问过,还有该单元格的 g,h,f 值。我们假定迷宫的开口位于整个迷宫的左上角,出口位于右下角。visitRooms 用来储存我们已访问过的单元格,roomLine 则记录我们的访问路径。我们将迷宫的入口处和出口处的 top,bottom 分别设为 true 后再设置为 false 是为了在绘制的过程中不出现边框,绘制完成后保证不能向上(下)移动。
ps:canvas 绘制线条是居中于我们坐标的,即在(1,1)处绘制宽度为 2 的线条起始是从(0,1)开始的,所以我们用整个 canvas 的宽度减去了线条的宽度 2,当然这里也可以设置为变量更方便修改。
接下来我们需要遍历每一个单元格,如下通过递归的形式访问每一个单元格,当某一个单元格的相邻元素全部被访问过并且 roomLine 数组为空时就意味着我们已经访问了所有的单元格,具体原因自行脑补。
calcCells : function(x,y,arr){var neighbors = [];
if(x-1 >=0 && !maze_cells[x-1][y].isVisited){neighbors.push({'x' : x-1 ,'y' : y})
}
if(x+1 < this.row && !maze_cells[x+1][y].isVisited){neighbors.push({'x' : x+1 ,'y' : y})
}
if(y-1 >=0 && !maze_cells[x][y-1].isVisited){neighbors.push({'x' : x ,'y' : y-1})
}
if(y+1 <this.col && !maze_cells[x][y+1].isVisited){neighbors.push({'x' : x ,'y' : y+1})
}
if(neighbors.length>0){ // 相邻房间有未访问房间
var current = {'x' : x , 'y' : y};
var next = neighbors[Math.floor(Math.random() * neighbors.length)];
maze_cells[next.x][next.y].isVisited = true;
visitRooms.push({'x' : next.x , 'y' : next.y})
roomsLine.push({'x' : next.x , 'y' : next.y});
this.breakWall(current,next);
this.calcCells(next.x,next.y,arr)
}else{var next = roomsLine.pop();
if(next != null){this.calcCells(next.x,next.y,arr)
}
}
},
我们看到如果当前单元格的相邻单元格有未访问的,则执行 breakWall 方法,即打通当前单元格与相邻单元格中间的墙,当然我们应该随机选择一个未访问的相邻单元格。我们通过将单元格的 top,bottom,left,right 属性设置为 true 或 false 来标识这个方向是否应该有边框,同时该方向是否可走。
breakWall : function(cur,next){if(cur.x < next.x){maze_cells[cur.x][cur.y].bottom = true;
maze_cells[next.x][next.y].top = true;
}
if(cur.x > next.x){maze_cells[cur.x][cur.y].top = true;
maze_cells[next.x][next.y].bottom = true;
}
if(cur.y < next.y){maze_cells[cur.x][cur.y].right = true;
maze_cells[next.x][next.y].left = true;
}
if(cur.y > next.y){maze_cells[cur.x][cur.y].left = true;
maze_cells[next.x][next.y].right = true;
}
},
进行完上面的两步,我们的一个完整数组已经构成了,接下来便可以开始绘制了,top,left,right,bottom 为 false 时则有边框,true 时无边框。这一步比较简单,我们在结尾调用了一个 drawOffset 方法,该方法将创建一个离屏对象,这样我们在动态修改迷宫的时候可以直接将离屏的图像绘制到当前画布中。
drawCells : function(){
var ctx = this.canvas, //canvas 对象
w = this.cell;
ctx.clearRect(0,0,$('canvas').width,$('canvas').height)
ctx.beginPath();
ctx.save();
ctx.translate(1,1)
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
for(var i in maze_cells){ //i 为 row
var len = maze_cells[i].length;
for(var j = 0; j < len; j++){var cell = maze_cells[i][j];
i = parseInt(i);
if(!cell.top){ctx.moveTo(j*w,i*w);
ctx.lineTo((j+1)*w ,i*w);
}
if(!cell.bottom){ctx.moveTo(j*w,(i+1)*w);
ctx.lineTo((j+1)*w ,(i+1)*w)
}
if(!cell.left){ctx.moveTo(j*w,i*w);
ctx.lineTo(j*w,(i+1)*w )
}
if(!cell.right){ctx.moveTo((j+1)*w,i*w);
ctx.lineTo((j+1)*w,(i+1)*w)
}
}
}
ctx.stroke();
ctx.restore();
this.drawOffset();},
drawOffset : function(){var offsetCanvas = document.createElement('canvas');
offsetCanvas.id = 'offset';
document.body.appendChild(offsetCanvas);
offsetCanvas.width = $('canvas').width;
offsetCanvas.height = $('canvas').height;
var offset = $('offset').getContext('2d');
offset.clearRect(0,0,$('canvas').width,$('canvas').height)
offset.drawImage($('canvas'),0,0,offsetCanvas.width,offsetCanvas.height);
$('offset').style.display ='none'
},
绑定事件比较简单,我们为 window 监听 keydown 事件,根据不同的 keyCode 来判断我们应该行走的方向。
var _self = this;
window.addEventListener('keydown',function(event){switch (event.keyCode) {
case 37 :
event.preventDefault();
if(maze_cells[start_row][start_col].left){start_col --;}
break;
case 38 :
event.preventDefault();
if(maze_cells[start_row][start_col].top){start_row --;}
break;
case 39 :
event.preventDefault();
if(maze_cells[start_row][start_col].right){start_col ++}
break;
case 40 :
event.preventDefault();
if(maze_cells[start_row][start_col].bottom){start_row ++;}
break;
}
_self.drawRect(start_col,start_row);
if(start_col == (_self.col - 1) && start_row == (_self.row - 1)){alert('到达终点了')
}
});
drawRect 便是我们移动的目标。
drawRect : function(col,row){
var ctx = this.canvas;
ctx.save();
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage($('offset'),0,0)
ctx.translate(2,2)
ctx.fillStyle = '#ff0000';
ctx.fillRect(col*this.cell,row*this.cell,this.cell-2,this.cell-2);
ctx.restore();},
到这里我们的迷宫便完成了。
作者:易企秀——樊一