300行代码实现canvas贪吃蛇

前言

上周末写了篇canvas实现贪吃蛇,今天再来个贪吃蛇!

技术选型

我们的贪吃蛇游戏将由Canvas渲染。下面是我们需要完成的步骤:

  1. 创建画布(Canvas)和游戏区域
  2. 绘制蛇和食物
  3. 监听键盘事件,以控制蛇的移动
  4. 判断输赢(当蛇吃到食物时,蛇会变长,游戏继续;当蛇碰到自身或边界时,游戏结束)

实现

第一步:创建画布和游戏区域

html 复制代码
<!-- html只需提供一个canvas标签 -->
<canvas id="canvas"></canvas>
js 复制代码
class Snake {
  constructor(canvas, { width = 400, height = 400 } = {}) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.width = width;
    this.height = height;
    // 格子宽度
    this.gap = 10; 
    // 初始化
    this.init();
  }
  init() {
    const { canvas, width, height } = this;
    canvas.width = width;
    canvas.height = height; 
    // 画格子
    this.drawGrid();
  }
  // 画格子
  drawGrid() {
    const { ctx, width, height, gap } = this;
    // 青绿色背景
    ctx.fillStyle = "#96c93d";
    ctx.fillRect(0, 0, width, height);
    // 白色线条
    ctx.lineWidth = 0.5;
    ctx.strokeStyle = "#fff";
    for (let i = 0; i < width; i += gap) {
      ctx.beginPath();
      ctx.moveTo(i, 0);
      ctx.lineTo(i, height);
      ctx.stroke();
      ctx.closePath();
    }
    for (let i = 0; i < height; i += gap) {
      ctx.beginPath();
      ctx.moveTo(0, i);
      ctx.lineTo(width, i);
      ctx.stroke();
      ctx.closePath();
    }
  }
}

此时效果如下

第二步:绘制蛇和食物

要先生成蛇和食物的坐标,默认蛇的位置是固定的,食物的位置是随机的,改写一下init方法

js 复制代码
init() {
  const { canvas, width, height } = this;
  canvas.width = width;
  canvas.height = height;
  // 画格子
  this.drawGrid();
  // 初始化蛇的坐标,位置固定
  this.snake = [
    { x: 10, y: 100 },
    { x: 20, y: 100 },
    { x: 30, y: 100 },
    { x: 40, y: 100 },
    { x: 50, y: 100 },
  ];
  // 初始化食物
  this.food = this.randomFood();
  // 绘制蛇
  this.drawSnake();
  // 绘制食物
  this.drawFood();
}

写个方法随机生成食物坐标,后面还要用。

js 复制代码
randomFood() {
  const { width, height } = this;
  // 生成0-最大宽度之间的10倍的整数
  const x = Math.floor((Math.random() * width) / 10) * 10;
  const y = Math.floor((Math.random() * height) / 10) * 10;

  return { x, y };
}
js 复制代码
// 绘制蛇
drawSnake() {
  const { ctx, snake, gap, width, height } = this;
  snake.forEach((item, index) => {
    // 蛇头部画个红色
    // 身体画黑色
    if (index === snake.length - 1) {
      ctx.fillStyle = "red";
    } else {
      ctx.fillStyle = "black";
    }
    ctx.fillRect(item.x + 1, item.y + 1, gap - 1, gap - 1);
  });
}
// 绘制食物
drawFood() {
  const { ctx, food, gap } = this;
  const { x, y } = food;
  ctx.fillStyle = "#035c03";
  ctx.fillRect(x, y, gap, gap);
}

此时效果如下

第三步:控制蛇的移动

每隔多长时间蛇前进一步,即,尾部减少一格,头部增加一格,增加的这一格的坐标应该怎么计算?这个要**「根据蛇当前的移动方向来计算」**。

实现核心的运动函数,在init中调用一下

js 复制代码
init() {
  // ... 前面的省略了
  this.move()
}
js 复制代码
move() {
  const { width, height, ctx, snake, gap } = this;
  // 每200ms更新一次蛇的坐标,
  // 并重新渲染Canvas,🐍就动起来了
  setInterval(() => {
    // 更新蛇的坐标
    // 获取增加的头部节点坐标
    // 因为默认往右的,头部其实是在数组的最后一项
    let { x, y } = snake.at(-1);
    // 头部x轴增加一格,y轴不变
    x += gap;
    // 尾巴去掉一格
    snake.shift();
    // 头部新增一格
    snake.push({ x, y });
    // 清空画布
    ctx.clearRect(0, 0, width, height);
    // 重新画格子
    this.drawGrid();
    // 重新画蛇
    this.drawSnake();
  }, 200);
}

这里更新了蛇的坐标,重新调用ctx.clearRect清空蛇尾,重新调用ctx.fillRect画蛇头,这样应该会更好,就不用清空整个画布重新绘制了。先实现功能吧~

此时效果如下(这里gif帧率过低了,跟实际效果有区别)

但是只实现了往一个方向移动,并且可以看到蛇跑到游戏区域外了,下面来解决这个问题。

定义一个方向变量,并且绑定键盘事件来改变方向。

js 复制代码
init() {
  // ... 前面的省略了
  // 方向,默认往右
  this.direction = "right"; 
  // 绑定操作
  this.bindEvent();
}
bindEvent() {
  // 键盘上的方向按键
  const keys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
  document.addEventListener("keydown", (event) => {
    // 规定只有按方向按键才起作用
    if (!keys.includes(event.key)) return
    // 不允许往反方向移动
    if (event.key === "ArrowUp" && this.direction !== "bottom") {
      this.direction = "top";
    } else if (event.key === "ArrowDown" && this.direction !== "top") {
      this.direction = "bottom";
    } else if (event.key === "ArrowLeft" && this.direction !== "right") {
      this.direction = "left";
    } else if (event.key === "ArrowRight" && this.direction !== "left") {
      this.direction = "right";
    }
    
    // 改变方向后要重新调用一下move函数
    this.move()
  });
}

move函数也要改写一下,增加根据方向控制运动的代码:

js 复制代码
move() {
  const { width, height, ctx, snake } = this;
  // 因为要多次调用,每次调用前要清空定时器
  clearInterval(this.timer);
  this.timer = setInterval(() => {
    // 更新蛇的坐标
    // 获取增加的节点坐标
    const { x, y } = this.updateSnake()
    // 尾巴去掉一格
    snake.shift();
    // 头部新增一格
    snake.push({ x, y });
    // 清空画布
    ctx.clearRect(0, 0, width, height);
    // 画格子
    this.drawGrid();
    // 重新画蛇
    this.drawSnake();
  }, 200);
}
js 复制代码
updateSnake() {
  const { snake, direction, gap } = this
  let { x, y } = snake.at(-1);
  switch (direction) {
    case "right":
      x += gap;
      break;
    case "left":
      x -= gap;
      break;
    case "top":
      y -= gap;
      break;
    case "bottom":
      y += gap;
      break;
    default:
      break;
  }
  return { x, y }
}

现在就可以用方向键控制拐弯了

第四步:判断输赢

  • 蛇吃到食物的判断:蛇的头部坐标和食物坐标重合,即吃到了食物,在蛇的尾部增加一格
  • 蛇撞到边界或自身的判断:蛇的头部和四周边缘坐标重合、或者和自己的身体某一部分坐标重合,即游戏结束

改写move函数,判断是否吃到食物,重新绘制食物

js 复制代码
move() {
  const { width, height, ctx, snake } = this;
  // 因为要多次调用,每次调用前要清空定时器
  clearInterval(this.timer);
  this.timer = setInterval(() => {
    // 更新蛇的坐标
    // 获取增加的节点坐标
    const { x, y } = this.updateSnake()
    // 吃的动作,把最新的头部坐标传进去
    this.handleEat(x, y)
    // 游戏结束
    if (this.isHitWall({ x, y }) || this.isEatSelf({ x, y })) {
      console.log('游戏结束')
      clearInterval(this.timer);
      return
    }
    // 尾巴去掉一格
    snake.shift();
    // 头部新增一格
    snake.push({ x, y });
    // 清空画布
    ctx.clearRect(0, 0, width, height);
    // 画格子
    this.drawGrid();
    // 重新画蛇
    this.drawSnake();
    // 重新绘制食物
    this.drawFood();
  }, 200);
}
// 吃
handleEat(x, y) {
  const { snake, direction, gap, ctx } = this
  if (x === this.food.x && y === this.food.y) {
    // 吃到了食物
    let { x, y } = snake[0];
    switch (direction) {
      case "right":
        x -= gap;
        break;
      case "left":
        x += gap;
        break;
      case "top":
        y += gap;
        break;
      case "bottom":
        y -= gap;
        break;
      default:
        break;
    }
    // 尾部增加一节
    snake.unshift({ x, y });
    // 清空食物
    ctx.clearRect(this.food.x, this.food.y, gap, gap);
    // 重新生成食物坐标
    this.food = this.randomFood();
  }
}

判断是否撞到边界或自身

js 复制代码
// 判断是否撞到边界
isHitWall(head) {
  const { width, height } = this;
  // x轴大于最大宽度或小于最小宽度
  // y轴大于最大高度或小于最小高度
  return head.x >= width || head.x < 0 || head.y >= height || head.y < 0
}

// 判断是否吃到了自己
isEatSelf(head) {
  const { snake } = this
  // 头部不可能和头部重合,只判断身体
  const body = snake.slice(1);
  // 头部和身体任何一个节点重合
  return body.some(item => item.x === head.x && item.y === head.y);
}

到这里基本功能就完成了,还可以加入计分逻辑手动控制开始暂停游戏兼容移动端等功能。

在线体验最新效果

github源码

相关推荐
德育处主任1 天前
p5.js 3D模型(model)入门指南
前端·前端框架·canvas
VincentFHR2 天前
Three.js 利用 shader 实现 3D 热力图
前端·three.js·canvas
德育处主任3 天前
p5.js 加载 3D 模型(loadModel)
前端·数据可视化·canvas
用户2519162427114 天前
Canvas之颜色渐变
前端·javascript·canvas
德育处主任4 天前
p5.js 从零开始创建 3D 模型,createModel入门指南
前端·数据可视化·canvas
德育处主任4 天前
p5.js 三角形triangle的用法
前端·数据可视化·canvas
德育处主任5 天前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
德育处主任5 天前
p5.js 矩形rect绘制教程
前端·数据可视化·canvas
用户2519162427115 天前
Canvas之图像合成
前端·javascript·canvas
敲敲敲敲暴你脑袋6 天前
Cesium绘制3D热力山丘图
数据可视化·canvas·cesium