资讯专栏INFORMATION COLUMN

canvas实例 --- 制作简易迷宫

孙吉亮 / 585人阅读

摘要:整个思路十分简单首先我们将迷宫视为一个行列的单元格组合,每一个单元格便可以表示为。用来储存我们已访问过的单元格,则记录我们的访问路径。我们通过将单元格的,,,属性设置为或来标识这个方向是否应该有边框,同时该方向是否可走。

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

我们先看效果:点击查看

我们直入正题,先说一说生成迷宫的思路。
整个思路十分简单:

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/104814.html

相关文章

  • canvas实例 --- 制作简易迷宫

    摘要:整个思路十分简单首先我们将迷宫视为一个行列的单元格组合,每一个单元格便可以表示为。用来储存我们已访问过的单元格,则记录我们的访问路径。我们通过将单元格的,,,属性设置为或来标识这个方向是否应该有边框,同时该方向是否可走。 这个系列分为两部分,第一部分为迷宫的生成及操作,第二部分为自动寻路算法。 我们先看效果:点击查看 我们直入正题,先说一说生成迷宫的思路。 整个思路十分简单: 首先...

    twohappy 评论0 收藏0
  • SegmentFault 技术周刊 Vol.35 - WebGL:打开网页看大片

    摘要:在文末,我会附上一个可加载的模型方便学习中文艺术字渲染用原生可以很容易地绘制文字,但是原生提供的文字效果美化功能十分有限。 showImg(https://segmentfault.com/img/bVWYnb?w=900&h=385); WebGL 可以说是 HTML5 技术生态链中最为令人振奋的标准之一,它把 Web 带入了 3D 的时代。 初识 WebGL 先通过几个使用 Web...

    objc94 评论0 收藏0
  • 快速实现一个简单的canvas迷宫游戏

    摘要:所以我先写了一个样例扔在服务器上,大家可以先体验一下效果用成就感作为驱动力哈哈哈点我体验地址正文实现这个小游戏也不难,让我们想想,一个迷宫游戏有哪些基本要素。迷宫地图的生成,可以借助谷歌的一个迷宫在线生成器来获得。 前言 (最近设计模式看的有点头大,一直面对纯js实在是有些枯燥-_-。所以写一点有趣的东西调剂一下)现在canvas已经不算新鲜了,不过由于日常业务中并不常用,所以实践并不...

    wangdai 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<