前两天我们用canvas制作了一个简单的五子棋小游戏,今天我再用canvas制作一个贪吃蛇小游戏。
试玩点这里:贪吃蛇
老规矩,先看看界面效果:
屏幕大致分成两个板块:上面的菜单板块和下面的游戏板块。
菜单板块是一些基本的html+css代码以及少量的js代码组成,我这里不多讲解,直接给出代码,看着代码也应该很容易理解。
html代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GreedSnake</title>
<link rel="stylesheet" href="snake.css">
</head>
<body>
<header>
<h1>贪吃蛇</h1>
<a href="" id="newgamebutton">New Game</a>
<a href="javascript:void(0)" id="pausegamebutton">Pause Game</a>
<a href="javascript:void(0)" id="gamehelpbutton">Game Help</a>
<p>score:<span id="score">0</span></p>
</head>
<canvas width="600" height="600" id="canvas"></canvas>
<script src="snake.js"></script>
</body>
</html>
css代码:
header {
display: block;
margin: 0 auto;
width: 100%;
text-align: center
}
header h1 {
font-family: Arial;
font-size: 40px;
font-weight: bold;
}
header a {
display: block;
margin: 20px auto;
width: 100px;
padding: 10px;
font-family: Arial;
color: white;
border-radius: 10px;
text-decoration: none;
}
#newgamebutton {
background-color: #f14343;
}
header #newgamebutton:hover {
background: #c90707;
}
#pausegamebutton {
background-color: rgb(57, 190, 243);
}
header #pausegamebutton:hover {
background: rgb(8, 168, 231);
}
#gamehelpbutton {
background-color: rgb(221, 224, 26);
}
header #gamehelpbutton:hover {
background: rgb(198, 201, 40);
}
header p {
font-family: Arial;
font-size: 25px;
margin: 20px auto;
}
canvas {
display: block;
margin: 30px auto;
background-color: #33cc99;
}
部分的js代码:
pause.addEventListener('click', function (e) {
alert("暂停游戏!");
});
help.addEventListener('click', function (e) {
alert("此游戏支持电脑键盘操作并有得分系统\n" +
"wsad分别控制蛇上下左右移动\n" +
"蛇的移速是动态的与蛇的长度成正比\n" +
"地图总共有三种功能的食物,具体功能有待你们自己去探索\n" +
"Have a Great Game!");
});
进入最重要的游戏版本界面。
游戏制作可以先粗分为三部分:绘制地图,绘制食物与蛇,制定游戏规则。
绘制地图:
这里的操作和五子棋棋盘的操作大同小异,也都是固定格式,我直接给出代码。
for (var i = 1; i < 20; i++) {
//+0.5是为了绘制出1px宽度的线条
tools.moveTo(30 * i + 0.5, 0);
tools.lineTo(30 * i + 0.5, 600);
tools.moveTo(0, 30 * i + 0.5);
tools.lineTo(600, 30 * i + 0.5);
}
tools.strokeStyle = 'white';
tools.stroke();
绘制食物:
简单来看,就是绘制一个矩形小方块,再赋予其随机算法即可,但是在游戏里我设定了三种不同的食物,所以我这里暂值给出简单的绘制矩形的代码和随机算法的代码,具体情况留到制定游戏规则板块。
tools.fillStyle = 'rgb(252, 101, 101)';
tools.fillRect(xRed, yRed, 30, 30);
var xRed = Math.floor(Math.random() * 20) * 30;
var yRed = Math.floor(Math.random() * 20) * 30;
绘制蛇:
简单来看,在地图里是蛇,但是实际上是用数组来保存,所以我们真实操作的也是这个数组。
for (var i = 0; i < snake.length; i++) {
if (i == 0) {
tools.fillStyle = '#ff0033';
} else {
tools.fillStyle = '#333399';
}
tools.fillRect(snake[i].x * 30, snake[i].y * 30, 30, 30);
}
制定游戏规则:
由于这个板块基本都牵连到全局,所以此部分我就无法单独给出其代码。这里我只讲述每个核心部分的实现思路,并在文章最后给出完整的js代码供大家参考(代码里都有详细的注释)。
这个板块主要又可细分为以下几个部分:
蛇的移动,蛇吃食物,蛇的移动方向,游戏结束判定。(基本功能)
动态移速,得分系统,减蛇长食物。(扩展功能)
蛇的移动:
蛇的移动其实是一种动画效果,而动画的原理就是两步:擦除+重绘。
说白了就是删除掉旧状态的画布,然后绘制出新状态的画布,而这里就需要用到js定时器。
蛇吃食物:
其实蛇吃食物也是人视觉的一个错觉,由于蛇的底层是一个数组,我们可以设置蛇的每一次运动都有出栈和“入栈”的操作来保持蛇长度的平衡,而在蛇遇到食物时取消出栈的操作,这些就能使得蛇的长度+1。
蛇的移动方向:
通过绑定键盘监听事件来实现,注意特别判定“蛇换头”的bug。
游戏结束判定:
就两种情况:蛇碰到自己的身体以及边界。(这里都可以通过坐标来处理)
动态移速:
通过修改定时器的时间间隔,使得其与蛇的长度成正比。
得分系统:
定义一个全局变量score,在每次蛇吃到食物后score给予相应的积累改变,并将结果呈现在页面。
减蛇长食物:
这个功能的设计想法是当蛇长达到一定长度后能够通过这个食物减短蛇一定的长度(减低移速)来平衡游戏难度,而当蛇的长度减小到一定长度限制后,此类食物会消失。
我采取的实现思路稍稍有些复杂,这里就不给出,可以直接参考代码。如果有更好实现思路的大佬,欢迎评论区留言。
完整的js代码:
var canvas = document.getElementById("canvas");
var scoreId = document.getElementById("score");
var pause = document.getElementById("pausegamebutton");
var help = document.getElementById("gamehelpbutton");
var tools = canvas.getContext("2d");
var xRed = Math.floor(Math.random() * 20) * 30;
var yRed = Math.floor(Math.random() * 20) * 30;
var xGold = Math.floor(Math.random() * 20) * 30;
var yGold = Math.floor(Math.random() * 20) * 30;
var xBlue = 0;
var yBlue = 0;
var isEatRed = false;
var isEatGold = false;
var isEatBlue = false;
var startEatBlue = false;
var startFlag = true;
var endFlag = true;
var endEatBlue = false;
var liveBlue = false;
var snake = [
{ x: 3, y: 0 },
{ x: 2, y: 0 },
{ x: 1, y: 0 }
];
var score = 0;
var directionX = 1;
var directionY = 0;
//监听键盘事件
document.addEventListener('keydown', function (e) {
//设定蛇的运行方向不能180度调头
if (e.keyCode == 87 && (directionX != 0 || directionY != 1)) {
directionX = 0;
directionY = -1;
} else if (e.keyCode == 83 && (directionX != 0 || directionY != -1)) {
directionX = 0;
directionY = 1;
} else if (e.keyCode == 65 && (directionX != 1 || directionY != 0)) {
directionX = -1;
directionY = 0;
} else if (e.keyCode == 68 && (directionX != -1 || directionY != 0)) {
directionX = 1;
directionY = 0;
}
});
pause.addEventListener('click', function (e) {
alert("暂停游戏!");
});
help.addEventListener('click', function (e) {
alert("此游戏支持电脑键盘操作并有得分系统\n" +
"wsad分别控制蛇上下左右移动\n" +
"蛇的移速是动态的与蛇的长度成正比\n" +
"地图总共有三种功能的食物,具体功能有待你们自己去探索\n" +
"Have a Great Game!");
});
//设定游戏默认定时器(蛇的运行速度)
var game = setInterval(start, 1000 / 3);
//下面三个函数都是产生一个新的合法食物
function newEatRed(snake) {
var xRed = Math.floor(Math.random() * 20) * 30;
var yRed = Math.floor(Math.random() * 20) * 30;
while (true) {
var flag = false;
for (var i = 0; i < snake.length; i++) {
if (snake[i].x * 30 == xRed && snake[i].y * 30 == yRed) {
flag = true;
break;
}
}
if (flag) {
xRed = Math.floor(Math.random() * 20) * 30;
yRed = Math.floor(Math.random() * 20) * 30;
} else {
break;
}
}
return {
x: xRed,
y: yRed
}
}
function newEatGold(snake, xRed, yRed) {
var xGold = Math.floor(Math.random() * 20) * 30;
var yGold = Math.floor(Math.random() * 20) * 30;
while (true) {
var flag = false;
for (var i = 0; i < snake.length; i++) {
if (snake[i].x * 30 == xRed && snake[i].y * 30 == yRed) {
flag = true;
break;
}
}
if (xGold == xRed && yGold == yRed) {
flag = true;
}
if (flag) {
xGold = Math.floor(Math.random() * 20) * 30;
yGold = Math.floor(Math.random() * 20) * 30;
} else {
break;
}
}
return {
x: xGold,
y: yGold
}
}
function newEatBlue(snake, xRed, yRed, xGold, yGold) {
var xBlue = Math.floor(Math.random() * 20) * 30;
var yBlue = Math.floor(Math.random() * 20) * 30;
while (true) {
var flag = false;
for (var i = 0; i < snake.length; i++) {
if (snake[i].x * 30 == xRed && snake[i].y * 30 == yRed) {
flag = true;
break;
}
}
if ((xBlue == xRed && yBlue == yRed) || (xBlue == xGold && yBlue == yGold)) {
flag = true;
}
if (flag) {
xBlue = Math.floor(Math.random() * 20) * 30;
xBlue = Math.floor(Math.random() * 20) * 30;
} else {
break;
}
}
return {
x: xBlue,
y: yBlue
}
}
//判断两种游戏结束条件
function judge(snake, newHead) {
if (newHead.x < 0 || newHead.x * 30 >= 600 || newHead.y * 30 < 0 || newHead.y * 30 >= 600) {
alert("游戏结束!");
clearInterval(game);
return true;
}
for (var i = 1; i < snake.length; i++) {
if (newHead.x == snake[i].x && newHead.y == snake[i].y) {
alert("游戏结束!");
clearInterval(game);
return true;
}
}
return false;
}
//绑定定时器的函数(核心函数)
function start() {
//擦除旧的canvas
tools.clearRect(0, 0, 600, 600);
var oldHead = snake[0];
var newHead = {
x: oldHead.x + directionX,
y: oldHead.y + directionY
}
//判断结束条件
var isFailed = judge(snake, newHead);
if(isFailed) {
return;
}
//蛇吃食物
if (snake[0].x * 30 == xRed && snake[0].y * 30 == yRed) {
score++;
scoreId.innerText = score;
isEatRed = true;
} else if (snake[0].x * 30 == xGold && snake[0].y * 30 == yGold) {
score = score + 2;
scoreId.innerText = score;
isEatGold = true;
} else {
isEatRed = false;
isEatGold = false;
snake.pop();
}
snake.unshift(newHead);
if (snake[0].x * 30 == xBlue && snake[0].y * 30 == yBlue) {
isEatBlue = true;
snake.pop();
} else {
isEatBlue = false;
}
//蛇的动态移速(无限递归)
clearInterval(game);
game = setInterval(start, 1000 / (3 + (snake.length - 1) / 5));
//下面两个判断蓝色食物代码必须放在蛇吃食物的下面,否则会出现蓝色食物出现与消失慢拍的bug
//出现蓝色食物
if (snake.length > 25 && startFlag) {
startEatBlue = true;
startFlag = false;
endEatBlue = false;
endFlag = true;
}
//取消蓝色食物
if (snake.length < 16 && endFlag) {
endEatBlue = true;
endFlag = false;
startEatBlue = false;
startFlag = true;
}
//产生红色食物与金色食物
if (isEatRed) {
var arrRed = newEatRed(snake);
xRed = arrRed.x;
yRed = arrRed.y;
}
tools.fillStyle = 'rgb(252, 101, 101)';
tools.fillRect(xRed, yRed, 30, 30);
if (isEatGold) {
var arrGold = newEatGold(snake, xRed, yRed);
xGold = arrGold.x;
yGold = arrGold.y;
}
tools.fillStyle = 'gold';
tools.fillRect(xGold, yGold, 30, 30);
if (endEatBlue) {
if (liveBlue) {
liveBlue = false;
}
}
//第一次产生蓝色食物
if (startEatBlue) {
if (!liveBlue) {
var arrBlue = newEatBlue(snake, xRed, yRed, xGold, yGold);
xBlue = arrBlue.x;
yBlue = arrBlue.y;
liveBlue = true;
}
tools.fillStyle = 'rgb(89, 195, 245)';
tools.fillRect(xBlue, yBlue, 30, 30);
}
//正常产生蓝色食物
if (startEatBlue && isEatBlue) {
var arrBlue = newEatBlue(snake, xRed, yRed, xGold, yGold);
xBlue = arrBlue.x;
yBlue = arrBlue.y;
tools.fillStyle = 'rgb(89, 195, 245)';
tools.fillRect(xBlue, yBlue, 30, 30);
}
//绘制蛇
for (var i = 0; i < snake.length; i++) {
if (i == 0) {
tools.fillStyle = '#ff0033';
} else {
tools.fillStyle = '#333399';
}
tools.fillRect(snake[i].x * 30, snake[i].y * 30, 30, 30);
}
//绘制地图
for (var i = 1; i < 20; i++) {
//+0.5是为了绘制出1px宽度的线条
tools.moveTo(30 * i + 0.5, 0);
tools.lineTo(30 * i + 0.5, 600);
tools.moveTo(0, 30 * i + 0.5);
tools.lineTo(600, 30 * i + 0.5);
}
tools.strokeStyle = 'white';
tools.stroke();
}
最后补充一点:start函数里面的代码执行顺序是很有讲究的,不可随意调换!
如果大家有兴趣,可以继续扩展游戏玩法,比如再设定一条蛇使其变成一个双人游戏等等。