canvas实例 ---- 制作简易迷宫(一)
这个系列分为两部分,第一部分为迷宫的生成及操作,第二部分为自动寻路算法。
我们先看效果:
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实例 ---- 制作简易迷宫(一)的更多相关文章
- Kivy 中文教程 实例入门 简易画板 (Simple Paint App):3. 随机颜色及清除按钮
1. 随机颜色 通过前面的教程,咪博士已经带大家实现了画板的绘图功能.但是,现在画板只能画出黄色的图案,还十分单调,接下来咪博士就教大家,如何使用随机颜色,让画板变得五彩斑斓. 改进后的代码如下: f ...
- Kivy 中文教程 实例入门 简易画板 (Simple Paint App):2. 实现绘图功能
1. 理解 kivy 坐标系统 上一节中,咪博士带大家实现了画板程序的基础框架,以及一个基本的自定义窗口部件(widget).在上一节的末尾,咪博士留了一道关于 kivy 坐标系统的思考题给大家.通过 ...
- 免费IP代理池定时维护,封装通用爬虫工具类每次随机更新IP代理池跟UserAgent池,并制作简易流量爬虫
前言 我们之前的爬虫都是模拟成浏览器后直接爬取,并没有动态设置IP代理以及UserAgent标识,本文记录免费IP代理池定时维护,封装通用爬虫工具类每次随机更新IP代理池跟UserAgent池,并制作 ...
- 小项目特供 简易迷宫(基于Java)
明天返校,于是昨天和今天简单熟系了一下JAVA的GUI,做了一个简易的迷宫小游戏(暂时没有时间实现随机迷宫及多关卡,仅供学习) 源码及运行文件(提供JRE8):链接:简易迷宫 密码:hy8v
- C#制作简易屏保(转)
C#制作简易屏保[原创] 原始网址: http://www.cnblogs.com/drizzlecrj/archive/2006/10/06/522182.html 2006-10-06 16:25 ...
- 【百度地图API】——如何用label制作简易的房产标签
原文:[百度地图API]--如何用label制作简易的房产标签 摘要: 最近,API爱好者们纷纷说,自定义marker太复杂了!不仅定义复杂,连所有的dom事件都要自己重新定义.有没有快速简易创建房产 ...
- 超多经典 canvas 实例,动态离子背景、移动炫彩小球、贪吃蛇、坦克大战、是男人就下100层、心形文字等等等
超多经典 canvas 实例 普及:<canvas> 元素用于在网页上绘制图形.这是一个图形容器,您可以控制其每一像素,必须使用脚本来绘制图形. 注意:IE 8 以及更早的版本不支持 &l ...
- Kivy 中文教程 实例入门 简易画板 (Simple Paint App):1. 自定义窗口部件 (widget)
1. 框架代码 用 PyCharm 新建一个名为 SimplePaintApp 的项目,然后新建一个名为 simple_paint_app.py 的 Python 源文件, 在代码编辑器中,输入以下框 ...
- Kivy 中文教程 实例入门 简易画板 (Simple Paint App):0. 项目简介 & 成果展示
本教程咪博士将带领大家学习创建自己的窗口部件 (widget).最终,我们完成的作品是一个简易的画板程序. 当用 kivy 创建应用时,我们需要仔细思考以下 3 个问题: 我们创建的应用需要处理什么数 ...
随机推荐
- WebClient 实现多文件/文本同时上传
public class CreateBytes { Encoding encoding = Encoding.UTF8; /**/ /// <summary> /// 拼接所有的二进制数 ...
- gitlab使用个人版v16.11
title: gitlab使用个人版v16.11 date: 2016-11-13 20:53:00 tags: [gitlab] --- 1.安装gitbash 附上地址链接:git 2.配置git ...
- Java的垃圾回收和内存分配策略
本文是<深入理解Java虚拟机 JVM高级特性与最佳实践>的读书笔记 在介绍Java的垃圾回收方法之前,我们先来了解一下Java虚拟机在执行Java程序的过程中把它管理的内存划分为若干个不 ...
- linux(centOS)下安装Oracle步骤
##创建用户和组: [LanRS@localhost ~]$ su root #切换到root Password: [root@localhost LanRS]# groupadd oinstall ...
- CorelDRAW x6 X8安装失败解决方法
CorelDRAW x6 X8自定义安装时,到最后经常会出现以下问题: 解决方法如下: 在自定义安装时,出现以下这个界面时,点击红色箭头的地方 将下图红色箭头指向的选项,点击取消,不要选上,即可解决安 ...
- 关于so文件cp覆盖导致调用者core的研究
先说cp好mv/rm的区别: cp from to,则被覆盖文件 to的inode依旧不变(属性也不变),内容变为from的: mv from to,则to的inode变为from的,相应的,to的属 ...
- mybatis调用存储过程 无参、带有输入输出参数,输出游标类型的 存储
存储过程在小公司用的不多,但是如果业务比较复杂或者性能要求比较苛刻的时候存储过程就派上用场了,ibatis的前期的一些版本貌似不支持存储过程因此我选择了mybatis来做实验. 1.无输入和输出参数的 ...
- SQL(横表和纵表)行列转换,PIVOT与UNPIVOT的区别和使用方法举例,合并列的例子
使用过SQL Server 2000的人都知道,要想实现行列转换,必须综合利用聚合函数和动态SQL,具体实现起来需要一定的技巧,而在SQL Server 2005中,使用新引进的关键字PIVOT/UN ...
- android 去掉actionbar 的虚线
if(Build.VERSION.SDK_INT>=21){ getSupportActionBar().setElevation(0); }
- qt 定时器
重写方法 virtual void timerEvent(QTimerEvent *event); 启动定时器 timerId = startTimer(500);