经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » HTML/CSS » HTML5 » 查看文章
教你如何一步一步用Canvas写一个贪吃蛇
来源:jb51  时间:2018/10/26 9:48:26  对本文有异议

之前在慕课网看了几集Canvas的视频,一直想着写点东西练练手。感觉贪吃蛇算是比较简单的了,当年大学的时候还写过C语言字符版的,没想到还是遇到了很多问题。

最终效果如下(图太大的话 时间太长 录制gif的软件有时限...)

首先定义游戏区域。贪吃蛇的屏幕上只有蛇身和苹果两种元素,而这两个都可以用正方形格子构成。正方形之间添加缝隙。为什么要添加缝隙?你可以想象当你成功填满所有格子的时候,如果没有缝隙,就是一个实心的大正方形......你根本不知道蛇身什么样。

画了一个图。

 

格子是左上角的坐标是(0, 0),向右是横坐标增加,向下是纵坐标增加。这个方向和Canvas相同。

每次画一个格子的时候,要从左上角开始,我们直知道Canvas的左上角坐标是(0, 0),假设格子的边长是 GRID_WIDTH 缝隙的宽度是  GAP_WIDTH ,可以得到第(i, j)个格子的左上角坐标  (i*(GRID_WIDTH+GAP_WIDTH)+GAP_WIDTH, j*(GRID_WIDTH+GAP_WIDTH)+GAP_WIDTH) 。

假设现在蛇身是由三个蓝色的格子组成的,我们不能只绘制三个格子,两个紫色的空隙也一定要绘制,否则,还是之前说的,你根本不知道蛇身什么样。如下图,不画缝隙虽然也能玩,但是体验肯定不一样。

绘制相邻格子之间间隙 不绘制间隙

现在我们可以尝试着画一条蛇了。蛇身其实就是一个格子的集合,每个格子用包含两个位置信息的数组表示,整条蛇可以用二维数组表示。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>blog_snack</title>
  6.     <style>
  7.         #canvas {
  8.              background-color: #000;
  9.         }
  10.     </style>
  11. </head>
  12. <body>
  13.     <canvas id="canvas"></canvas>
  14.     <script>
  15.         const GRID_WIDTH = 10;  // 格子的边长
  16.         const GAP_WIDTH = 2;    // 空隙的边长
  17.         const ROW = 10;         // 一共有多少行格子&每行有多少个格子
  18.  
  19.         let canvas = document.getElementById('canvas');
  20.         canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  21.         canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  22.         let ctx = canvas.getContext('2d');
  23.  
  24.         let snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一条??
  25.  
  26.         drawSnack(ctx, snack, '#fff');
  27.  
  28.         function drawSnack(ctx, snack, color) {
  29.             ctx.fillStyle = color;
  30.             for (let i = 0; i < snack.length; i++) {
  31.                 ctx.fillRect(...getGridULCoordinate(snack[i]), GRID_WIDTH, GRID_WIDTH);
  32.                 if (i) {
  33.                     ctx.fillRect(...getBetweenTwoGridGap(snack[i], snack[- 1]));
  34.                 }
  35.             }
  36.         }
  37.         // 传入一个格子 返回左上角坐标
  38.         function getGridULCoordinate(g) {
  39.             return [g[0] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH, g[1] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH];
  40.         }
  41.         // 传入两个格子 返回两个格子之间的矩形缝隙
  42.         // 这里传入的两个格子必须是相邻的
  43.         // 返回一个数组 分别是这个矩形缝隙的 左上角横坐标 左上角纵坐标 宽 高
  44.         function getBetweenTwoGridGap(g1, g2) {
  45.             let width = GRID_WIDTH + GAP_WIDTH;
  46.             if (g1[0] === g2[0]) { // 横坐标相同 是纵向相邻的两个格子
  47.                 let x = g1[0] * width + GAP_WIDTH;
  48.                 let y = Math.min(g1[1], g2[1]) * width + width;
  49.                 return [x, y, GRID_WIDTH, GAP_WIDTH];
  50.             } else { // 纵坐标相同 是横向相邻的两个格子
  51.                 let x = Math.min(g1[0], g2[0]) * width + width;
  52.                 let y = g1[1] * width + GAP_WIDTH;
  53.                 return [x, y, GAP_WIDTH, GRID_WIDTH];
  54.             }
  55.         }
  56.     </script>
  57. </body>
  58. </html>

我初始化了一条蛇,看起来是符合预期的。

接下来要做的是让蛇动起来。蛇动起来这事很简单,蛇向着当前运动的方向前进一格,删掉蛇尾,也就是最后一个格子就可以了。之前说的二维数组表示一条蛇, 现在规定其中snack[0]表示蛇尾,snack[snack.length-1]表示蛇头。 动画就简单的用setInterval实现了。

  1. const GRID_WIDTH = 10;  // 格子的边长
  2. const GAP_WIDTH = 2;    // 空隙的边长
  3. const ROW = 10;         // 一共有多少行格子&每行有多少个格子
  4. const COLOR = '#fff';   // 蛇的颜色
  5. const BG_COLOR = '#000';// 背景颜色
  6.  
  7. const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3;    // 定义蛇前进的方向
  8. const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每个方向前进时格子坐标的变化
  9.  
  10. let canvas = document.getElementById('canvas');
  11. canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  12. canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  13. let ctx = canvas.getContext('2d');
  14.  
  15. let snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一条??
  16. let dir = RIGHT; // 初始化一个方向
  17.  
  18. drawSnack(ctx, snack, COLOR);
  19.  
  20. let timer = setInterval(() => {
  21.     // 每隔一段时间就刷新一次
  22.     let head = snack[snack.length - 1]; // 蛇头
  23.     let change = CHANGE[dir];           // 下一个格子前进位置
  24.     let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
  25.     snack.push(newGrid);    // 新格子加入蛇身的数组中
  26.     ctx.fillStyle = COLOR;
  27.     ctx.fillRect(...getGridULCoordinate(newGrid), GRID_WIDTH, GRID_WIDTH); // 画新格子
  28.     ctx.fillRect(...getBetweenTwoGridGap(head, newGrid)); // 新蛇头和旧蛇头之间的缝隙
  29.     ctx.fillStyle = BG_COLOR;
  30.     let delGrid = snack.shift();    // 删除蛇尾-最后一个元素
  31.     ctx.fillRect(...getGridULCoordinate(delGrid), GRID_WIDTH, GRID_WIDTH); // 擦除删除元素
  32.     ctx.fillRect(...getBetweenTwoGridGap(delGrid, snack[0])); // 擦除删除元素和当前最后一个元素之间的缝隙
  33. }, 1000);
  34.  
  35. ..... // 和之前相同

现在蛇已经可以动起来了。

但这肯定不是我想要的效果&mdash;&mdash;它的移动是一顿一顿的,而我想要顺滑的。

现在每一次变化都是直接移动一个格子边长的距离,保证蛇移动速度不变的情况下,动画是不可能变得顺滑的。所以想要移动变得顺滑,一种可行的方法是,移动一个格子的距离的过程分多次绘制。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>blog_snack</title>
  6.     <style>
  7.         #canvas {
  8.              background-color: #000;
  9.         }
  10.     </style>
  11. </head>
  12. <body>
  13.     <canvas id="canvas"></canvas>
  14.     <script>
  15.         const GRID_WIDTH = 10;  // 格子的边长
  16.         const GAP_WIDTH = 2;    // 空隙的边长
  17.         const ROW = 10;         // 一共有多少行格子&每行有多少个格子
  18.         const COLOR = '#fff';   // 蛇的颜色
  19.         const BG_COLOR = '#000';// 背景颜色
  20.         const INTERVAL = 1000;
  21.  
  22.         const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3;    // 定义蛇前进的方向
  23.         const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每个方向前进时格子坐标的变化
  24.  
  25.         let canvas = document.getElementById('canvas');
  26.         canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  27.         canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  28.         let ctx = canvas.getContext('2d');
  29.  
  30.         let snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一条??
  31.         let dir = RIGHT; // 初始化一个方向
  32.  
  33.         drawSnack(ctx, snack, COLOR);
  34.  
  35.         let timer = setInterval(() => {
  36.             // 每隔一段时间就刷新一次
  37.             let head = snack[snack.length - 1]; // 蛇头
  38.             let change = CHANGE[dir];           // 下一个格子前进位置
  39.             let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
  40.             snack.push(newGrid);    // 新格子加入蛇身的数组中
  41.             gradientRect(ctx, ...getUniteRect(newGrid, getBetweenTwoGridGap(head, newGrid)), dir, COLOR, INTERVAL);
  42.             let delGrid = snack.shift();    // 删除蛇尾-最后一个元素
  43.             gradientRect(ctx, ...getUniteRect(delGrid, getBetweenTwoGridGap(delGrid, snack[0])), 
  44.                 getDirection(delGrid, snack[0]), BG_COLOR, INTERVAL);
  45.         }, INTERVAL);
  46.  
  47.         // 给定一个格子的坐标和一个格子间隙的矩形(左上角,宽,高) 返回两个合并的矩形 的左上角、右下角 坐标
  48.         function getUniteRect(g, rect) {
  49.             let p = getGridULCoordinate(g);
  50.             if (p[0] === rect[0] && p[1] < rect[1] ||   // 矩形是在格子正下方
  51.                 p[1] === rect[1] && p[0] < rect[0]) {   // 矩形在格子的正右方
  52.                 return [p[0], p[1], rect[0] + rect[2], rect[1] + rect[3]];
  53.             } else if (p[0] === rect[0] && p[1] > rect[1] || // 矩形是在格子正上方
  54.                 p[1] === rect[1] && p[0] > rect[0]) { // 矩形在格子的正左方
  55.                 return [rect[0], rect[1], p[0] + GRID_WIDTH, p[1] + GRID_WIDTH];
  56.             }
  57.         }
  58.         // 从格子1 移动到格子2 的方向
  59.         function getDirection(g1, g2) {
  60.             if (g1[0] === g2[0] && g1[1] < g2[1]) return DOWN;
  61.             if (g1[0] === g2[0] && g1[1] > g2[1]) return UP;
  62.             if (g1[1] === g2[1] && g1[0] < g2[0]) return RIGHT;
  63.             if (g1[1] === g2[1] && g1[0] > g2[0]) return LEFT;
  64.         }
  65.  
  66.         // 慢慢的填充一个矩形 (真的不知道则怎么写 瞎写...动画的执行时间可能不等于duration 但一定要保证<=duration
  67.         // 传入的是矩形左上角和右下角的坐标 以及渐变的方向
  68.         function gradientRect(ctx, x1, y1, x2, y2, dir, color, duration) {
  69.             let dur = 20;
  70.             let times = Math.floor(duration / dur); // 更新次数
  71.             let nowX1 = x1, nowY1 = y1, nowX2 = x2, nowY2 = y2;
  72.             let dx1 = 0, dy1 = 0, dx2 = 0, dy2 = 0;
  73.             if (dir === UP) { dy1 = (y1 - y2) / times; nowY1 = y2; }
  74.             if (dir === DOWN) { dy2 = (y2 - y1) / times; nowY2 = y1; }
  75.             if (dir === LEFT) { dx1 = (x1 - x2) / times; nowX1 = x2; }
  76.             if (dir === RIGHT) { dx2 = (x2 - x1) / times; nowX2 = x1; }
  77.             let startTime = Date.now();
  78.             let timer = setInterval(() => {
  79.                 nowX1 += dx1, nowX2 += dx2, nowY1 += dy1, nowY2 += dy2; // 更新
  80.                 let runTime = Date.now() - startTime;
  81.                 if (nowX1 < x1 || nowX2 > x2 || nowY1 < y1 || nowY2 > y2 || runTime >= duration - dur) {
  82.                     nowX1 = x1, nowX2 = x2, nowY1 = y1, nowY2 = y2;
  83.                     clearInterval(timer);
  84.                 }
  85.                 ctx.fillStyle = color;
  86.                 ctx.fillRect(nowX1, nowY1, nowX2 - nowX1, nowY2 - nowY1);
  87.             }, dur);
  88.         }
  89.         // 根据snack二维数组画一条蛇
  90.         function drawSnack(ctx, snack, color) {
  91.             ctx.fillStyle = color;
  92.             for (let i = 0; i < snack.length; i++) {
  93.                 ctx.fillRect(...getGridULCoordinate(snack[i]), GRID_WIDTH, GRID_WIDTH);
  94.                 if (i) {
  95.                     ctx.fillRect(...getBetweenTwoGridGap(snack[i], snack[- 1]));
  96.                 }
  97.             }
  98.         }
  99.         // 传入一个格子 返回左上角坐标
  100.         function getGridULCoordinate(g) {
  101.             return [g[0] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH, g[1] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH];
  102.         }
  103.         // 传入两个格子 返回两个格子之间的矩形缝隙
  104.         // 这里传入的两个格子必须是相邻的
  105.         // 返回一个数组 分别是这个矩形缝隙的 左上角横坐标 左上角纵坐标 宽 高
  106.         function getBetweenTwoGridGap(g1, g2) {
  107.             let width = GRID_WIDTH + GAP_WIDTH;
  108.             if (g1[0] === g2[0]) { // 横坐标相同 是纵向相邻的两个格子
  109.                 let x = g1[0] * width + GAP_WIDTH;
  110.                 let y = Math.min(g1[1], g2[1]) * width + width;
  111.                 return [x, y, GRID_WIDTH, GAP_WIDTH];
  112.             } else { // 纵坐标相同 是横向相邻的两个格子
  113.                 let x = Math.min(g1[0], g2[0]) * width + width;
  114.                 let y = g1[1] * width + GAP_WIDTH;
  115.                 return [x, y, GAP_WIDTH, GRID_WIDTH];
  116.             }
  117.         }
  118.     </script>
  119. </body>
  120. </html>

实话,代码写的非常糟糕......我也很无奈......

反正现在蛇可以缓慢顺滑的移动了。

接下来要做的是判断是否触碰到边缘或者触碰到自身导致游戏结束,以及响应键盘事件。

这里的改动很简单。用一个map标记每一个格子是否被占。每一个格子(i, j)可以被编号i*row+j。

  1. const GRID_WIDTH = 10;  // 格子的边长
  2. const GAP_WIDTH = 2;    // 空隙的边长
  3. const ROW = 10;         // 一共有多少行格子&每行有多少个格子
  4. const COLOR = '#fff';   // 蛇的颜色
  5. const BG_COLOR = '#000';// 背景颜色
  6. const INTERVAL = 300;
  7.  
  8. const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3;    // 定义蛇前进的方向
  9. const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每个方向前进时格子坐标的变化
  10.  
  11. let canvas = document.getElementById('canvas');
  12. canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  13. canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  14. let ctx = canvas.getContext('2d');
  15.  
  16. let snack, dir, map, nextDir;
  17.  
  18. function initialize() {
  19.     snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一条??
  20.     nextDir = dir = RIGHT; // 初始化一个方向
  21.     map = [];
  22.     for (let i = 0; i < ROW * ROW; i++) map[i] = 0;
  23.     for (let i = 0; i < snack.length; i++) map[ getGridNumber(snack[i]) ] = 1;
  24.     window.onkeydown = function(e) {
  25.         // e.preventDefault();
  26.         if (e.key === 'ArrowUp') nextDir = UP;
  27.         if (e.key === 'ArrowDown') nextDir = DOWN;
  28.         if (e.key === 'ArrowRight') nextDir = RIGHT;
  29.         if (e.key === 'ArrowLeft') nextDir = LEFT;
  30.     }
  31.     drawSnack(ctx, snack, COLOR);
  32. }
  33.  
  34. initialize();
  35.  
  36. let timer = setInterval(() => {
  37.     // 每隔一段时间就刷新一次
  38.     // 只有转头方向与当前方向垂直的时候 才改变方向
  39.     if (nextDir !== dir && nextDir + dir !== 3) dir = nextDir;
  40.     let head = snack[snack.length - 1]; // 蛇头
  41.     let change = CHANGE[dir];           // 下一个格子前进位置
  42.     let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
  43.     if (!isValidPosition(newGrid)) { // 新位置不合法 游戏结束
  44.         clearInterval(timer);
  45.         return;
  46.     }
  47.     snack.push(newGrid);    // 新格子加入蛇身的数组中
  48.     map[getGridNumber(newGrid)] = 1;
  49.     gradientRect(ctx, ...getUniteRect(newGrid, getBetweenTwoGridGap(head, newGrid)), dir, COLOR, INTERVAL);
  50.     let delGrid = snack.shift();    // 删除蛇尾-最后一个元素
  51.     map[getGridNumber(delGrid)] = 0;
  52.     gradientRect(ctx, ...getUniteRect(delGrid, getBetweenTwoGridGap(delGrid, snack[0])), 
  53.         getDirection(delGrid, snack[0]), BG_COLOR, INTERVAL);
  54. }, INTERVAL);
  55.  
  56. function isValidPosition(g) {
  57.     if (g[0] >= 0 && g[0] < ROW && g[1] >= 0 && g[1] < ROW && !map[getGridNumber(g)]) return true;
  58.     return false;
  59. }
  60. // 获取一个格子的编号
  61. function getGridNumber(g) {
  62.     return g[0] * ROW + g[1];
  63. }
  64. // 给定一个格子的坐标和一个格子间隙的矩形(左上角,宽,高) 返回两个合并的矩形 的左上角、右下角 坐标
  65. function getUniteRect(g, rect) {
  66. /// ... 后面代码不改变 略....

这时已经可以控制蛇的移动了。

最后一个步骤了,画苹果。苹果的位置应该是随机的,且不与蛇身重叠,另外蛇吃到苹果的时候,长度会加一。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>blog_snack</title>
  6.     <style>
  7.         #canvas {
  8.              background-color: #000;
  9.         }
  10.     </style>
  11. </head>
  12. <body>
  13.     <canvas id="canvas"></canvas>
  14.     <script>
  15.         const GRID_WIDTH = 10;  // 格子的边长
  16.         const GAP_WIDTH = 2;    // 空隙的边长
  17.         const ROW = 10;         // 一共有多少行格子&每行有多少个格子
  18.         const COLOR = '#fff';   // 蛇的颜色
  19.         const BG_COLOR = '#000';// 背景颜色
  20.         const FOOD_COLOR = 'red'; // 食物颜色
  21.         const INTERVAL = 300;
  22.  
  23.         const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3;    // 定义蛇前进的方向
  24.         const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每个方向前进时格子坐标的变化
  25.  
  26.         let canvas = document.getElementById('canvas');
  27.         canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  28.         canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
  29.         let ctx = canvas.getContext('2d');
  30.  
  31.         let snack, dir, map, nextDir, food;
  32.  
  33.         function initialize() {
  34.             snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一条??
  35.             nextDir = dir = RIGHT; // 初始化一个方向
  36.             map = [];
  37.             for (let i = 0; i < ROW * ROW; i++) map[i] = 0;
  38.             for (let i = 0; i < snack.length; i++) map[ getGridNumber(snack[i]) ] = 1;
  39.             window.onkeydown = function(e) {
  40.                 // e.preventDefault();
  41.                 if (e.key === 'ArrowUp') nextDir = UP;
  42.                 if (e.key === 'ArrowDown') nextDir = DOWN;
  43.                 if (e.key === 'ArrowRight') nextDir = RIGHT;
  44.                 if (e.key === 'ArrowLeft') nextDir = LEFT;
  45.             }
  46.             drawSnack(ctx, snack, COLOR);
  47.             drawFood();
  48.         }
  49.  
  50.         initialize();
  51.  
  52.         let timer = setInterval(() => {
  53.             // 每隔一段时间就刷新一次
  54.             // 只有转头方向与当前方向垂直的时候 才改变方向
  55.             if (nextDir !== dir && nextDir + dir !== 3) dir = nextDir;
  56.             let head = snack[snack.length - 1]; // 蛇头
  57.             let change = CHANGE[dir];           // 下一个格子前进位置
  58.             let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
  59.             if (!isValidPosition(newGrid)) { // 新位置不合法 游戏结束
  60.                 clearInterval(timer);
  61.                 return;
  62.             }
  63.             snack.push(newGrid);    // 新格子加入蛇身的数组中
  64.             map[getGridNumber(newGrid)] = 1;
  65.             gradientRect(ctx, ...getUniteRect(newGrid, getBetweenTwoGridGap(head, newGrid)), dir, COLOR, INTERVAL);
  66.             if (newGrid[0] === food[0] && newGrid[1] === food[1]) {
  67.                 drawFood();
  68.                 return;
  69.             }
  70.             let delGrid = snack.shift();    // 删除蛇尾-最后一个元素
  71.             map[getGridNumber(delGrid)] = 0;
  72.             gradientRect(ctx, ...getUniteRect(delGrid, getBetweenTwoGridGap(delGrid, snack[0])), 
  73.                 getDirection(delGrid, snack[0]), BG_COLOR, INTERVAL);
  74.         }, INTERVAL);
  75.         // 画食物
  76.         function drawFood() {
  77.             food = getFoodPosition();
  78.             ctx.fillStyle = FOOD_COLOR;
  79.             ctx.fillRect(...getGridULCoordinate(food), GRID_WIDTH, GRID_WIDTH);
  80.         }
  81.         // 判断一个新生成的格子位置是否合法
  82.         function isValidPosition(g) {
  83.             if (g[0] >= 0 && g[0] < ROW && g[1] >= 0 && g[1] < ROW && !map[getGridNumber(g)]) return true;
  84.             return false;
  85.         }
  86.         // 获取一个格子的编号
  87.         function getGridNumber(g) {
  88.             return g[0] * ROW + g[1];
  89.         }
  90.         function getFoodPosition() {
  91.             let r = Math.floor(Math.random() * (ROW * ROW - snack.length)); // 随机获取一个数字 数字范围和剩余的格子数相同
  92.             for (let i = 0; ; i++) {    // 只有遇到空位的时候 计数君 r 才减一
  93.                 if (!map[i] && --< 0) return [Math.floor(/ ROW), i % ROW];
  94.             }
  95.         }
  96.         // 给定一个格子的坐标和一个格子间隙的矩形(左上角,宽,高) 返回两个合并的矩形 的左上角、右下角 坐标
  97.         function getUniteRect(g, rect) {
  98.             let p = getGridULCoordinate(g);
  99.             if (p[0] === rect[0] && p[1] < rect[1] ||   // 矩形是在格子正下方
  100.                 p[1] === rect[1] && p[0] < rect[0]) {   // 矩形在格子的正右方
  101.                 return [p[0], p[1], rect[0] + rect[2], rect[1] + rect[3]];
  102.             } else if (p[0] === rect[0] && p[1] > rect[1] || // 矩形是在格子正上方
  103.                 p[1] === rect[1] && p[0] > rect[0]) { // 矩形在格子的正左方
  104.                 return [rect[0], rect[1], p[0] + GRID_WIDTH, p[1] + GRID_WIDTH];
  105.             }
  106.         }
  107.         // 从格子1 移动到格子2 的方向
  108.         function getDirection(g1, g2) {
  109.             if (g1[0] === g2[0] && g1[1] < g2[1]) return DOWN;
  110.             if (g1[0] === g2[0] && g1[1] > g2[1]) return UP;
  111.             if (g1[1] === g2[1] && g1[0] < g2[0]) return RIGHT;
  112.             if (g1[1] === g2[1] && g1[0] > g2[0]) return LEFT;
  113.         }
  114.  
  115.         // 慢慢的填充一个矩形 (真的不知道则怎么写 瞎写...动画的执行时间可能不等于duration 但一定要保证<=duration
  116.         // 传入的是矩形左上角和右下角的坐标 以及渐变的方向
  117.         function gradientRect(ctx, x1, y1, x2, y2, dir, color, duration) {
  118.             let dur = 20;
  119.             let times = Math.floor(duration / dur); // 更新次数
  120.             let nowX1 = x1, nowY1 = y1, nowX2 = x2, nowY2 = y2;
  121.             let dx1 = 0, dy1 = 0, dx2 = 0, dy2 = 0;
  122.             if (dir === UP) { dy1 = (y1 - y2) / times; nowY1 = y2; }
  123.             if (dir === DOWN) { dy2 = (y2 - y1) / times; nowY2 = y1; }
  124.             if (dir === LEFT) { dx1 = (x1 - x2) / times; nowX1 = x2; }
  125.             if (dir === RIGHT) { dx2 = (x2 - x1) / times; nowX2 = x1; }
  126.             let startTime = Date.now();
  127.             let timer = setInterval(() => {
  128.                 nowX1 += dx1, nowX2 += dx2, nowY1 += dy1, nowY2 += dy2; // 更新
  129.                 let runTime = Date.now() - startTime;
  130.                 if (nowX1 < x1 || nowX2 > x2 || nowY1 < y1 || nowY2 > y2 || runTime >= duration - dur) {
  131.                     nowX1 = x1, nowX2 = x2, nowY1 = y1, nowY2 = y2;
  132.                     clearInterval(timer);
  133.                 }
  134.                 ctx.fillStyle = color;
  135.                 ctx.fillRect(nowX1, nowY1, nowX2 - nowX1, nowY2 - nowY1);
  136.             }, dur);
  137.         }
  138.         // 根据snack二维数组画一条蛇
  139.         function drawSnack(ctx, snack, color) {
  140.             ctx.fillStyle = color;
  141.             for (let i = 0; i < snack.length; i++) {
  142.                 ctx.fillRect(...getGridULCoordinate(snack[i]), GRID_WIDTH, GRID_WIDTH);
  143.                 if (i) {
  144.                     ctx.fillRect(...getBetweenTwoGridGap(snack[i], snack[- 1]));
  145.                 }
  146.             }
  147.         }
  148.         // 传入一个格子 返回左上角坐标
  149.         function getGridULCoordinate(g) {
  150.             return [g[0] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH, g[1] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH];
  151.         }
  152.         // 传入两个格子 返回两个格子之间的矩形缝隙
  153.         // 这里传入的两个格子必须是相邻的
  154.         // 返回一个数组 分别是这个矩形缝隙的 左上角横坐标 左上角纵坐标 宽 高
  155.         function getBetweenTwoGridGap(g1, g2) {
  156.             let width = GRID_WIDTH + GAP_WIDTH;
  157.             if (g1[0] === g2[0]) { // 横坐标相同 是纵向相邻的两个格子
  158.                 let x = g1[0] * width + GAP_WIDTH;
  159.                 let y = Math.min(g1[1], g2[1]) * width + width;
  160.                 return [x, y, GRID_WIDTH, GAP_WIDTH];
  161.             } else { // 纵坐标相同 是横向相邻的两个格子
  162.                 let x = Math.min(g1[0], g2[0]) * width + width;
  163.                 let y = g1[1] * width + GAP_WIDTH;
  164.                 return [x, y, GAP_WIDTH, GRID_WIDTH];
  165.             }
  166.         }
  167.     </script>
  168. </body>
  169. </html>

我不管 我写完了 我的代码最棒了(口区

如果蛇能自己动就好了。。。我的想法很单纯。。。但是想了很久没结果的时候,Google一下才发现这好像涉及到AI了。。。头疼。。。

最终我选取的方案是:

  1. if 存在蛇头到苹果的路径 and 蛇身长度小于整个地图的一半
  2.     虚拟蛇去尝试吃苹果
  3.     if 吃完苹果后能找到蛇头到蛇尾的路径
  4.         BFS到蛇尾
  5. else if 存在蛇头到蛇尾的路径
  6.     走蛇头到蛇尾的最长路径
  7. else
  8.     随机一个方向

我只是想练习Canvas而已...所以就没有好好写。代码有点长就不贴了。

(因为我的蛇很蠢。。是真的蠢。。。

完整代码可见github --> https://github.com/G-lory/front-end-practice/blob/master/canvas/blog_snack.html

这次写完感觉我的代码能力实在是太差了,写了两遍还是很乱。 以后还是要多练习。

反正没有bug是不可能的,这辈子是不可能的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号