likes
comments
collection
share

从0到1教你用canvas写一个贪吃蛇游戏

作者站长头像
站长
· 阅读数 11

每个前端都有一个游戏梦,每次看到一些游戏就想着能不能通过前端去做一些游戏方面的事情,但是游戏的门槛众所周知是比较高的,这篇文章就通过贪吃蛇来给大家简单讲述一下如何用canvas做一个游戏

先看效果,键盘的上下左右用来操控

从0到1教你用canvas写一个贪吃蛇游戏

那如何实现这么一个游戏呢?首先我们需要创建这样一个布局,一个画布

<div id="gameContainer">
  <canvas id="gameContent" width="500" height="500"></canvas>
  <div id="score">0</div>
  <button id="reset">重置</button>
</div>

样式和布局这里就不多说了,一个要游戏的画布,一个分数和一个重置按钮

整个游戏是建立在canvas上的

然后我们通过js来获取这几个dom元素

const gameContent = document.querySelector("#gameContent");

const scoreText = document.querySelector("#score");

const resetBtn = document.querySelector("#reset");

然后通过const ctx = gameContent.getContext("2d");获取到canvas的上下文,其中传了一个2d,表示contextType,可以传递以下几个参数

  • 2d:2d渲染上下文
  • webgl:三维渲染的上下文
  • bitmaprenderer:将canvas内容替换为指定ImageBitmap功能

因为这里贪吃蛇只需要用的2d的内容,所以传入2d即可

接下来需要定义一些常量,也就是一些配置,方便以后的使用

const {
  gameBackground,
  snakeColor,
  snakeBorder,
  foodColor,
  gameWidth,
  gameHeight,
  unitSize,
} = {
  // 背景颜色
  gameBackground: "white",
  // 蛇的颜色
  snakeColor: "lightgreen",
  // 蛇的边框颜色
  snakeBorder: "black",
  // 食物颜色
  foodColor: "red",
  // 画布宽度
  gameWidth: gameContent.width,
  // 画布高度
  gameHeight: gameContent.height,
  // 单位大小
  unitSize: 25,
};

开始之前,我们需要了解一个概念,就是坐标系,因为我们当前设置的画布的宽高是500,然后单位大小是25,是正好能够被整除的,可以建立一个以顶点为中心的坐标系

从0到1教你用canvas写一个贪吃蛇游戏

这是我们简历的画布坐标系,x轴0-500,向右为正,y轴为0-500,向下为正

有了这个坐标系,我们来开始写代码,首先创建一个随机产生食物的方法,定义一个foodPos表示食物基于坐标系的位置,通过x、y就能够确认食物的位置,所以要有一个产生随机坐标的函数,实现一下

const foodPos = {
  x: null,
  y: null,
};

function createFood() {
  function randomFood(min, max) {
    const randNum =
      Math.round((Math.random() * (max - min) + min) / unitSize) * unitSize;
    return randNum;
  }
  foodPos.x = randomFood(0, gameWidth - unitSize);
  foodPos.y = randomFood(0, gameHeight - unitSize);
}

randNum这个数据要注意,因为我们的基础单位是以25为基准,所以我们要除以unitSize取到一个整数,然后再乘以unitSize得到25的整数倍,这样就能够在地图上随机产生了一个坐标,有了这个食物的坐标,我们来绘制食物

绘制需要有一点canvas的知识,先看代码

function drawFood() {
  ctx.fillStyle = foodColor;
  ctx.fillRect(foodPos.x, foodPos.y, unitSize, unitSize);
}

ctx就是我们刚开始的canvas实例,通过fillStyle改变画笔颜色,然后fillRect可以画一个方形,前两个参数是坐标,后两个参数是绘制的方形的宽高,然后调用一下createFood和drawFood,发现已经能够正常在canvas上绘制出来了,并且坐标是25的倍数

从0到1教你用canvas写一个贪吃蛇游戏

接下来创建蛇的数据以及对蛇的绘制,先给一个初始化的数据,初始的蛇有5个方块,使用数据来保存这5个方块的位置信息

let snake = [
  {
    x: unitSize * 4,
    y: 0,
  },
  {
    x: unitSize * 3,
    y: 0,
  },
  {
    x: unitSize * 2,
    y: 0,
  },
  {
    x: unitSize,
    y: 0,
  },
  {
    x: 0,
    y: 0,
  },
];

然后将这5个方块绘制到画布上

function drawSnake() {
  ctx.fillStyle = snakeColor;
  ctx.strokeStyle = snakeBorder;
  snake.forEach((snakePart) => {
    ctx.fillRect(snakePart.x, snakePart.y, unitSize, unitSize);
    ctx.strokeRect(snakePart.x, snakePart.y, unitSize, unitSize);
  });
}

这次我们新增了一个strokeRect的绘制,其实就是css中的border,同样执行一下drawSnake,可以看到蛇已经被绘制出来了

从0到1教你用canvas写一个贪吃蛇游戏

如何让蛇移动呢?移动的原理也比较简单,因为当前蛇是一个数组,所以我们只需要每帧在数组unshift添加一个数据,同时删除数组尾部的数据即可,数组头部的数据就是蛇的第一结数据加上其对应的移动方向,所以这里我们先定义一个xVelocaity和yVelocaity表示在x轴和y轴上的移动速度,默认开始的移动速度是在x轴移动

let [xVelocaity, yVelocaity] = [unitSize, 0];

function moveSnake() {
  const head = {
    x: snake[0].x + xVelocaity,
    y: snake[0].y + yVelocaity,
  };
  snake.unshift(head);
  snake.pop();
}

同时这里要新增一个判断,如果碰到了食物,就不需要去pop了,并且新创建一个food

function moveSnake() {
  const head = {
    x: snake[0].x + xVelocaity,
    y: snake[0].y + yVelocaity,
  };
  snake.unshift(head);
  // 检查是否被吃掉
  if (snake[0].x === foodPos.x && snake[0].y === foodPos.y) {
    createFood();
  } else {
    snake.pop();
  }
}

然后我们创建一个nextTick函数,每次调用一下moveSnake让蛇动起来,数据改变了之后记得绘制

function nextTick() {
  setTimeout(() => {
    moveSnake();
    drawSnake();
    nextTick();
  }, 80);
}

nextTick()

从0到1教你用canvas写一个贪吃蛇游戏

动是动起来了,但是由于每次没有清除画布,导致画布内容不断叠加,所以我们给一个清除画布的方法

每次nextTick的时候先清除画布

function clearContent() {
  ctx.fillStyle = gameBackground;
  ctx.fillRect(0, 0, gameWidth, gameHeight);
}

现在蛇的运动没有问题了,接下来我们要做的就是键盘操作蛇的移动,在全局监听键盘的事件,采用window.addEventListener("keydown", changeDirection);,changeDirection就是我们要实现的方法

这个方法有一个event参数,里面有键盘按下的详细信息,通过keyCode可以拿到键盘具体按下的是哪个按键,比如上下左右四个按键分别对应的是38、40、37、39,先看代码

function changeDirection(event) {
  const keyPressed = event.keyCode;
  const LEFT = 37;
  const RIGHT = 39;
  const UP = 38;
  const DOWN = 40;

  const goingUp = yVelocaity == -unitSize;
  const goingDown = yVelocaity == unitSize;
  const goingRight = xVelocaity == unitSize;
  const goingLeft = xVelocaity == -unitSize;

  switch (true) {
    case keyPressed == LEFT && !goingRight:
      xVelocaity = -unitSize;
      yVelocaity = 0;
      break;
    case keyPressed == UP && !goingDown:
      xVelocaity = 0;
      yVelocaity = -unitSize;
      break;
    case keyPressed == RIGHT && !goingLeft:
      xVelocaity = unitSize;
      yVelocaity = 0;
      break;
    case keyPressed == DOWN && !goingUp:
      xVelocaity = 0;
      yVelocaity = unitSize;
      break;
  }
}

比较简单,通过当前的xVelocaity和yVelocaity来判断当前蛇移动的方向,然后判断按键按下的内容结合当前蛇运动的方向来改变蛇x y方位的速度,然后每次蛇运动的时候就会根据xVelocaity和yVelocaity来进行移动

移动没问题了,下面就是边界碰撞检测,因为当前蛇运动是可以超出屏幕的,这肯定是不行的,所以建立一个checkGameOver的方法

并在全局给一个变量running,表明游戏是否在进行中,只需要检测蛇的头部是否超出即可,然后再检测蛇的头部有没有跟身体各个部位有碰撞,有的话就结束游戏,将running设置为false

let running = false
function checkGameOver() {
  switch (true) {
    case snake[0].x < 0:
      running = false;
      break;
    case snake[0].x >= gameWidth:
      running = false;
      break;
    case snake[0].y < 0:
      running = false;
      break;
    case snake[0].y >= gameHeight:
      running = false;
      break;
  }
  for (let i = 1; i < snake.length; i += 1) {
    if (snake[i].x == snake[0].x && snake[i].y == snake[0].y) {
      running = false;
    }
  }
}

再来一个游戏结束时的展示game over的样式

function displayGameOver() {
  ctx.font = "50px MV Boli";
  ctx.fillStyle = "black";
  ctx.textAlign = "center";
  ctx.fillText("GAME OVER!", gameWidth / 2, gameHeight / 2);
  running = false;
}

改造一下nextTick

function nextTick() {
  if (running) {
    setTimeout(() => {
      clearContent();
      drawFood();
      moveSnake();
      drawSnake();
      checkGameOver();
      nextTick();
    }, 80);
  } else {
    displayGameOver();
  }
}

最后再将整个代码整合,绑定重置按钮的click事件,编写一个gameStart以及resetGame的方法,就是将一些状态初始化就可以了,附上完整的js代码,dom和css部分自行完善

这样贪吃蛇游戏就完成了

从0到1教你用canvas写一个贪吃蛇游戏

const gameContent = document.querySelector("#gameContent");
const ctx = gameContent.getContext("2d");

const scoreText = document.querySelector("#score");

const resetBtn = document.querySelector("#reset");

const {
  gameBackground,
  snakeColor,
  snakeBorder,
  foodColor,
  gameWidth,
  gameHeight,
  unitSize,
} = {
  gameBackground: "white",
  snakeColor: "lightgreen",
  snakeBorder: "black",
  foodColor: "red",
  gameWidth: gameContent.width,
  gameHeight: gameContent.height,
  unitSize: 25,
};

let [xVelocaity, yVelocaity] = [unitSize, 0];

const foodPos = {
  x: null,
  y: null,
};

let score = 0;
let running = false;

let snake = [
  {
    x: unitSize * 4,
    y: 0,
  },
  {
    x: unitSize * 3,
    y: 0,
  },
  {
    x: unitSize * 2,
    y: 0,
  },
  {
    x: unitSize,
    y: 0,
  },
  {
    x: 0,
    y: 0,
  },
];

window.addEventListener("keydown", changeDirection);
resetBtn.addEventListener("click", resetGame);
gameStart();
function gameStart() {
  running = true;
  scoreText.textContent = score;
  createFood();
  drawFood();
  drawSnake();
  nextTick();
}

function nextTick() {
  if (running) {
    setTimeout(() => {
      clearContent();
      drawFood();
      moveSnake();
      drawSnake();
      checkGameOver();
      nextTick();
    }, 80);
  } else {
    displayGameOver();
  }
}

function clearContent() {
  ctx.fillStyle = gameBackground;
  ctx.fillRect(0, 0, gameWidth, gameHeight);
}

function createFood() {
  function randomFood(min, max) {
    const randNum =
      Math.round((Math.random() * (max - min) + min) / unitSize) * unitSize;
    return randNum;
  }
  foodPos.x = randomFood(0, gameWidth - unitSize);
  foodPos.y = randomFood(0, gameHeight - unitSize);
}

function drawFood() {
  ctx.fillStyle = foodColor;
  ctx.fillRect(foodPos.x, foodPos.y, unitSize, unitSize);
}

function moveSnake() {
  const head = {
    x: snake[0].x + xVelocaity,
    y: snake[0].y + yVelocaity,
  };
  snake.unshift(head);
  // 检查是否被吃掉
  if (snake[0].x === foodPos.x && snake[0].y === foodPos.y) {
    score += 1;
    scoreText.textContent = score;
    createFood();
  } else {
    snake.pop();
  }
}

function drawSnake() {
  ctx.fillStyle = snakeColor;
  ctx.strokeStyle = snakeBorder;
  snake.forEach((snakePart) => {
    ctx.fillRect(snakePart.x, snakePart.y, unitSize, unitSize);
    ctx.strokeRect(snakePart.x, snakePart.y, unitSize, unitSize);
  });
}

function changeDirection(event) {
  const keyPressed = event.keyCode;
  const LEFT = 37;
  const RIGHT = 39;
  const UP = 38;
  const DOWN = 40;

  const goingUp = yVelocaity == -unitSize;
  const goingDown = yVelocaity == unitSize;
  const goingRight = xVelocaity == unitSize;
  const goingLeft = xVelocaity == -unitSize;

  switch (true) {
    case keyPressed == LEFT && !goingRight:
      xVelocaity = -unitSize;
      yVelocaity = 0;
      break;
    case keyPressed == UP && !goingDown:
      xVelocaity = 0;
      yVelocaity = -unitSize;
      break;
    case keyPressed == RIGHT && !goingLeft:
      xVelocaity = unitSize;
      yVelocaity = 0;
      break;
    case keyPressed == DOWN && !goingUp:
      xVelocaity = 0;
      yVelocaity = unitSize;
      break;
  }
}

function checkGameOver() {
  switch (true) {
    case snake[0].x < 0:
      running = false;
      break;
    case snake[0].x >= gameWidth:
      running = false;
      break;
    case snake[0].y < 0:
      running = false;
      break;
    case snake[0].y >= gameHeight:
      running = false;
      break;
  }
  for (let i = 1; i < snake.length; i += 1) {
    if (snake[i].x == snake[0].x && snake[i].y == snake[0].y) {
      running = false;
    }
  }
}

function displayGameOver() {
  ctx.font = "50px MV Boli";
  ctx.fillStyle = "black";
  ctx.textAlign = "center";
  ctx.fillText("GAME OVER!", gameWidth / 2, gameHeight / 2);
  running = false;
}

function resetGame() {
  score = 0;
  xVelocaity = unitSize;
  yVelocaity = 0;
  snake = [
    { x: unitSize * 4, y: 0 },
    { x: unitSize * 3, y: 0 },
    { x: unitSize * 2, y: 0 },
    { x: unitSize, y: 0 },
    { x: 0, y: 0 },
  ];
  gameStart();
}