原生js生成迷宫 (一)

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

我们先看效果:

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();
},

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

时间: 2024-08-07 04:15:20

原生js生成迷宫 (一)的相关文章

原生js怎么为动态生成的标签添加各种事件

这几天用zepto.js写了不少事件,突然想到一个问题,那就是原生的js如何给动态生成的标签添加事件?因为这些标签都是后来通过ajax或者运行其他点击事件生成的,那么如果之前给他们写事件他们这个dom对象是找不到的,jq通过事件委托解决了这个问题,但是原生js这个问题该怎么解决呢?我在网上查了很多资料,好像只有一种办法,那就是在生成标签并把标签添加到html结构中后再添加对于这个新标签的各种事件,如果有更好的方法,欢迎提出来. <!DOCTYPE html> <html lang=&qu

用jQuery基于原生js封装的轮播

我发现轮播在很多网站里面都用到过,一个绚丽的轮播可以为网页增色不少,最近闲来无事,也用原生js封装了一个轮播,可能不像网上的插件那么炫,但是也有用心去做.主要用了闭包的思想.需要传递的参数有:图片地址的数组,图片宽度,上一页,下一页的id,图片列表即ul的id(这儿使用无序列表排列的图片),自动轮播间隔的时间.功能:实现了轮播的自动轮播,可以点击上一页,下一页进行切换. 下面是html中的代码,只需要把存放的容器写好,引入jquery即可: <!DOCTYPE html><html>

AJAX请求和跨域请求详解(原生JS、Jquery)

一.概述 AJAX 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术. AJAX = 异步 JavaScript 和 XML,是一种用于创建快速动态网页的技术.通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新.这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新.传统的网页(不使用 AJAX)如果需要更新内容,必需重载整个网页面. 本博客实验环境: python:2.7.11 web框架:tonado jquery:2.1.1 二.“伪”AJAX 由于

通过原生js的ajax或jquery的ajax获取服务器的时间

在实际的业务逻辑中,经常是与时间相关的,而前端能获得的时间有两个:客户端的时间,服务器的时间.客户端时间通过 javascript中的Date对象可以获取,如 Java代码   var dt = new Date(); var tm = dt.getTime(); 那么tm就是客户端的时间,另外也可以通过对应的getFullYear(),getMonth(),getDate()取到对应的年月日等...但这个时间可靠吗?好吧,那取服务器时间吧经常用到的是后台写一个php,jsp,cgi,asp..

原生js canvas 碰撞游戏的开发笔记

-----------------------------------------------福利--------------------------------------------- -----------------------------------------------分割线--------------------------------------------- 今天 我们研究下碰撞游戏 什么是碰撞游戏? 当然是东西碰到在一起啦 用前端逻辑来说 就是2个物品互相碰撞产生的事件 问

原生JS实现瀑布流

浏览网页的时候经常会遇到瀑布流布局的网站.也许有些读者不了解瀑布流.瀑布流,又称瀑布流式布局.是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部.比如下面图片的效果,就是一个典型的瀑布流. 网上有很多JQ的瀑布流插件,而且都写好了兼容,都可以尝试去使用,这里只是跟大家分享一下原生js实现瀑布流的效果,一起学习. 一步一步来: 首先新建一个文件,就叫瀑布流.html吧. <!doctype html> <html

原生js打飞机小游戏

最近为了巩固一下原生的知识,然后拿了一个js小游戏来入手.主要也是为了学习和练手. js代码如下: 1 window.onload = function(){ 2 var oBtn = document.getElementById('gameBtn'); 3 oBtn.onclick = function(){ 4 this.style.display = 'none'; 5 Game.init('div1');//把父级传进去 6 }; 7 }; 8 //创建Game单体 9 10 var

【第2章第300回】原生JS与jQuery对AJAX的实现

一.定义 W3C里这么解释AJAX: AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML).AJAX 不是新的编程语言,而是一种使用现有标准的新方法.AJAX 是与服务器交换数据并更新部分网页的艺术,在不重新加载整个页面的情况下. 就是利用JS来无刷新与后端交互,通过get和post方式把数据发送到后端,或者请求后端的数据,然后根据请求的数据进行改变DOM节点等操作,从而取消掉用form的submit方式一提交就会跳转页面的情况

原生JS面向对象思想封装轮播图组件

原生JS面向对象思想封装轮播图组件 在前端页面开发过程中,页面中的轮播图特效很常见,因此我就想封装一个自己的原生JS的轮播图组件.有了这个需求就开始着手准备了,代码当然是以简洁为目标,轮播图的各个功能实现都分别分为不同的模块.目前我封装的这个版本还不适配移动端,只适配PC端. 主要的功能有:自动轮播,点击某一张图片对应的小圆点就跳转到指定图片,有前后切换按钮.使用的时候只需要传入图片的路径以及每张图片分别所对应的跳转路径还有目标盒子ID就可以了,还可以自定义每张图轮播的延时,不过延时参数不是必须