• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    教你如何一步一步用Canvas写一个贪吃蛇

    之前在慕课网看了几集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;
        window.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;
                window.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了。。。头疼。。。

    最终我选取的方案是:

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

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

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

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

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

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

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

    上一篇:html5/css3响应式页面开发总结
    下一篇:如何使用localstorage代替cookie实现跨域共享数据问题
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯 版权所有

    《增值电信业务经营许可证》 苏ICP备15040257号-8

    教你如何一步一步用Canvas写一个贪吃蛇 教你,如何,一步,用,Canvas,