小言_互联网的博客

用JavaScript canvas做的走迷宫游戏,肝了一下午,请帮忙点个赞!

355人阅读  评论(0)

引言:

上午女儿跟我去逛超市,在文具区看到一本书,总共有10幅图都是小迷宫游戏,图什么的都挺漂亮,就是有点贵应该是纸比较好,要30多块钱,我就觉得划不来(典型的铁公鸡),我就跟女儿说家里有,买了其他东西就回来了,然后网上查了一下,主要用到的是一个算法,于是吃完午饭就开始写了,这就学马老师来一波回首掏!

有人可能会说你这人真抠门,这点钱都舍不得掏。

我会说:这是钱的问题吗?这是专业,我们程序员的钱有那么好赚吗?我待会就跟我老婆要30块钱,说我买了个迷宫游戏书,我们程序员的钱不就是敲代码来的吗,变现有问题?

效果

刷新就可以换一个通道,不比书香,可以一直玩,一直玩一直爽。

算法(网上抄的)

            1.将起点作为当前迷宫单元并标记为已访问
            2.当还存在未标记的迷宫单元,进行循环
                1).如果当前迷宫单元有未被访问过的的相邻的迷宫单元
                    (1).随机选择一个未访问的相邻迷宫单元
                    (2).将当前迷宫单元入栈
                    (3).移除当前迷宫单元与相邻迷宫单元的墙
                    (4).标记相邻迷宫单元并用它作为当前迷宫单元
                2).如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
                    (1).栈顶的迷宫单元出栈
                    (2).令其成为当前迷宫单元

这个算法叫做“深度优先”,简单来说,就是从起点开始走,寻找它的上下左右4个邻居,然后随机一个走,到走不通的时候就返回上一步继续走,直到全部单元都走完。

实现思路(这个自己写的)

1.创建格子单元对象。

2.通过算法将这些格子打通,绘制出迷宫的形状。

3.绘制入口与终点的格子。

4.添加键盘的上、下、左、右移动事件,写好对应的函数,到达终点提示胜利。

相关图示图

1.每个单元的墙,分为上墙、右墙、下墙、左墙,把这些墙用长度为4的数组表示,元素的值为true则表示墙存在,否则墙不存在,代码里数组的下标方式来确定墙是否存在。

2.单元是根据行列来创建的,会用到双循环,类似表格,比如第二行用 i 来表示的话就是 1,第3列用 j 来表示就是2,那第二行第3列的元素组合起来就是(1,2)

3.那同理它的上邻居就是(0,2),右邻居(1,3),下邻居(2,2),左邻居(1,1),也就是上下邻居是 i 减加1,左右邻居是 j 减加1。

4.正方形4个点的坐标分别为(x1,y1)(x2,y2)(x3,y3)(x4,y4),计算坐标的公式为:


  
  1. //左上角坐标
  2. x1= j*w;
  3. y1= i*w;
  4. //右上角坐标
  5. x2= (j+1)*w;
  6. y2= i*w;
  7. //右下角坐标
  8. x3= (j+1)*w;
  9. y3= (i+1)*w;
  10. //左下角坐标
  11. x4= j*w;
  12. y4= (i+1)*w;

计算坐标,假如每个正方形的宽高都是40,那么(1,2)这个单元的坐标如下图:

5.墙的处理,之前说到墙是以一个4个元素的数组来表示的,比如数组为:[true,true,true,true],则图为:

如果数组为[false,true,true,true],则图为:

6.如果要联通右边的邻居要怎么做呢?当前单元去除右墙,右边单元去除左墙,这样就联通了。

去除后就这样,以此类推

代码及讲解

新增构造函数

此构造函数不是直接利用Rect来绘制方形的,而是自己以绘制4条直线的方式来绘制的,既方形的上、右、下、左4条直线。

1.计算坐标,这个上面已经提过。

2.根据墙数组的值来确定是否绘制这条直线,[true,true,true,true]就绘制完整的方形,[false,true,true,true]的话,上边就会缺失。

代码


  
  1. //用4条直线画方形的构造函数
  2. function LineRect(o){
  3. this.x= 0, //x坐标
  4. this.y= 0, //y坐标
  5. this. init(o);
  6. this.axis( this.i, this.j);
  7. }
  8. LineRect.prototype. init=function(o){
  9. for( var key in o){
  10. this[key]=o[key];
  11. }
  12. //上右下左4面墙 true就表示要绘制
  13. this.walls=[ true, true, true, true];
  14. }
  15. //根据i,j计算出坐标
  16. LineRect.prototype.axis=function(i,j){
  17. var w = this.maze.dis;
  18. //i代表行 j代表列
  19. //左上角坐标
  20. this.x1=j*w;
  21. this.y1=i*w;
  22. //右上角坐标
  23. this.x2=(j+ 1)*w;
  24. this.y2=i*w;
  25. //右下角坐标
  26. this.x3=(j+ 1)*w;
  27. this.y3=(i+ 1)*w;
  28. //左下角坐标
  29. this.x4=j*w;
  30. this.y4=(i+ 1)*w;
  31. }
  32. //绘制函数
  33. LineRect.prototype.render=function(context){
  34. this.ctx=context;
  35. innerRender( this);
  36. function innerRender(obj){
  37. var ctx=obj.ctx;
  38. ctx.save()
  39. ctx.beginPath();
  40. ctx.translate(obj.x,obj.y);
  41. if(obj.lineWidth){
  42. ctx.lineWidth=obj.lineWidth;
  43. }
  44. //判断上、右、下、左 的墙,true的话墙就会有,否则墙就没有
  45. var top = obj.walls[ 0];
  46. var right = obj.walls[ 1];
  47. var bottom = obj.walls[ 2];
  48. var left = obj.walls[ 3];
  49. if(top){
  50. ctx.moveTo(obj.x1,obj.y1);
  51. ctx.lineTo(obj.x2,obj.y2);
  52. }
  53. if(right){
  54. ctx.moveTo(obj.x2,obj.y2);
  55. ctx.lineTo(obj.x3,obj.y3);
  56. }
  57. if(bottom){
  58. ctx.moveTo(obj.x3,obj.y3);
  59. ctx.lineTo(obj.x4,obj.y4);
  60. }
  61. if(left){
  62. ctx.moveTo(obj.x4,obj.y4);
  63. ctx.lineTo(obj.x1,obj.y1);
  64. }
  65. obj.strokeStyle?(ctx.strokeStyle=obj.strokeStyle): null;
  66. ctx.stroke();
  67. ctx.restore();
  68. }
  69. return this;
  70. }

绘制


  
  1. Maze.prototype.drawGrid=function(){
  2. this.rows = Math.floor( this.h/ this.dis);
  3. this.cols = Math.floor( this.w/ this.dis);
  4. //根据行数、列数来创建格子
  5. for( var i= 0;i< this.rows;i++){
  6. for( var j= 0;j< this.cols;j++){
  7. var cell = this.buildCell(i,j);
  8. this.renderArr.push(cell);
  9. }
  10. }
  11. }
  12. //创建格子
  13. Maze.prototype.buildCell=function(i,j){
  14. var param={i:i,j:j,lineWidth: 1,maze: this};
  15. //创建格子对象
  16. var cell = new LineRect(param);
  17. return cell;
  18. }

根据算法打通墙

给每个单元格对象都增加邻居查找方法


  
  1. //查找当前单元是否有未被访问的邻居单元
  2. LineRect.prototype.findNeighbors=function(){
  3. //邻居分为上下左右
  4. var maze = this.maze ;
  5. this.arr = maze.renderArr;
  6. var res=[]; //返回的数组
  7. var top = this.getNeighbor( '0');
  8. var right = this.getNeighbor( '1');
  9. var bottom = this.getNeighbor( '2');
  10. var left = this.getNeighbor( '3');
  11. if(top){
  12. res.push(top);
  13. }
  14. if(right){
  15. res.push(right);
  16. }
  17. if(bottom){
  18. res.push(bottom);
  19. }
  20. if(left){
  21. res.push(left);
  22. }
  23. return res; //返回邻居数组
  24. }
  25. //查找邻居
  26. LineRect.prototype.getNeighbor=function(type,lost_visited){
  27. var key,neighbor;
  28. if(type== '0'){
  29. key = this.assemKey( this.i- 1, this.j);
  30. } else if(type== '1'){
  31. key = this.assemKey( this.i, this.j+ 1);
  32. } else if(type== '2'){
  33. key = this.assemKey( this.i+ 1, this.j);
  34. } else if(type== '3'){
  35. key = this.assemKey( this.i, this.j- 1);
  36. }
  37. if(key){
  38. neighbor = this.arr[key]; //首先找到这个邻居
  39. if(neighbor.visited && !lost_visited){ //判断是否被访问,如果被访问了返回undefined lost_visited表示是否忽略访问的情况
  40. neighbor = undefined;
  41. }
  42. }
  43. return neighbor;
  44. }
  45. //根据i,j计算数组单元在数组中的下标值
  46. LineRect.prototype.assemKey=function(i,j){
  47. if(i< 0 || j< 0 || i>= this.maze.rows || j>= this.maze.cols){ //超出边界了
  48. return undefined;
  49. }
  50. return i* this.maze.cols+j; //计算出i,j位置单元在数组中的下标
  51. }

计算

跟着算法来写的代码,唯一要注意的是我设置了一个值unVisitedCount,初始值为所有单元的数量,每当一个单元被标记为已访问后,这个值就递减1,当值为0后就终止循环,结束算法。


  
  1. Maze.prototype.computed=function(){
  2. /*
  3. 1.将起点作为当前迷宫单元并标记为已访问
  4. 2.当还存在未标记的迷宫单元,进行循环
  5. 1).如果当前迷宫单元有未被访问过的的相邻的迷宫单元
  6. (1).随机选择一个未访问的相邻迷宫单元
  7. (2).将当前迷宫单元入栈
  8. (3).移除当前迷宫单元与相邻迷宫单元的墙
  9. (4).标记相邻迷宫单元并用它作为当前迷宫单元
  10. 2).如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
  11. (1).栈顶的迷宫单元出栈
  12. (2).令其成为当前迷宫单元
  13. */
  14. var stack = this.stack ; //栈
  15. var arr = this.renderArr;
  16. var current = arr[ 0]; //取第一个为当前单元
  17. this.pathArr.push(current);
  18. current.visited= true; //标记为已访问
  19. var unVisitedCount=arr.length- 1; //因为第一个已经设置为访问了
  20. var neighbors ;
  21. while(unVisitedCount> 0){
  22. neighbors = current.findNeighbors(); //查找邻居集合(未被访问的)
  23. if(neighbors.length> 0){ //如果当前迷宫单元有未被访问过的的相邻的迷宫单元
  24. //随机选择一个未访问的相邻迷宫单元
  25. var index = _.getRandom( 0,neighbors.length);
  26. var next = neighbors[index];
  27. //将当前迷宫单元入栈
  28. stack.push(current);
  29. //移除当前迷宫单元与相邻迷宫单元的墙
  30. this.removeWall(current,next);
  31. //标记相邻迷宫单元并用它作为当前迷宫单元
  32. next.visited= true;
  33. //标记一个为访问,则计数器递减1
  34. unVisitedCount--; //递减
  35. current = next;
  36. } else if(stack.length> 0){ //如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
  37. /*
  38. 1.栈顶的迷宫单元出栈
  39. 2.令其成为当前迷宫单元
  40. */
  41. var cell = stack.pop();
  42. current = cell;
  43. }
  44. //推入路线数组
  45. this.pathArr.push(current);
  46. }
  47. }

移除墙


  
  1. //移除当前迷宫单元与相邻迷宫单元的墙
  2. Maze.prototype.removeWall= function(a,b){
  3. if(a.i==b.i){ //横向邻居
  4. if(a.j>b.j){ //匹配到的是左边邻居
  5. //左边邻居的话,要移除自己的左墙和邻居的右墙
  6. a.walls[ 3]= false;
  7. b.walls[ 1]= false;
  8. } else{ //匹配到的是右边邻居
  9. //右边邻居的话,要移除自己的右墙和邻居的左墙
  10. a.walls[ 1]= false;
  11. b.walls[ 3]= false;
  12. }
  13. } else if(a.j==b.j){ //纵向邻居
  14. if(a.i>b.i){ //匹配到的是上边邻居
  15. //上边邻居的话,要移除自己的上墙和邻居的下墙
  16. a.walls[ 0]= false;
  17. b.walls[ 2]= false;
  18. } else{ //匹配到的是下边邻居
  19. //下边邻居的话,要移除自己的下墙和邻居的上墙
  20. a.walls[ 2]= false;
  21. b.walls[ 0]= false;
  22. }
  23. }
  24. }

绘制入口出口


  
  1. //创建起点和终点格子
  2. Maze.prototype.drawRunCell=function(i,j){
  3. var end = new _.Rect({
  4. x:( this.cols- 1)* this.dis+ this.dis2,
  5. y:( this.rows- 1)* this.dis+ this.dis2,
  6. width: this.dis- 2* this.dis2,
  7. height: this.dis- 2* this.dis2,
  8. fill: true,
  9. fillStyle: 'red'
  10. });
  11. end.i= this.rows- 1,end.j= this.cols- 1; //设定i,j值,判断是否终点
  12. this.renderArr2.push(end);
  13. var start = new _.Rect({
  14. x: 0+ this.dis2,
  15. y: 0+ this.dis2,
  16. width: this.dis- 2* this.dis2,
  17. height: this.dis- 2* this.dis2,
  18. fill: true,
  19. fillStyle: 'blue'
  20. });
  21. start.i= 0,start.j= 0; //设定i,j值,控制移动
  22. this.renderArr2.push(start);
  23. }

加入移动监听


  
  1. //按键的控制
  2. Maze.prototype.control= function(){
  3. var that= this;
  4. global.addEventListener( 'keydown', function(e){
  5. console.log(that.endFlag)
  6. if(that.endFlag) return ;
  7. var dir;
  8. switch (e.keyCode){
  9. case 87: //w
  10. case 38: //上
  11. dir= 0; //上移动
  12. break;
  13. case 83: //s
  14. case 40: //下
  15. dir= 2; //下移动
  16. break;
  17. case 65: //a
  18. case 37: //左
  19. dir= 3; //左移动
  20. break;
  21. case 68: //d
  22. case 39: //右
  23. dir= 1; //右移动
  24. break;
  25. }
  26. that.move(dir);
  27. //测试用,记得删除
  28. that.render();
  29. });
  30. }

加入移动函数


  
  1. //移动
  2. Maze.prototype.move=function(dir){
  3. var cur = this.renderArr2[ 1]; //当前移动的方块
  4. var key = this.assemKey(cur); //根据移动方块的i,j计算出key
  5. var cell = this.renderArr[key]; //得到移动方块对应的单元
  6. var wall = cell.walls[dir]; //得到对应的那面墙
  7. if(!wall){ //表示是没有墙能移动
  8. var neighbor = cell.getNeighbor(dir, 1);
  9. if(!neighbor){
  10. return ;
  11. }
  12. cur.x=neighbor.x1+ this.dis2;
  13. cur.y=neighbor.y1+ this.dis2;
  14. cur.i=neighbor.i;
  15. cur.j=neighbor.j;
  16. }
  17. var end = this.renderArr2[ 0];
  18. if(cur.i==end.i && cur.j==end.j ){
  19. this.endFlag= true;
  20. console.log( '完成');
  21. this.endShow();
  22. }
  23. }
  24. Maze.prototype.assemKey=function(e){
  25. return e.i* this.cols+e.j; //计算出i,j位置单元在数组中的下标
  26. }

加入胜利图


  
  1. //展示结束的图片(胜利)
  2. Maze.prototype.endShow=function(){
  3. var image,img,sx= 0,sy= 0,sWidth= 225,sHeight= 108,dx= this.w /2-110,dy=this.h/ 2 -100,dWidth= 225,dHeight= 108;
  4. image = this.imgObj[ 'suc'];
  5. img = new _.ImageDraw({ image:image, sx:sx, sy:sy, sWidth:sWidth, sHeight:sHeight, dx:dx, dy:dy , dWidth:dWidth, dHeight:dHeight});
  6. this.renderArr2.push(img);
  7. this.render();
  8. }

写出来也花了不少脑细胞,能看到这里的都是大佬,我去找老婆提现去了。

欢迎各位大佬 点赞+评论+关注,谢谢!

源码下载

方式1:少量积分,下载代码

方式2:关注下方公众号,回复 130 下载代码

 

 


转载:https://blog.csdn.net/dkm123456/article/details/116095303
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场