如何编写贪吃蛇游戏代码🐍
我通常不会把这类博客发到开发者社区,因为这通常是我自己做的事情。游戏开发很有趣,我从中获得了许多乐趣。如果你喜欢这篇博客,请告诉我,也许我会在这里发布更多内容!
诺基亚的贪吃蛇游戏于 1997 年随诺基亚 6110 手机一同推出,这款手机堪称移动游戏的催化剂!然而,贪吃蛇游戏的概念早在 1997 年的版本问世之前就已存在。在另一篇博客中,我探讨了如何编写 Blockade(1976 年)这款游戏,它本质上就是玩家对战的贪吃蛇游戏,比 1997 年的版本早了整整 21 年!
在这篇博客中,我将一步一步地讲解如何编写贪吃蛇游戏的代码,因为贪吃蛇游戏通常被推荐给编程新手。然而,我个人并不推荐初学者直接编写贪吃蛇游戏,因为它确实有一些需要摸索才能解决的棘手问题。如果你是一名编程新手,我建议你看看“适合初学者编写代码的五大最佳游戏”!或者,如果你从未接触过编程,那么可以看看“如何以初学者的身份编写游戏”。
让我们开始编写贪吃蛇游戏的代码吧!
所以,在这篇博客中,我将使用https://editor.p5js.org/,它使用了p5.js库,这使得 Canvas 的使用体验更加流畅。我为这篇博客编写的所有代码都可以在这里找到,如果您遇到任何问题,可以随时参考。
为蛇建造一个居住的网格
如果你研究一下贪吃蛇游戏,你会发现所有东西都放置在一个隐藏的网格中,让我们创建一个我们自己的网格——最后会将其移除——这样我们就可以把所有东西都放在合适的位置,让我们的游戏更轻松。
const GRID_SIZE = 20;
function setup() {
createCanvas(700, 700);
}
function draw() {
background(155, 204, 153);
for (let x = 0; x < width; x += width / GRID_SIZE) {
for (let y = 0; y < height; y += height / GRID_SIZE) {
stroke(255);
strokeWeight(1);
line(x, 0, x, height);
line(0, y, width, y);
}
}
}
这样应该会得到类似这样的结果:
现在我们可以随时调整GRID_SIZE游戏设置,使其更符合我们的喜好。这是一个重要的变量,蛇的位置和食物的位置都将基于此。
创造蛇
让我们创建一个名为 `.snake.js` 的新文件snake.js,其中将包含我们蛇的类。别忘了添加对 `.snake.js` 的引用index.html,以便我们能够使用它:
<body>
<script src="sketch.js"></script>
<script src="snake.js"></script> // add this
</body>
SNAKE.JS
class Snake {
constructor() {
this.body = [];
this.body.push({x: width/2, y: height/2}); // the head of the snake
this.dir = 1; // 1 = right, 2 = down, 3 = left, 4 = right
}
draw() {
fill(0);
for (let b of this.body) {
rect(b.x, b.y, width / GRID_SIZE, height / GRID_SIZE)
}
}
update() {
if (this.dir == 1) {
this.body[0].x += width / GRID_SIZE;
} else if (this.dir == 2) {
this.body[0].y += height / GRID_SIZE;
} else if (this.dir == 3) {
this.body[0].x -= width / GRID_SIZE;
} else if (this.dir == 4) {
this.body[0].y -= height / GRID_SIZE;
}
}
}
所以我们为蛇创建了一个body数组,其中包含蛇身各部分的坐标x和y位置。我们还在构造函数中将蛇头添加到蛇身,所以当我们创建一个新的 Snake 对象时,蛇头就被添加到蛇身中。
我正在预判蛇的移动,我知道它可以上下左右移动,所以如果值dir设置为 1,我们就向右移动,如果设置为 2,我们就向下移动,设置为 3,我们就向左移动,设置为 4,我们就向上移动。
我们还有一种draw方法,可以简单地绘制代表蛇身的矩形。
最后,还有一种update方法,它只移动蛇头,使其朝向我们移动的方向。注意,我只移动蛇头,因为如果我移动整条蛇,它看起来就完全不像蛇了。我们之后还需要用到这种方法,因为我们需要更新蛇身的其他部分——前提是我们已经有了蛇身的其他部分,而不仅仅是蛇头。
SKETCH.JS
回到游戏,sketch.js我们需要创建蛇形对象并调用更新/绘制方法。另外,我们将帧速率限制为 4,以营造复古感!
const GRID_SIZE = 20;
let snake;
function setup() {
createCanvas(700, 700);
snake = new Snake();
frameRate(4);
}
function draw() {
background(155, 204, 153);
for (let x = 0; x < width; x += width / GRID_SIZE) {
for (let y = 0; y < height; y += height / GRID_SIZE) {
stroke(255);
strokeWeight(1);
line(x, 0, x, height);
line(0, y, width, y);
}
}
snake.update();
snake.draw();
}
点击播放后,你应该会看到类似这样的画面:
添加按键处理程序
当我们按下方向键时,我们希望改变方向,请将此功能添加到您的代码中。sketch.js
function keyPressed() {
if (keyCode === 39 && snake.dir !== 3) {
snake.dir = 1;
} else if (keyCode === 40 && snake.dir !== 4) {
snake.dir = 2;
} else if (keyCode === 37 && snake.dir !== 1) {
snake.dir = 3;
} else if (keyCode === 38 && snake.dir !== 2) {
snake.dir = 4;
}
}
所以,它的意思是,当我们按下右键但没有向左移动时,就改变方向向右;当我们按下下键但没有向上移动时,就向下移动,等等。
玩一下:
给蛇添加食物
蛇吃东西就会长大,我们喂它吧。
食品.JS
创建一个新文件food.js,并记得在你的文件中添加对此文件的引用index.html。
class Food {
constructor() {
this.spawn();
}
spawn() {
let randX = random(width);
let randY = random(height);
this.x = randX - randX % (width / GRID_SIZE);
this.y = randY - randY % (height / GRID_SIZE)
}
draw() {
fill(255, 100, 100);
rect(this.x, this.y, width / GRID_SIZE, height / GRID_SIZE);
}
}
所以我们给食物在网格中随机放置一个位置,代码randX - randX % (width / GRID_SIZE);只是允许我们将食物对齐到网格的某个方格内。
我们已经放好了食物,但是没有给蛇提供吃东西的途径😢 让我们给它一张嘴吧。
在我们的主绘图函数中sketch.js
...
function draw() {
background(155, 204, 153);
for (let x = 0; x < width; x += width / GRID_SIZE) {
for (let y = 0; y < height; y += height / GRID_SIZE) {
stroke(255);
strokeWeight(1);
line(x, 0, x, height);
line(0, y, width, y);
}
}
snake.update();
if (snake.hasEatenFood()) { // add this code
food.spawn();
}
snake.draw();
food.draw();
}
我们还没有编写该hasEatenFood方法,所以让我们把它添加进去。snake.js
...
hasEatenFood() {
if (this.body[0].x == food.x && this.body[0].y == food.y) {
return true;
}
}
太棒了!现在我们的蛇可以吃东西而不会变大,羡慕吗?让我们添加一些代码,让我们的蛇变大吧。
SNAKE.JS
这里还有一些东西需要添加,这是最终的完整文件,下面我将解释添加的内容:
class Snake {
constructor() {
this.body = [];
this.body.push({x: width/2, y: height/2}); // the head of the snake
this.dir = 1; // 1 = right, 2 = down, 3 = left, 4 = right
this.lastX = width/2;
this.lastY = height/2;
}
draw() {
fill(0);
for (let b of this.body) {
rect(b.x, b.y, width / GRID_SIZE, height / GRID_SIZE)
}
}
update() {
this.lastX = this.body[this.body.length-1].x; // track the last X and Y
this.lastY = this.body[this.body.length-1].y; // so we can put the new body there
for (let i = this.body.length-1; i >= 1; i--) {
this.body[i].x = this.body[i-1].x;
this.body[i].y = this.body[i-1].y;
}
if (this.dir == 1) {
this.body[0].x += width / GRID_SIZE;
} else if (this.dir == 2) {
this.body[0].y += height / GRID_SIZE;
} else if (this.dir == 3) {
this.body[0].x -= width / GRID_SIZE;
} else if (this.dir == 4) {
this.body[0].y -= height / GRID_SIZE;
}
}
grow() {
this.body.push({x: this.lastX, y: this.lastY});
}
hasEatenFood() {
if (this.body[0].x == food.x && this.body[0].y == food.y) {
return true;
}
}
}
我们现在追踪蛇的头部lastX和尾部lastY,这样当蛇进食时,食物基本上就会落到那个位置,也就是蛇的尾部。我还添加了代码来更新蛇的整个身体,所以在更新方法中你会看到以下代码:
for (let i = this.body.length-1; i >= 1; i--) {
this.body[i].x = this.body[i-1].x;
this.body[i].y = this.body[i-1].y;
}
这是将每个主体更新为其前x一个y元素的主体,记住头部位于索引 0。
我们还添加了一个grow()方法,该方法只需将新内容添加到数组中即可。
添加命中检测
我们要确保蛇不会撞到自己,如果撞到了,我们就需要重新开始游戏。
我添加了一个新方法,该方法会在类的`<method> hitDetection()` 方法中调用:update()snake
hitDetection() {
for (let i = 1; i < this.body.length; i++) {
if (this.body[0].x == this.body[i].x && this.body[0].y == this.body[i].y) {
this.spawn();
}
}
}
这段代码简单地检查蛇头是否碰到身体的其他部位。我还把构造函数代码移到了一个spawn()与类相同的方法中food,以下是全部代码:
class Snake {
constructor() {
this.spawn();
}
// added this
spawn() {
this.body = [];
this.body.push({x: width/2, y: height/2});
this.dir = 1;
this.lastX = width/2;
this.lastY = height/2;
}
draw() {
fill(0);
for (let b of this.body) {
rect(b.x, b.y, width / GRID_SIZE, height / GRID_SIZE)
}
}
update() {
this.hitDetection();
this.lastX = this.body[this.body.length-1].x;
this.lastY = this.body[this.body.length-1].y;
for (let i = this.body.length-1; i >= 1; i--) {
this.body[i].x = this.body[i-1].x;
this.body[i].y = this.body[i-1].y;
}
if (this.dir == 1) {
this.body[0].x += width / GRID_SIZE;
} else if (this.dir == 2) {
this.body[0].y += height / GRID_SIZE;
} else if (this.dir == 3) {
this.body[0].x -= width / GRID_SIZE;
} else if (this.dir == 4) {
this.body[0].y -= height / GRID_SIZE;
}
}
// added this
hitDetection() {
for (let i = 1; i < this.body.length; i++) {
if (this.body[0].x == this.body[i].x && this.body[0].y == this.body[i].y) {
this.spawn();
}
}
}
grow() {
this.body.push({x: this.lastX, y: this.lastY});
}
hasEatenFood() {
if (this.body[0].x == food.x && this.body[0].y == food.y) {
return true;
}
}
}
失去电网
接下来,让我们删除为网格添加的代码,它只是为了确保所有内容都排列整齐!
所以你的绘图函数sketch.js应该看起来像这样:
function draw() {
background(155, 204, 153);
/**
let x = 0; x < width; x += width / GRID_SIZE) {
for (let y = 0; y < height; y += height / GRID_SIZE) {
stroke(255);
strokeWeight(1);
line(x, 0, x, height);
line(0, y, width, y);
}
}
*/
snake.update();
if (snake.hasEatenFood()) {
food.spawn();
snake.grow();
}
stroke(155, 204, 153); // add this
snake.draw();
food.draw();
}
接下来是什么?
贪吃蛇游戏最复杂的部分已经完成,但还有一些地方需要调整。学习的最佳方法就是实践,所以我鼓励你继续编写代码,看看能否完成以下任务。如果你完成了其中任何一项或全部任务,请务必告诉我,我将不胜感激!
- 当蛇到达地图边缘时,游戏应该重新开始,或者蛇应该从墙的另一侧出来。
- 添加评分系统并显示分数(可以是蛇的
body长度) - 确保食物不会产在蛇的身上。
- 这条蛇的初始体长就设定为5。
希望您喜欢这篇博客。如果您奇迹般地喜欢上了我的絮叨,那就去我的博客网站codeheir.com看看吧,我每周都会在那里写博客,内容涵盖编程世界中所有吸引我注意力的话题!
文章来源:https://dev.to/lukegarrigan/how-to-code-snake-1jeb







