首页 > 网页制作 > html5

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

admin html5 2022-02-06 13:36:30 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) 。

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

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

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




    
    blog_snack
    


    
    

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

接下来要做的是让蛇动起来。蛇动起来这事很简单,蛇向着当前运动的方向前进一格,删掉蛇尾,也就是最后一个格子就可以了。之前说的二维数组表示一条蛇, 现在规定其中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);

..... // 和之前相同

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

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

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




    
    blog_snack
    


    
    

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

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

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

这里的改动很简单。用一个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) {
/// ... 后面代码不改变 略....

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

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




    
    blog_snack
    


    
    

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

如果蛇能自己动就好了。。。我的想法很单纯。。。但是想了很久没结果的时候,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是不可能的,这辈子是不可能的。

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

版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。
本文地址:/web/html5/76881.html

留言与评论(共有 0 条评论)
   
验证码:

潘少俊衡

| 桂ICP备2023010378号-4

Powered By EmpireCMS

爱享小站

中德益农

谷姐神农

环亚肥料

使用手机软件扫描微信二维码

关注我们可获取更多热点资讯

感谢潘少俊衡友情技术支持