首页 > 作文

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

更新时间:2023-04-06 20:04:09 阅读: 评论:0

之前在慕课网看了几集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 chart="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];英语不会怎么办            } el { // 纵坐标相同 是横向相邻的两个格子                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]表示蛇头。 动画就简单的用tinterval实现了。

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 = tinterval(() => {    // 每隔一段时间就刷新一次    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 chart="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 = tinterval(() => {            // 每隔一段时间就刷新一次            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]];            } el 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, durat应用电子技术专业ion) {            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 = tinterval(() => {                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];            } el { // 纵坐标相同 是横向相邻的两个格子                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 = tinterval(() => {    // 每隔一段时间就刷新一次    // 只有转头方向与当前方向垂直的时候 才改变方向    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 fal;}// 获取一个格子的编号function getgridnumber(g) {    return g[0] * row + g[1];}// 给定一个格子的坐标和一个格子间隙的矩形(左上角,宽,高) 返回两个合并的矩形 的左上角、右下角 坐标function getuniterect(g, rect) {/// ... 后面代码不改变 略....

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

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

<!doctype html><html lang="en"><head><meta chart="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 = tinterval(() => {// 每隔一段时间就刷新一次// 只有转头方向与当前方向垂直的时候 才改变方向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 fal;}// 获取一个格子的编号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]];} el 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 = tinterval(() => {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];} el { // 纵坐标相同 是横向相邻的两个格子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到蛇尾el if 存在蛇头到蛇尾的路径走蛇头到蛇尾的最长路径el随机一个方向

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

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

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

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

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

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

本文发布于:2023-04-06 20:03:04,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/zuowen/d7225212befe8e71deaea66033d733ce.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

本文word下载地址:教你如何一步一步用Canvas写一个贪吃蛇.doc

本文 PDF 下载地址:教你如何一步一步用Canvas写一个贪吃蛇.pdf

标签:格子   矩形   左上角   方向
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图