0%

制作贪吃蛇

设计

直接步入正题,由于游戏本身游玩以及操作比较简单,因此这次的游戏界面会有点简陋,实在是不知道该加啥东西了😿

游戏主体部分是宽为650px,高为450px,而小蛇的每一格身体都是25 * 25的,因此整个游戏界面有18行,26列。

这一次我会尽量把每一个功能的代码都记录下来,一是因为这个贪吃蛇代码肯定比俄罗斯方块要少,二是因为,不写代码的话,我这篇博客已经结束了,这也太短了吧喂!

代码

HTML代码如下,布局都是采用position:absolute的方式,因为部件比较少,这样代码也不会很繁琐,而且很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>贪吃蛇</title>
<script src="./snake.js"></script>
<style>
div{
border: #000000 3.5px solid;
}

.container{
height: 570px;
width: 707px;
position: relative;
margin: 0px auto;
margin-top: 75px;
text-align: center;
}

.gameDiv{
height: 450px;
width: 650px;
position: absolute;
left: 25px;
top: 70px;
}

p{
position: absolute;
font-weight: 600;
right: 50px;
}

.length{
top: -5px;
}

.score{
top: 20px;
}

h2{
position: absolute;
font-weight: 600;
left: 305px;
font-size: 30px;
top: -10px;
}

.deflory{
position: absolute;
bottom: 10px;
right: 5px;
height: 30px;
width: 150px;
}

.snakeCube{
position: absolute;
}
</style>
</head>
<body onload="init()">
<div class="container">
<h2>贪吃蛇</h2>
<p class="length">
长度:
</p>
<p class="score">
分数:
</p>
<div class="gameDiv">

</div>
<img src="img/Deflory.png" class="deflory"/>
</div>
</body>
</html>

1. 移动贪吃蛇

(1) 键盘监听

首先需要让小蛇动起来,因此就会需要键盘监听事件。

凡事都不能一口吃成一个大胖子,不要急着写移动的功能,先看看能否监听到键盘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var ROWS = 18
var COLS = 26
var SNAKE_SIZE = 25

// 初始化
function init(){
onKeyDown();
}

// 键盘监听事件
function onKeyDown(){
document.onkeydown = function(event){
switch(event.keyCode){
// W 和 ↑
case 38:
case 87:{
console.log("上");
break;
}
// A 和 ←
case 37:
case 65:{
console.log("左");
break;
}
// S 和 ↓
case 40:
case 83:{
console.log("下");
break;
}
// D 和 →
case 39:
case 68:{
console.log("右");
break;
}
}
}
}

稍微按了几下,浏览器的输出也正常,接下来就开始写移动的函数了。

console

但在此之前,还有一件事。

havesomethingmore

(2) 创建小蛇

我们没有东西去移动啊!所以要写一个小蛇初始化的函数。

此处snakeCube类我只写了个position:absolute,不然每次写挺麻烦的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//创建初始的贪吃蛇
function createSnake(){
game_div = document.getElementsByClassName("gameDiv")[0];
// 蛇头
snakeHead = document.createElement("img");
snakeHead.setAttribute("src","./img/right.png")
snakeHead.className = "snakeCube";
game_div.appendChild(snakeHead);

// 蛇身×3
for(var i=0;i<3;i++){
snakeBody = document.createElement("img");
snakeBody.setAttribute("src","./img/body.png")
snakeBody.className = "snakeCube";
game_div.appendChild(snakeBody);
}
}

如果单单只是创建元素,而不给它“定位”,也就是设定top和left值,他显示是会叠在一起的。

error

(3) 定位

综上所述,还有一件事,再写一个定位的函数。不要忘了在createSnake的末尾加上这个定位函数。

1
2
3
4
5
6
7
8
9
10
// 定位蛇的位置
var snakeLocation = [[200, 125],[175, 125],[150, 125],[125, 125]]
function snakeLocate(){
var snakes = document.getElementsByClassName("snakeCube");
var len = snakes.length;
for(var i=0;i<len;i++){
snakes[i].style.left = snakeLocation[i][0] + "px";
snakes[i].style.top = snakeLocation[i][1] + "px";
}
}

(4) 移动

好了,历经千辛万苦,终于可以准备让这个小蛇动起来了。

先来解释下这里的逻辑吧,想想贪吃蛇是怎么移动的呢?

是不是头先动,然后紧接着第一节身体移动到原先头的位置,第二节身体移动到原先第一节身体的位置。因此我们只需要把snakeLocation数组从后往前覆盖就行了(不能从先往后噢,想想就明白了)。

此外这里x,y都是单位长度,因此还要乘以蛇的大小。

1
2
3
4
5
6
7
8
9
10
// 贪吃蛇移动,x为横向,y为纵向
function move(x, y){
for(var i=snakeLen-1;i>0;i--){
snakeLocation[i][0] = snakeLocation[i-1][0];
snakeLocation[i][1] = snakeLocation[i-1][1];
}
snakeLocation[0][0] = snakeLocation[0][0] + SNAKE_SIZE * x;
snakeLocation[0][1] = snakeLocation[0][1] + SNAKE_SIZE * y;
snakeLocate();
}

好像略微有些惊悚嘛,不过没关系,修改一下移动时头部的方向就行。

只需要在移动前修改方向变量,定位时判断方向即可,这里代码太多就放出来一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var snakeHeadDirection = "right";
// 键盘监听事件,
function onKeyDown(){
document.onkeydown = function(event){
switch(event.keyCode){
// W 和 ↑
case 38:
case 87:{
snakeHeadDirection = "up";
move(0, -1);
console.log("上");
break;
}
}
}
}

// 定位蛇的位置
function snakeLocate(){
var snakes = document.getElementsByClassName("snakeCube");
for(var i=0;i<snakeLen;i++){
snakes[i].style.left = snakeLocation[i][0] + "px";
snakes[i].style.top = snakeLocation[i][1] + "px";
}

switch(snakeHeadDirection){
case "up":{
snakes[0].setAttribute("src","./img/up.png")
break;
}
}
}

2. 碰撞判定

(1) 自身碰撞判定

贪吃蛇碰到自己身体就应该结束游戏了。

自身碰撞判定无非就是判断头和身体有没有碰撞,一个for-loop就行了。

当然,我们还需要一个flag,来判断当前是否已经结束,默认是false。

函数实现起来比较轻松,注意每次move都需判断,而且得在“定位”之前判断。

1
2
3
4
5
6
7
8
9
10
// 自身碰撞判定
function isTouchItself(){
var left = snakeLocation[0][0];
var top = snakeLocation[0][1];
for(var i=1;i<snakeLen;i++){
if(left == snakeLocation[i][0] && top == snakeLocation[i][1]){
gameover = true;
}
}
}

然后还得修改一下键盘监听事件,只有gameover = false时才监听移动事件。

这里我多添加了两个方法,一会儿实现。

一个是gameover = true时的restart();一个是gameover = false时的pause()。

前者用于重新开始,后者用于游戏暂停

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 键盘监听事件
function onKeyDown(){
document.onkeydown = function(event){
if(!gameover){
switch(event.keyCode){
// W 和 ↑
case 38:
case 87:{
snakeHeadDirection = "up";
move(0, -1);
console.log("上");
break;
}
// A 和 ←
case 37:
case 65:{
snakeHeadDirection = "left";
move(-1, 0);
console.log("左");
break;
}
// S 和 ↓
case 40:
case 83:{
snakeHeadDirection = "down";
move(0, 1);
console.log("下");
break;
}
// D 和 →
case 39:
case 68:{
snakeHeadDirection = "right";
move(1, 0);
console.log("右");
break;
}
// 空格键
case 32:{
pause();
break;
}
}
}else{
switch(event.keyCode){
// 空格键
case 32:{
restart();
break;
}
}
}
}
}

(2) 边框碰撞判定

贪吃蛇是不能穿越这个游戏的边框的,至于碰到边框是会死,还是从另一边出来可以自由发挥。

我这里实现的是从另一头出来的方式。

类似自身碰撞判定,也是要在“定位”之前判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 边界碰撞判定
function crashBoundary(){
var left = snakeLocation[0][0];
var top = snakeLocation[0][1];
if(left < 0){
snakeLocation[0][0] = 625;
}else if(left > 625){
snakeLocation[0][0] = 0;
}else if(top < 0){
snakeLocation[0][1] = 425;
}else if(top > 425){
snakeLocation[0][1] = 0;
}
}

3.游戏结束

(1) 计时器

首先,我们的小蛇不能自己动,需要设置一个计时器。

因为需要暂停和继续游戏,所以我们在方法中判断,如果当前有计时器就取消它,没有则设置一个。

记得在初始化的时候调用一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 暂停/继续游戏
var timer = null;
function pause(){
if(timer == null){
timer = setInterval(function(){
switch(snakeHeadDirection){
case "up":{
move(0, -1);
break;
}
case "down":{
move(0, 1);
break;
}
case "left":{
move(-1, 0);
break;
}
case "right":{
move(1, 0);
break;
}
}
}, 200);
}else{
clearInterval(timer);
timer = null;
}
}

暂停的时候,屏幕上空荡荡的也不大好,给人有一种没有暂停的错觉。所以加一个div,美化下暂停的界面。

1
2
3
4
5
6
7
8
9
.paused{
position: absolute;
display: none;
top: 210px;
z-index: 9;
color: #ffffff;
left: 205px;
line-height: 60px;
}
1
2
3
<h1 class="paused">
按下 SPACE 继续游戏
</h1>

在修改下上面pause方法,暂停的时候显示提示,继续时隐去提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function pause(){
if(timer == null){
timer = setInterval(function(){
switch(snakeHeadDirection){
case "up":{
move(0, -1);
break;
}
case "down":{
move(0, 1);
break;
}
case "left":{
move(-1, 0);
break;
}
case "right":{
move(1, 0);
break;
}
}
}, 200);
var end = document.getElementsByClassName("paused")[0]
end.style.display = "none";
}else{
clearInterval(timer);
timer = null;
var end = document.getElementsByClassName("paused")[0]
end.style.display = "block";
}
}

(2) 游戏结束

在此之前,我又写了个div,可以先看看效果。同暂停类似,只需要将它的display值默认设为none,待到游戏结束时在改成block即可。

处于美观,我将游戏背景颜色改成了深灰色。

1
2
3
4
5
6
7
8
9
.gameover{
display: none;
position: absolute;
top: 190px;
z-index: 9;
color: #ffffff;
left: 205px;
line-height: 60px;
}
1
2
3
4
5
<h1 class="gameover">
游戏结束
<br>
按下 <font color="#ee3000">SPACE</font> 重新开始
</h1>
1
2
3
4
5
function final(){
var end = document.getElementsByClassName("gameover")[0]
end.style.display = "none";
pause();
}

(3) 重新开始

重新开始需要把所有的变量都重置,然后删除游戏中的贪吃蛇,再把游戏结束的提示隐去,然后和init()一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 重新开始
function restart(){
snakeLocation = [[200, 125],[175, 125],[150, 125],[125, 125]]
snakeLen = 4;
snakeHeadDirection = "right";
gameover = false;
timer = null;

var gameDiv = document.getElementsByClassName("gameDiv")[0];
var children = gameDiv.children;
var childrenLen = children.length;
for(var i=childrenLen-1;i>=0;i--){
gameDiv.removeChild(children[i]);
}
var end = document.getElementsByClassName("gameover")[0]
end.style.display = "none";
createSnake();
pause();
}

4. 分数系统

最后就是小蛇蛇吃的食物啦。

(1) 食物是在屏幕中随机出现,而且吃完一个就会出现下一个。

创建食物同创建小蛇类似,只是多了个random函数,要确保食物随机在游戏界面中。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建食物
function createFood(){
foodLeft = Math.floor(Math.random() * COLS);
foodTop = Math.floor(Math.random() * ROWS);
var gameDiv = document.getElementsByClassName("gameDiv")[0];
var food = document.createElement("img");
food.className = "foodCube";
food.setAttribute("src","./img/food.png");
food.style.top = foodTop * SNAKE_SIZE + "px";
food.style.left = foodLeft * SNAKE_SIZE + "px";
gameDiv.appendChild(food);
updateData();
}

(2) 吃一个长度+1,分数+10,且小蛇的身体+1。

蛇头碰到食物即吃掉,删除该img,然后生成新的食物;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 吃掉食物
function eatFood(){
var left = snakeLocation[0][0];
var top = snakeLocation[0][1];
if(foodLeft == left && foodTop == top){
snakeLen++;
score += 10;
var food = document.getElementsByClassName("foodCube")[0];
var gameDiv = document.getElementsByClassName("gameDiv")[0];
gameDiv.removeChild(food);
createFood();
grow();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 长身体
function grow(){
var gameDiv = document.getElementsByClassName("gameDiv")[0];
var body = document.createElement("img");
body.className = "snakeCube";
body.setAttribute("src","./img/body.png");

// 当前最后一个身体的位置
var lastTop = snakeLocation[snakeLen-2][1];
var lastLeft = snakeLocation[snakeLen-2][0];
// 新的身体的位置
var bodyTop = 0;
var bodyLeft = 0;

switch(snakeHeadDirection){
case "up":{
bodyTop = lastTop + 1;
bodyLeft = lastLeft;
break;
}
case "down":{
bodyTop = lastTop - 1;
bodyLeft = lastLeft;
break;
}
case "left":{
bodyTop = lastTop;
bodyLeft = lastLeft + 1;
break;
}
case "right":{
bodyTop = lastTop;
bodyLeft = lastLeft - 1;
break;
}
}
body.style.top = bodyTop * SNAKE_SIZE + "px";
body.style.left = bodyLeft * SNAKE_SIZE + "px";
snakeLocation[snakeLen-1] = [bodyLeft, bodyTop];
gameDiv.appendChild(body);
}

之后更新界面分数

1
2
3
4
5
6
7
// 更新界面中分数和长度
function updateData(){
var scoreH1 = document.getElementsByClassName("score")[0];
var lengthH1 = document.getElementsByClassName("length")[0];
scoreH1.innerHTML = "分数:" + score;
lengthH1.innerHTML = "长度:" + snakeLen;
}

(3) 分数越高,小蛇移动速度越快。

三句代码即可,记得添加到适当的位置,这里就不给出来了。

1
2
3
var speed = 1;
speed += score / 10 * 0.05;
setInterval(function(){}, 100 / speed);

5. 游戏性

至此,贪吃蛇的代码已全部写完,之后可以按自己的想法添加一些游戏的规则,例如可以添加一个红色果实的设定,效果是绿色果实的三倍,出现概率10%。我就不实现了,现在已经凌晨了,累了。

游戏运行画面如下。

以后写小游戏就不写这么详细了,这也太累了😭