这个系列分为两部分,第一部分为迷宫的生成及操作,第二部分为自动寻路算法。

我们先看效果:

See the Pen QGKBjm by fanyipin (@fanyipin) on CodePen.

我们直入正题,先说一说生成迷宫的思路。

整个思路十分简单:

首先我们将迷宫视为一个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();
},

到这里我们的迷宫便完成了。

canvas实例 ---- 制作简易迷宫(一)的更多相关文章

  1. Kivy 中文教程 实例入门 简易画板 (Simple Paint App):3. 随机颜色及清除按钮

    1. 随机颜色 通过前面的教程,咪博士已经带大家实现了画板的绘图功能.但是,现在画板只能画出黄色的图案,还十分单调,接下来咪博士就教大家,如何使用随机颜色,让画板变得五彩斑斓. 改进后的代码如下: f ...

  2. Kivy 中文教程 实例入门 简易画板 (Simple Paint App):2. 实现绘图功能

    1. 理解 kivy 坐标系统 上一节中,咪博士带大家实现了画板程序的基础框架,以及一个基本的自定义窗口部件(widget).在上一节的末尾,咪博士留了一道关于 kivy 坐标系统的思考题给大家.通过 ...

  3. 免费IP代理池定时维护,封装通用爬虫工具类每次随机更新IP代理池跟UserAgent池,并制作简易流量爬虫

    前言 我们之前的爬虫都是模拟成浏览器后直接爬取,并没有动态设置IP代理以及UserAgent标识,本文记录免费IP代理池定时维护,封装通用爬虫工具类每次随机更新IP代理池跟UserAgent池,并制作 ...

  4. 小项目特供 简易迷宫(基于Java)

    明天返校,于是昨天和今天简单熟系了一下JAVA的GUI,做了一个简易的迷宫小游戏(暂时没有时间实现随机迷宫及多关卡,仅供学习) 源码及运行文件(提供JRE8):链接:简易迷宫 密码:hy8v

  5. C#制作简易屏保(转)

    C#制作简易屏保[原创] 原始网址: http://www.cnblogs.com/drizzlecrj/archive/2006/10/06/522182.html 2006-10-06 16:25 ...

  6. 【百度地图API】——如何用label制作简易的房产标签

    原文:[百度地图API]--如何用label制作简易的房产标签 摘要: 最近,API爱好者们纷纷说,自定义marker太复杂了!不仅定义复杂,连所有的dom事件都要自己重新定义.有没有快速简易创建房产 ...

  7. 超多经典 canvas 实例,动态离子背景、移动炫彩小球、贪吃蛇、坦克大战、是男人就下100层、心形文字等等等

    超多经典 canvas 实例 普及:<canvas> 元素用于在网页上绘制图形.这是一个图形容器,您可以控制其每一像素,必须使用脚本来绘制图形. 注意:IE 8 以及更早的版本不支持 &l ...

  8. Kivy 中文教程 实例入门 简易画板 (Simple Paint App):1. 自定义窗口部件 (widget)

    1. 框架代码 用 PyCharm 新建一个名为 SimplePaintApp 的项目,然后新建一个名为 simple_paint_app.py 的 Python 源文件, 在代码编辑器中,输入以下框 ...

  9. Kivy 中文教程 实例入门 简易画板 (Simple Paint App):0. 项目简介 & 成果展示

    本教程咪博士将带领大家学习创建自己的窗口部件 (widget).最终,我们完成的作品是一个简易的画板程序. 当用 kivy 创建应用时,我们需要仔细思考以下 3 个问题: 我们创建的应用需要处理什么数 ...

随机推荐

  1. MySQL 优化数据库对象

    一.考虑是用 procedure analyse() 函数对当前应用的表进行分析.字段类型是否可优化. 二.通过拆分提高表的访问效率. (A) 针对MyISAM表,有两种拆分方法: 垂直拆分:主码和某 ...

  2. TJpgDec—轻量级JPEG解码器

    TJpgDec-轻量级JPEG解码器 本文由乌合之众lym瞎编,欢迎转载blog.cnblogs.net/oloroso 下文中解码一词皆由decompression/decompress翻译而来. ...

  3. Java并发1——线程创建、启动、生命周期与线程控制

    内容提要: 线程与进程 为什么要使用多线程/进程?线程与进程的区别?线程对比进程的优势?Java中有多进程吗? 线程的创建与启动 线程的创建有哪几种方式?它们之间有什么区别? 线程的生命周期与线程控制 ...

  4. ASP.NET MVC RenderPartial和Partial的区别

    背景:ASP.NET MVC 4.0 @{ Html.RenderPartial(...); } public static void RenderPartial(this HtmlHelper ht ...

  5. matlab绘图基础

    matlab绘制条形图并分组显示: a =[1 2 3] b =[4 5 6] >> d=[a;b] d = 1 2 3 4 5 6 >> bar(d,'group') 修改横 ...

  6. SQL 基础

    创建模式 create schema <schema_name> authorization <username> 没有指定schema_name时默认是用户名 删除模式 dr ...

  7. HTML 5 背离贪吃蛇 写成了类似于屏幕校准

    中间写了改 改了写 还是没做出自己满意的效果 ,看来自己的确不是一个走前端的料子.当然h5还是学一点好一点 具体说来 就是 在canvas 的画布中 鼠标点击后画上一个圆形 然后就有随机的在画布上面出 ...

  8. 解决VMware虚拟机宿主机与虚拟机通讯慢

    本地连接--> 属性 --> 配置(C) -->高级 页面的属性框中找到"Large Send Offload"(中文名称叫:大量传送减负)==>Disab ...

  9. UILabel 根据文本内容设置frame

    CGRect senderFrame = cell.senderLabel.frame;    CGRect creatAtFrame = cell.creatAtLabel.frame;    CG ...

  10. SQL Server数据阻塞原因

    阻塞形成原因 是由于SQL Server是高并发的,同一时间会有很多用户访问,为了保证数据一致性和数据安全,引入了锁的机制.同一时间只有拿到钥匙的用户能够访问,而其他用户需要等待. 死锁形成四大必要条 ...