之前在慕课网看了几集 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) .
假设现在蛇身是由三个蓝色的格子组成的, 我们不能只绘制三个格子, 两个紫色的空隙也一定要绘制, 否则, 还是之前说的, 你根本不知道蛇身什么样. 如下图, 不画缝隙虽然也能玩, 但是体验肯定不一样.
绘制相邻格子之间间隙 不绘制间隙
现在我们可以尝试着画一条蛇了. 蛇身其实就是一个格子的集合, 每个格子用包含两个位置信息的数组表示, 整条蛇可以用二维数组表示.
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <title>
- blog_snack
- </title>
- <style>
- #canvas { background-color: #000; }
- </style>
- </head>
- <body>
- <canvas id="canvas">
- </canvas>
- <script>
- const GRID_WIDTH = 10; // 格子的边长
- const GAP_WIDTH = 2; // 空隙的边长
- const ROW = 10; // 一共有多少行格子 & 每行有多少个格子
- let canvas = document.getElementById('canvas');
- canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- let ctx = canvas.getContext('2d');
- let snack = [[2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5]]; // 初始化一条
- drawSnack(ctx, snack, '#fff');
- function drawSnack(ctx, snack, color) {
- ctx.fillStyle = color;
- for (let i = 0; i < snack.length; i++) {
- ctx.fillRect(...getGridULCoordinate(snack[i]), GRID_WIDTH, GRID_WIDTH);
- if (i) {
- ctx.fillRect(...getBetweenTwoGridGap(snack[i], snack[i - 1]));
- }
- }
- }
- // 传入一个格子 返回左上角坐标
- function getGridULCoordinate(g) {
- return [g[0] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH, g[1] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH];
- }
- // 传入两个格子 返回两个格子之间的矩形缝隙
- // 这里传入的两个格子必须是相邻的
- // 返回一个数组 分别是这个矩形缝隙的 左上角横坐标 左上角纵坐标 宽 高
- function getBetweenTwoGridGap(g1, g2) {
- let width = GRID_WIDTH + GAP_WIDTH;
- if (g1[0] === g2[0]) { // 横坐标相同 是纵向相邻的两个格子
- let x = g1[0] * width + GAP_WIDTH;
- let y = Math.min(g1[1], g2[1]) * width + width;
- return [x, y, GRID_WIDTH, GAP_WIDTH];
- } else { // 纵坐标相同 是横向相邻的两个格子
- let x = Math.min(g1[0], g2[0]) * width + width;
- let y = g1[1] * width + GAP_WIDTH;
- return [x, y, GAP_WIDTH, GRID_WIDTH];
- }
- }
- </script>
- </body>
- </HTML>
我初始化了一条蛇, 看起来是符合预期的.
接下来要做的是让蛇动起来. 蛇动起来这事很简单, 蛇向着当前运动的方向前进一格, 删掉蛇尾, 也就是最后一个格子就可以了. 之前说的二维数组表示一条蛇, 现在规定其中 snack[0]表示蛇尾, snack[snack.length-1]表示蛇头. 动画就简单的用 setInterval 实现了.
- const GRID_WIDTH = 10; // 格子的边长
- const GAP_WIDTH = 2; // 空隙的边长
- const ROW = 10; // 一共有多少行格子 & 每行有多少个格子
- const COLOR = '#fff'; // 蛇的颜色
- const BG_COLOR = '#000';// 背景颜色
- const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3; // 定义蛇前进的方向
- const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每个方向前进时格子坐标的变化
- let canvas = document.getElementById('canvas');
- canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- let ctx = canvas.getContext('2d');
- let snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一条
- let dir = RIGHT; // 初始化一个方向
- drawSnack(ctx, snack, COLOR);
- let timer = setInterval(() => {
- // 每隔一段时间就刷新一次
- let head = snack[snack.length - 1]; // 蛇头
- let change = CHANGE[dir]; // 下一个格子前进位置
- let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
- snack.push(newGrid); // 新格子加入蛇身的数组中
- ctx.fillStyle = COLOR;
- ctx.fillRect(...getGridULCoordinate(newGrid), GRID_WIDTH, GRID_WIDTH); // 画新格子
- ctx.fillRect(...getBetweenTwoGridGap(head, newGrid)); // 新蛇头和旧蛇头之间的缝隙
- ctx.fillStyle = BG_COLOR;
- let delGrid = snack.shift(); // 删除蛇尾 - 最后一个元素
- ctx.fillRect(...getGridULCoordinate(delGrid), GRID_WIDTH, GRID_WIDTH); // 擦除删除元素
- ctx.fillRect(...getBetweenTwoGridGap(delGrid, snack[0])); // 擦除删除元素和当前最后一个元素之间的缝隙
- }, 1000);
- ..... // 和之前相同
现在蛇已经可以动起来了.
但这肯定不是我想要的效果 -- 它的移动是一顿一顿的, 而我想要顺滑的.
现在每一次变化都是直接移动一个格子边长的距离, 保证蛇移动速度不变的情况下, 动画是不可能变得顺滑的. 所以想要移动变得顺滑, 一种可行的方法是, 移动一个格子的距离的过程分多次绘制.
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <title>
- blog_snack
- </title>
- <style>
- #canvas { background-color: #000; }
- </style>
- </head>
- <body>
- <canvas id="canvas">
- </canvas>
- <script>
- const GRID_WIDTH = 10; // 格子的边长
- const GAP_WIDTH = 2; // 空隙的边长
- const ROW = 10; // 一共有多少行格子 & 每行有多少个格子
- const COLOR = '#fff'; // 蛇的颜色
- const BG_COLOR = '#000'; // 背景颜色
- const INTERVAL = 1000;
- const UP = 0,
- LEFT = 1,
- RIGHT = 2,
- DOWN = 3; // 定义蛇前进的方向
- const CHANGE = [[0, -1], [ - 1, 0], [1, 0], [0, 1]]; // 每个方向前进时格子坐标的变化
- let canvas = document.getElementById('canvas');
- canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- let ctx = canvas.getContext('2d');
- let snack = [[2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5]]; // 初始化一条
- let dir = RIGHT; // 初始化一个方向
- drawSnack(ctx, snack, COLOR);
- let timer = setInterval(() = >{
- // 每隔一段时间就刷新一次
- let head = snack[snack.length - 1]; // 蛇头
- let change = CHANGE[dir]; // 下一个格子前进位置
- let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
- snack.push(newGrid); // 新格子加入蛇身的数组中
- gradientRect(ctx, ...getUniteRect(newGrid, getBetweenTwoGridGap(head, newGrid)), dir, COLOR, INTERVAL);
- let delGrid = snack.shift(); // 删除蛇尾 - 最后一个元素
- gradientRect(ctx, ...getUniteRect(delGrid, getBetweenTwoGridGap(delGrid, snack[0])), getDirection(delGrid, snack[0]), BG_COLOR, INTERVAL);
- },
- INTERVAL);
- // 给定一个格子的坐标和一个格子间隙的矩形(左上角, 宽, 高) 返回两个合并的矩形 的左上角, 右下角 坐标
- function getUniteRect(g, rect) {
- let p = getGridULCoordinate(g);
- if (p[0] === rect[0] && p[1] < rect[1] || // 矩形是在格子正下方
- p[1] === rect[1] && p[0] < rect[0]) { // 矩形在格子的正右方
- return [p[0], p[1], rect[0] + rect[2], rect[1] + rect[3]];
- } else if (p[0] === rect[0] && p[1] > rect[1] || // 矩形是在格子正上方
- p[1] === rect[1] && p[0] > rect[0]) { // 矩形在格子的正左方
- return [rect[0], rect[1], p[0] + GRID_WIDTH, p[1] + GRID_WIDTH];
- }
- }
- // 从格子 1 移动到格子 2 的方向
- function getDirection(g1, g2) {
- if (g1[0] === g2[0] && g1[1] < g2[1]) return DOWN;
- if (g1[0] === g2[0] && g1[1] > g2[1]) return UP;
- if (g1[1] === g2[1] && g1[0] < g2[0]) return RIGHT;
- if (g1[1] === g2[1] && g1[0] > g2[0]) return LEFT;
- }
- // 慢慢的填充一个矩形 (真的不知道则怎么写 瞎写... 动画的执行时间可能不等于 duration 但一定要保证<=duration
- // 传入的是矩形左上角和右下角的坐标 以及渐变的方向
- function gradientRect(ctx, x1, y1, x2, y2, dir, color, duration) {
- let dur = 20;
- let times = Math.floor(duration / dur); // 更新次数
- let nowX1 = x1,
- nowY1 = y1,
- nowX2 = x2,
- nowY2 = y2;
- let dx1 = 0,
- dy1 = 0,
- dx2 = 0,
- dy2 = 0;
- if (dir === UP) {
- dy1 = (y1 - y2) / times;
- nowY1 = y2;
- }
- if (dir === DOWN) {
- dy2 = (y2 - y1) / times;
- nowY2 = y1;
- }
- if (dir === LEFT) {
- dx1 = (x1 - x2) / times;
- nowX1 = x2;
- }
- if (dir === RIGHT) {
- dx2 = (x2 - x1) / times;
- nowX2 = x1;
- }
- let startTime = Date.now();
- let timer = setInterval(() = >{
- nowX1 += dx1,
- nowX2 += dx2,
- nowY1 += dy1,
- nowY2 += dy2; // 更新
- let runTime = Date.now() - startTime;
- if (nowX1 < x1 || nowX2 > x2 || nowY1 < y1 || nowY2 > y2 || runTime >= duration - dur) {
- nowX1 = x1,
- nowX2 = x2,
- nowY1 = y1,
- nowY2 = y2;
- clearInterval(timer);
- }
- ctx.fillStyle = color;
- ctx.fillRect(nowX1, nowY1, nowX2 - nowX1, nowY2 - nowY1);
- },
- dur);
- }
- // 根据 snack 二维数组画一条蛇
- function drawSnack(ctx, snack, color) {
- ctx.fillStyle = color;
- for (let i = 0; i < snack.length; i++) {
- ctx.fillRect(...getGridULCoordinate(snack[i]), GRID_WIDTH, GRID_WIDTH);
- if (i) {
- ctx.fillRect(...getBetweenTwoGridGap(snack[i], snack[i - 1]));
- }
- }
- }
- // 传入一个格子 返回左上角坐标
- function getGridULCoordinate(g) {
- return [g[0] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH, g[1] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH];
- }
- // 传入两个格子 返回两个格子之间的矩形缝隙
- // 这里传入的两个格子必须是相邻的
- // 返回一个数组 分别是这个矩形缝隙的 左上角横坐标 左上角纵坐标 宽 高
- function getBetweenTwoGridGap(g1, g2) {
- let width = GRID_WIDTH + GAP_WIDTH;
- if (g1[0] === g2[0]) { // 横坐标相同 是纵向相邻的两个格子
- let x = g1[0] * width + GAP_WIDTH;
- let y = Math.min(g1[1], g2[1]) * width + width;
- return [x, y, GRID_WIDTH, GAP_WIDTH];
- } else { // 纵坐标相同 是横向相邻的两个格子
- let x = Math.min(g1[0], g2[0]) * width + width;
- let y = g1[1] * width + GAP_WIDTH;
- return [x, y, GAP_WIDTH, GRID_WIDTH];
- }
- }
- </script>
- </body>
- </HTML>
实话, 代码写的非常糟糕...... 我也很无奈......
反正现在蛇可以缓慢顺滑的移动了.
接下来要做的是判断是否触碰到边缘或者触碰到自身导致游戏结束, 以及响应键盘事件.
这里的改动很简单. 用一个 map 标记每一个格子是否被占. 每一个格子 (i, j) 可以被编号 i*row+j.
- const GRID_WIDTH = 10; // 格子的边长
- const GAP_WIDTH = 2; // 空隙的边长
- const ROW = 10; // 一共有多少行格子 & 每行有多少个格子
- const COLOR = '#fff'; // 蛇的颜色
- const BG_COLOR = '#000';// 背景颜色
- const INTERVAL = 300;
- const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3; // 定义蛇前进的方向
- const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每个方向前进时格子坐标的变化
- let canvas = document.getElementById('canvas');
- canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- let ctx = canvas.getContext('2d');
- let snack, dir, map, nextDir;
- function initialize() {
- snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一条
- nextDir = dir = RIGHT; // 初始化一个方向
- map = [];
- for (let i = 0; i <ROW * ROW; i++) map[i] = 0;
- for (let i = 0; i < snack.length; i++) map[ getGridNumber(snack[i]) ] = 1;
- Windows.onkeydown = function(e) {
- // e.preventDefault();
- if (e.key === 'ArrowUp') nextDir = UP;
- if (e.key === 'ArrowDown') nextDir = DOWN;
- if (e.key === 'ArrowRight') nextDir = RIGHT;
- if (e.key === 'ArrowLeft') nextDir = LEFT;
- }
- drawSnack(ctx, snack, COLOR);
- }
- initialize();
- let timer = setInterval(() => {
- // 每隔一段时间就刷新一次
- // 只有转头方向与当前方向垂直的时候 才改变方向
- if (nextDir !== dir && nextDir + dir !== 3) dir = nextDir;
- let head = snack[snack.length - 1]; // 蛇头
- let change = CHANGE[dir]; // 下一个格子前进位置
- let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
- if (!isValidPosition(newGrid)) { // 新位置不合法 游戏结束
- clearInterval(timer);
- return;
- }
- snack.push(newGrid); // 新格子加入蛇身的数组中
- map[getGridNumber(newGrid)] = 1;
- gradientRect(ctx, ...getUniteRect(newGrid, getBetweenTwoGridGap(head, newGrid)), dir, COLOR, INTERVAL);
- let delGrid = snack.shift(); // 删除蛇尾 - 最后一个元素
- map[getGridNumber(delGrid)] = 0;
- gradientRect(ctx, ...getUniteRect(delGrid, getBetweenTwoGridGap(delGrid, snack[0])),
- getDirection(delGrid, snack[0]), BG_COLOR, INTERVAL);
- }, INTERVAL);
- function isValidPosition(g) {
- if (g[0]>= 0 && g[0] <ROW && g[1]>= 0 && g[1] <ROW && !map[getGridNumber(g)]) return true;
- return false;
- }
- // 获取一个格子的编号
- function getGridNumber(g) {
- return g[0] * ROW + g[1];
- }
- // 给定一个格子的坐标和一个格子间隙的矩形(左上角, 宽, 高) 返回两个合并的矩形 的左上角, 右下角 坐标
- function getUniteRect(g, rect) {
- /// ... 后面代码不改变 略....
这时已经可以控制蛇的移动了.
最后一个步骤了, 画苹果. 苹果的位置应该是随机的, 且不与蛇身重叠, 另外蛇吃到苹果的时候, 长度会加一.
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <title>
- blog_snack
- </title>
- <style>
- #canvas { background-color: #000; }
- </style>
- </head>
- <body>
- <canvas id="canvas">
- </canvas>
- <script>
- const GRID_WIDTH = 10; // 格子的边长
- const GAP_WIDTH = 2; // 空隙的边长
- const ROW = 10; // 一共有多少行格子 & 每行有多少个格子
- const COLOR = '#fff'; // 蛇的颜色
- const BG_COLOR = '#000'; // 背景颜色
- const FOOD_COLOR = 'red'; // 食物颜色
- const INTERVAL = 300;
- const UP = 0,
- LEFT = 1,
- RIGHT = 2,
- DOWN = 3; // 定义蛇前进的方向
- const CHANGE = [[0, -1], [ - 1, 0], [1, 0], [0, 1]]; // 每个方向前进时格子坐标的变化
- let canvas = document.getElementById('canvas');
- canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
- let ctx = canvas.getContext('2d');
- let snack,
- dir,
- map,
- nextDir,
- food;
- function initialize() {
- snack = [[2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5]]; // 初始化一条
- nextDir = dir = RIGHT; // 初始化一个方向
- map = [];
- for (let i = 0; i < ROW * ROW; i++) map[i] = 0;
- for (let i = 0; i < snack.length; i++) map[getGridNumber(snack[i])] = 1;
- Windows.onkeydown = function(e) {
- // e.preventDefault();
- if (e.key === 'ArrowUp') nextDir = UP;
- if (e.key === 'ArrowDown') nextDir = DOWN;
- if (e.key === 'ArrowRight') nextDir = RIGHT;
- if (e.key === 'ArrowLeft') nextDir = LEFT;
- }
- drawSnack(ctx, snack, COLOR);
- drawFood();
- }
- initialize();
- let timer = setInterval(() = >{
- // 每隔一段时间就刷新一次
- // 只有转头方向与当前方向垂直的时候 才改变方向
- if (nextDir !== dir && nextDir + dir !== 3) dir = nextDir;
- let head = snack[snack.length - 1]; // 蛇头
- let change = CHANGE[dir]; // 下一个格子前进位置
- let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
- if (!isValidPosition(newGrid)) { // 新位置不合法 游戏结束
- clearInterval(timer);
- return;
- }
- snack.push(newGrid); // 新格子加入蛇身的数组中
- map[getGridNumber(newGrid)] = 1;
- gradientRect(ctx, ...getUniteRect(newGrid, getBetweenTwoGridGap(head, newGrid)), dir, COLOR, INTERVAL);
- if (newGrid[0] === food[0] && newGrid[1] === food[1]) {
- drawFood();
- return;
- }
- let delGrid = snack.shift(); // 删除蛇尾 - 最后一个元素
- map[getGridNumber(delGrid)] = 0;
- gradientRect(ctx, ...getUniteRect(delGrid, getBetweenTwoGridGap(delGrid, snack[0])), getDirection(delGrid, snack[0]), BG_COLOR, INTERVAL);
- },
- INTERVAL);
- // 画食物
- function drawFood() {
- food = getFoodPosition();
- ctx.fillStyle = FOOD_COLOR;
- ctx.fillRect(...getGridULCoordinate(food), GRID_WIDTH, GRID_WIDTH);
- }
- // 判断一个新生成的格子位置是否合法
- function isValidPosition(g) {
- if (g[0] >= 0 && g[0] < ROW && g[1] >= 0 && g[1] < ROW && !map[getGridNumber(g)]) return true;
- return false;
- }
- // 获取一个格子的编号
- function getGridNumber(g) {
- return g[0] * ROW + g[1];
- }
- function getFoodPosition() {
- let r = Math.floor(Math.random() * (ROW * ROW - snack.length)); // 随机获取一个数字 数字范围和剩余的格子数相同
- for (let i = 0;; i++) { // 只有遇到空位的时候 计数君 r 才减一
- if (!map[i] && --r < 0) return [Math.floor(i / ROW), i % ROW];
- }
- }
- // 给定一个格子的坐标和一个格子间隙的矩形(左上角, 宽, 高) 返回两个合并的矩形 的左上角, 右下角 坐标
- function getUniteRect(g, rect) {
- let p = getGridULCoordinate(g);
- if (p[0] === rect[0] && p[1] < rect[1] || // 矩形是在格子正下方
- p[1] === rect[1] && p[0] < rect[0]) { // 矩形在格子的正右方
- return [p[0], p[1], rect[0] + rect[2], rect[1] + rect[3]];
- } else if (p[0] === rect[0] && p[1] > rect[1] || // 矩形是在格子正上方
- p[1] === rect[1] && p[0] > rect[0]) { // 矩形在格子的正左方
- return [rect[0], rect[1], p[0] + GRID_WIDTH, p[1] + GRID_WIDTH];
- }
- }
- // 从格子 1 移动到格子 2 的方向
- function getDirection(g1, g2) {
- if (g1[0] === g2[0] && g1[1] < g2[1]) return DOWN;
- if (g1[0] === g2[0] && g1[1] > g2[1]) return UP;
- if (g1[1] === g2[1] && g1[0] < g2[0]) return RIGHT;
- if (g1[1] === g2[1] && g1[0] > g2[0]) return LEFT;
- }
- // 慢慢的填充一个矩形 (真的不知道则怎么写 瞎写... 动画的执行时间可能不等于 duration 但一定要保证<=duration
- // 传入的是矩形左上角和右下角的坐标 以及渐变的方向
- function gradientRect(ctx, x1, y1, x2, y2, dir, color, duration) {
- let dur = 20;
- let times = Math.floor(duration / dur); // 更新次数
- let nowX1 = x1,
- nowY1 = y1,
- nowX2 = x2,
- nowY2 = y2;
- let dx1 = 0,
- dy1 = 0,
- dx2 = 0,
- dy2 = 0;
- if (dir === UP) {
- dy1 = (y1 - y2) / times;
- nowY1 = y2;
- }
- if (dir === DOWN) {
- dy2 = (y2 - y1) / times;
- nowY2 = y1;
- }
- if (dir === LEFT) {
- dx1 = (x1 - x2) / times;
- nowX1 = x2;
- }
- if (dir === RIGHT) {
- dx2 = (x2 - x1) / times;
- nowX2 = x1;
- }
- let startTime = Date.now();
- let timer = setInterval(() = >{
- nowX1 += dx1,
- nowX2 += dx2,
- nowY1 += dy1,
- nowY2 += dy2; // 更新
- let runTime = Date.now() - startTime;
- if (nowX1 < x1 || nowX2 > x2 || nowY1 < y1 || nowY2 > y2 || runTime >= duration - dur) {
- nowX1 = x1,
- nowX2 = x2,
- nowY1 = y1,
- nowY2 = y2;
- clearInterval(timer);
- }
- ctx.fillStyle = color;
- ctx.fillRect(nowX1, nowY1, nowX2 - nowX1, nowY2 - nowY1);
- },
- dur);
- }
- // 根据 snack 二维数组画一条蛇
- function drawSnack(ctx, snack, color) {
- ctx.fillStyle = color;
- for (let i = 0; i < snack.length; i++) {
- ctx.fillRect(...getGridULCoordinate(snack[i]), GRID_WIDTH, GRID_WIDTH);
- if (i) {
- ctx.fillRect(...getBetweenTwoGridGap(snack[i], snack[i - 1]));
- }
- }
- }
- // 传入一个格子 返回左上角坐标
- function getGridULCoordinate(g) {
- return [g[0] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH, g[1] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH];
- }
- // 传入两个格子 返回两个格子之间的矩形缝隙
- // 这里传入的两个格子必须是相邻的
- // 返回一个数组 分别是这个矩形缝隙的 左上角横坐标 左上角纵坐标 宽 高
- function getBetweenTwoGridGap(g1, g2) {
- let width = GRID_WIDTH + GAP_WIDTH;
- if (g1[0] === g2[0]) { // 横坐标相同 是纵向相邻的两个格子
- let x = g1[0] * width + GAP_WIDTH;
- let y = Math.min(g1[1], g2[1]) * width + width;
- return [x, y, GRID_WIDTH, GAP_WIDTH];
- } else { // 纵坐标相同 是横向相邻的两个格子
- let x = Math.min(g1[0], g2[0]) * width + width;
- let y = g1[1] * width + GAP_WIDTH;
- return [x, y, GAP_WIDTH, GRID_WIDTH];
- }
- }
- </script>
- </body>
- </HTML>
我不管 我写完了 我的代码最棒了(口区
如果蛇能自己动就好了... 我的想法很单纯... 但是想了很久没结果的时候, Google 一下才发现这好像涉及到 AI 了... 头疼...
随便看了几个文章, 部分思想是相同的.(http://www.waitingfy.com/archives/951,https://github.com/chuyangliu/Snake)
最终我选取的方案是:
if 存在蛇头到苹果的路径 and 蛇身长度小于整个地图的一半
虚拟蛇去尝试吃苹果
if 吃完苹果后能找到蛇头到蛇尾的路径
BFS 到蛇尾
else if 存在蛇头到蛇尾的路径
走蛇头到蛇尾的最长路径
else
随机一个方向
我只是想练习 Canvas 而已... 所以就没有好好写. 代码有点长就不贴了.
(因为我的蛇很蠢.. 是真的蠢...
完整代码可见 GitHub --> https://github.com/G-lory/front-end-practice/blob/master/canvas/blog_snack.html
这次写完感觉我的代码能力实在是太差了, 写了两遍还是很乱. 以后还是要多练习.
反正没有 bug 是不可能的, 这辈子是不可能的.
来源: https://www.cnblogs.com/wenruo/p/9813231.html