从0到1教你用canvas写一个贪吃蛇游戏
每个前端都有一个游戏梦,每次看到一些游戏就想着能不能通过前端去做一些游戏方面的事情,但是游戏的门槛众所周知是比较高的,这篇文章就通过贪吃蛇来给大家简单讲述一下如何用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,是正好能够被整除的,可以建立一个以顶点为中心的坐标系
这是我们简历的画布坐标系,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的倍数
接下来创建蛇的数据以及对蛇的绘制,先给一个初始化的数据,初始的蛇有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,可以看到蛇已经被绘制出来了
如何让蛇移动呢?移动的原理也比较简单,因为当前蛇是一个数组,所以我们只需要每帧在数组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()
动是动起来了,但是由于每次没有清除画布,导致画布内容不断叠加,所以我们给一个清除画布的方法
每次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部分自行完善
这样贪吃蛇游戏就完成了
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();
}
转载自:https://juejin.cn/post/7205127600271786041