贪吃蛇 —— Python 小项目实战

贪吃蛇 —— Python 小项目实战

前言:

在知乎上听大牛说编程直接上项目就是干,以项目为导向,以开发为目标,效果非常好。正巧最近想学python,故尝试以零基础做小项目,疑难部分通过谷歌和书本解决,并通过发表博文检验和锤炼学习成果。

本文结构的思维导图:

导入贪吃蛇小游戏所需要的库和模块

python导入库和模块

使用python进行编程时,有些功能无法用原生python实现。这时需要导入一些python库或者模块,这类似于在Windows操作系统为了实现操作系统没有的功能而去install相应的软件。

导出的基本语法为

import Modulename

Module 模块

import Modulename as Modulename_Aliases

Alias 别名

如常用的

import pandas

import pandas as np

当有多个库或模块时,也可以写到一句中,用逗号隔开

import Module1, Module2, Module3

导入需要的库

  • pygame

    pygame库是一个免费开源的python库,一个利用SDL库的写就的游戏库。

  • sys

    sys是一个python标准模块,提供了一些变量和函数。(在该程序中可加可不加)

  • random

random属于python标准库中的数学和数字模块,作用是生成随机数。在贪吃蛇中,食物出现的位置时随机的。

1
2
3
import pygame
import sys
import random

或者是

1
import pygame, sys, random

设定贪吃蛇游戏界面的大小

全局定义

程序中如果有常量,如恒定的数等。可以在开头全局定义。

语法格式为

name = value

value 值

贪吃蛇游戏中,游戏界面的长和宽是不会变化的,是恒定的值

设定贪吃蛇游戏界面的大小

1
2
3
4
5
6
7
8
9
10
11
12
# 全局定义游戏界面的长和宽(单位为分辨率)
SCREEN_X = 600 # 全局定义游戏界面的长
SCREEN_Y = 600 # 全局定义游戏界面的宽

# 创建一个函数main()作为主函数
def main(): # 创建一个名为main的主函数
pygame.init() #1 初始化pygame模块,确保pygame模块完整可用
screen_size = (SCREEN_X,SCREEN_Y)
screen = pygame.display.set_mode(screen_size) #2 设定游戏窗口的大小(单位:分辨率)
pygame.display.set_caption('Snake') #3 设定游戏窗口的标题为“Snake”
screen.fill((25,25,25)) # 设定屏幕颜色(RGB)
main() # 运行主函数

批注:在Python中,我们用缩进来表示不同代码之间的关系。若要说明一段代码从属于另一端代码,则需要通过缩进四个空格或一个Tab键来表示。不缩进的代码段之间为同级关系。Python中正确的运用缩进不仅能使程序顺利运行,还可以增强代码的可读性。

#1 我们已经知道python有一个特殊的“库(模块)”叫pygame了。在我们要动手用它完成我们的想法之前,电脑这个强迫症需要我们检查一遍,这个工具包是否完整,能否正常给我们提供帮助。而这个检查的动作,就是pygame.init()

#2 pygame.display.set_mode(resolution=(SCREEN_X,SCREEN_Y))

初始化一个准备显示的窗口或屏幕。

#3 pygame.display.set_caption(‘title’)

设置当前游戏窗口的标题

当设置完成后,运行代码就可以看到一个“一闪而过”的游戏窗口了

用类创建一个“贪吃蛇”模板

Python从问世之初就是一个可以面向对象的语言。在面向对象编程中,可以用类(class)表示现实世界的事物和情形。类(class)类似于现实世界中的模板。当创建了类后,你定义了整个对象类别可以有的一般行为和特征。

如在贪吃蛇游戏中,我需要定义一个贪吃蛇的类,在其中定义一般贪吃蛇所具有的行为和特征。当用贪吃蛇的类创建一个独立的对象——一条贪吃蛇,就等于把这个类实例化(instantiation)。该条贪吃蛇就可以一个实例(instance)。

在python中创建一个类的基本语法为(在Python中以下三者等价):

class ClassName:

class ClassName():

class ClassName(object):

创建一个贪吃蛇的类,并初始化个各种需要的属性

1
2
3
4
5
6
7
8
# 在Python中类的名称一般首字母大写,与类的实例(全部小写)进行区分
class Snake:
# 初始化各种需要的属性 [开始时默认向右/身体块x5]
def __init__(self): #4
self.dirction = pygame.K_RIGHT #5 设置贪吃蛇默认从右边开始运动,即按→方向键
self.body = [] #6 为实例创建一个名为body的空列表
for x in range(5): #7 将addnote()函数从0到4循环五次
self.addnode() #8 初始设定贪吃蛇有5个蛇块

类中的函数在Python中叫做方法(method)。在类中的方法具有函数的一切特征,也有一些区别。

创建函数的一般语法为

def funcitonname(parameter1, parameter2, ...):

parameter 参数

#4处的__init__是一类特殊的函数。当我们根据类创建了一个实例时,Python会自动调用__init__函数。 在Python中的每个类中都必须要有__init__函数。

#4处定义的__init__函数一个参数self。**self参数在每一个类的每一个方法里都是必须的,而且顺序必须是第一个。**因为当我们根据类创建实例后,每一个与实例相关联的方法的调用会自动传递self参数,self是对实例本身的引用,它可以让各个实例可以访问类中的属性。在我们创建的__init__函数中,不需要其他参数,所以只需要self一个必须参数就可以了。

#5处的变量self.dirction带有self的前缀。在类中任何以self为前缀的变量都可以被类中的每个方法调用(因为每个方法中都有参数self),我们也可以通过从类中创建的任何实例来访问这些变量。

#5处的pygame.K_RIGHT是Pygame库中的属性,作用是使创建的对象(在这里是贪吃蛇)向右运动,相当于键入→方向键。

同理 pygame.K_UP使创建的对象向上运动。

同理 pygame.K_DOWN使创建的对象向下运动。
Pygame库中可以用字符常量表示输入键盘中的特定键位。

Pygame Constant The key in keyboard
K_UP up arrow
K_DOWN down arrow
K_LEFT left arrow
K_RIGHT right arrow
K_SPACE space

#6处为self参数创建一个body的属性,body初始化为空列表,利用列表的可变性(列表中元素的值和数量都可以变化)存放蛇块。

#7&#8处,为self参数创建一个addnote()的属性,用以增加蛇块的数量,初始化蛇块数量设定为5(可以根据个人喜好修改,但不宜过多)

设定贪吃蛇移动时的蛇块变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 无论何时 都在前端增加蛇块
def addnode(self): #9
# 设定贪吃蛇从屏幕的右上角开始出现
left,top = (0,0) #10
if self.body:
left,top = (self.body[0].left,self.body[0].top) #11
# 定义蛇在游戏开始时出现的位置,以及蛇的每一节身体的长宽(单位为分辨率)
node = pygame.Rect(left, top, 25, 25) #12
if self.dirction == pygame.K_LEFT: #13
node.left -= 25
elif self.dirction == pygame.K_RIGHT: #14
node.left += 25
elif self.dirction == pygame.K_UP: #15
node.top -= 25
elif self.dirction == pygame.K_DOWN: #16
node.top += 25
self.body.insert(0,node) #17

# 删除最后一个块
def delnode(self): #18
self.body.pop() #19

#9 在这段代码中我们创建了addnode方法,该方法有且self这一个必须参数

#10 设定贪吃蛇的出现位置在最右上角。left, top在后文代表的意思可以用下面这张图片解释。

#11处出现的rect函数中的left, top指的是矩形区域(白色部分)距离x轴和y轴的水平距离和垂直距离。

Pygame 通过 Rect 对象存储和操作矩形区域,由pygame.Rect(left, top, width, height)命令创建。

该一整段代码的运行流程:

__init__函数中有一个for循环,使addnote()方法按照该流程循环了5次。

第一次循环并没有执行if self.body:后的语句,因为我们最初定义的self.body是一个空列表。所以第一次循环首先执行node = pygame.Rect(left, top, 25, 25)。这时我们创建了一个在游戏界面最右上角,大小为25*25像素的矩形对象node。因为在__init__函数中我们self.dirction = pygame.K_RIGHT语句使对象向右移动,所以判定向右运动为真,node变量的left参数加25——即向右运动了25像素。这样实现了一个像素块向右的移动。最后的self.body.insert(0,node)命令在self.body列表的插入node变量,并将其放在第一位。

之后四次循环中,因为self.body列表不再是空列表,所以总是执行第一个if语句后的命令,将参数left,top赋值为self.body列表中的第一位,这样做,使得每次循环之初的node对象总是上一次循环得到的移动过后的像素块,并在此基础上根据运动方向再次移动——这样做实现 贪吃蛇身体的连续,使得贪吃蛇刚出来就是完整的五个25*25像素块

这样做是不够的,因为随着贪吃蛇的移动,是不断右像素块(图中绿色,颜色可自定义)产生和像素块的消失。在最后的像素块总是最先消失的。也就是self.body[-1]会随着每次移动而删去。

所以我们需要另外创建一个delnode函数

1
2
def delnode(self):
self.body.pop()

delnode方法同样仅有self这一必须参数,pop函数可以删去列表中的最后一个元素。

我们需要同时同刷新率运行addnode和delnode方法。这时,又需要创建一个方法move.在运行addnode增加蛇块的同时运行delnode减少蛇块。

1
2
3
4
# 移动!
def move(self):
self.addnode()
self.delnode()

那么,除非吃到食物(后面会设置),我们以类创建出来的贪吃蛇的蛇块增加速度等于删除速度,最后蛇的长度不变。又因为蛇块总是在动态变化,我们就用程序完成了蛇的移动。

效果图

改变方向 但是左右、上下不能被逆向改变

但这样是不够的。在经典的贪吃蛇游戏中,蛇是不能逆向运动的。我们需要在蛇左右或上下运动的同时对其运动方向做出限制。

1
2
3
4
5
6
7
8
9
10
# 改变方向 但是左右、上下不能被逆向改变
def changedirection(self,curkey): #20
LR = [pygame.K_LEFT,pygame.K_RIGHT] #21
UD = [pygame.K_UP,pygame.K_DOWN]
if curkey in LR+UD: #22
if (curkey in LR) and (self.dirction in LR): #23
return self.dirction #24
if (curkey in UD) and (self.dirction in UD):
return self.dirction
self.dirction = curkey #25

我的方案是将左右运动和上下运动分开讨论,因为两者的情况是不一样。

在左右运动时,无论是按还是→键,蛇总是按照原来的方向前进。左右运动时情况类似。

#21 我们分别创建LR和UD列表代表左右运动和上下运动。

#20 这时我们需要输入蛇当前的运动方向,所以在定义方法时在self参数后加了curkey参数。

#23&#24 我们需要判定curkey是否与self.dirction都处于向左或向右运动方向上,如果为真,则返回self.dirction。后面的语句就不会执行了。

#25 若curkey与当前的self.dirction不冲突,则将curkey的值赋予self.dirction,这样贪吃蛇的方向就发生了改变。

贪吃蛇的死亡判断

经典的贪吃蛇中有两种死亡方式,一是碰撞到墙壁(在这里我们用屏幕边界代替),而是头碰到自己的身体。这两者只要满足一个就可以判定为死亡。

这里先做一个简单的死亡判断函数,对死亡前后的各种对象的设置将会在main函数中进行。

1
2
3
4
5
6
7
8
9
10
11
# 死亡判断
def isdead(self):
# 撞墙
if self.body[0].x not in range(SCREEN_X): # 若蛇块不在屏幕范围内,判定死亡为真
return True
if self.body[0].y not in range(SCREEN_Y):
return True
# 撞自己
if self.body[0] in self.body[1:]: # 如果撞到自己,判定为死亡
return True
return False

一开始,我们需要在主函数中初始化isdead方法为否,即

isdead = False

self.body列表中存储着一些代表蛇块的矩形对象。当任何一个矩形对象超出屏幕范围内时,则返回isdead为真;或者,当代表蛇头的矩形对象(在self.body列表中总是第一个元素)的位置(left和top参数)包含于蛇身(self.body[1:])时,则返回isdead为真。

当两个条件都不满足时,返回isdead = False

用类创建一个“食物”模板

食物所具有的特征就贪吃蛇简单多了,我们需要创建关于食物的类,满足下列的要求:

1.食物出现的位置随机

2.食物被贪吃蛇碰到后会更换位置出现在界面上

3.食物的大小适中(在这里我们固定食物的大小为25*25像素,等于一个蛇块的大小)

创建一个食物的类

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Food:
def __init__(self):
self.rect = pygame.Rect(0,0,25,25) # rect对象是用来存储矩形对象的
def remove(self): # 去除食物
self.rect.left=0

def set(self): # 随机设置食物的出现地点
if self.rect.left == 0:
allpos = [] # 26
# 不靠墙太近 25 ~ SCREEN_X-25 之间fenb
for pos in range(25,SCREEN_X-25,25):
# 将屏幕减小外围25分辨率后的所有位置(以25*25像素块为单位)都添加到allpos列表中
allpos.append(pos)
self.rect.left = random.choice(allpos)
self.rect.top = random.choice(allpos)
print(self.rect)

我们需要一个刷新食物出现地点的判定标准,当类Food的实例被创建时,python会自动调用__init__函数,这时就应该自动刷新食物出现的地点。当食物被贪吃蛇吃掉时,也应该自动刷新食物出现的地点。

在这里我选择self.rect.left == 0为真,作为食物刷新的条件。所以在__init__方法中,我将0赋给了self.rect.left,并设定了食物的大小为25*25分辨率。

在remove方法中,我选择直接赋值,因为食物大小已经设置过了,不需要再次设置。

之后,很容易想到可以用random函数随机分配食物的位置。

这时可以选择先构建一个包含所有位置的库(为了降低游戏难度,删除了外围25分辨率),在用choice函数在位置库中随机选取。也可以直接在random函数中选构建所有位置的表达式后一步解决。这里选择较简单的前一种。

#26 用for循环将除外围25分辨率的所有位置加入空列表allpos,再用random.choice()方法随机返回。choice() 方法返回一个列表,元组或字符串的随机项。

1
2
import random
random.choice( seq )

注意:choice()是不能直接访问的,需要导入 random 模块,然后通过 random 静态对象调用该方法。

seq – 可以是一个列表,元组或字符串。

实例化类Snack和类Food

从该分隔线以下代码均在main函数内

从该分隔线以下代码均在main函数内

从该分隔线以下代码均在main函数内


实例化类的语法为instancename = ClassName(parameter1, parameter2, ...)

在python中一般用全小写字母表示实例,与类做出区分。

1
2
snack = Snack()
food = Food()

在pygame中,设定游戏开始有相对固定的一套代码

1
2
3
4
5
6
7
8
9
10
while True:
for event in pygame.event.get(): # 设定游戏开始
if event.type == pygame.QUIT: # 判定玩家是否退出(点击窗口的X关闭游戏)
pygame.quit()
if event.type == pygame.KEYDOWN: # 检查玩家是否按下关键键位,为真则执行if后语句
snake.changedirection(event.key) # 玩家按下方向键以后执行改变方向
# 死后按space重新开始
if event.key == pygame.K_SPACE and isdead:
# 如果贪吃蛇死亡判定为真,且玩家按了space键则重新开始游戏
return main()

pygame提供了现成的方法检测玩家按下输入关键键位

pygame.KEYDOWN 按下键盘时所按下的键

pygame.KEYUP 释放键盘时

event.key 指的是玩家按下的关键键位

构建蛇身体

1
2
3
4
if not isdead:                               # 如果蛇没有死,那么蛇会一直移动
snake.move()
for rect in snake.body:
pygame.draw.rect(screen,(20,220,39),rect) #27 设置贪吃蛇的颜色(RGB颜色)

如果isdead参数为否,则执行snake实例中的move方法。

#27 rect表示矩形对象, 对snake.body中的每一个矩形对象,用pygame.draw.rect(screen, RGB,shape)绘制矩形(贪吃蛇就是由矩形构成)。 screen表示屏幕界面,RGB用三元元组表示,rect为固定参数。

加入食物与蛇的互动

1
2
3
4
5
6
7
8
9
# 食物处理 / 吃到+50分
# 当食物rect与蛇头重合,吃掉 -> Snake增加一个Node
if food.rect == snake.body[0]:
food.remove() # 移除
snake.addnode() # 增加蛇的一节身体

# 食物投递
food.set() # 刷新一个食物
pygame.draw.rect(screen,(136,0,21),food.rect) # 绘制矩形对象

计算分数并在屏幕上打印文字

添加语句创建scores 变量并赋初值为0

1
2
3
4
5
6
7
8
9
def main():
pygame.init() #初始化pygame模块,确保pygame模块完整可用
screen_size = (SCREEN_X,SCREEN_Y)
screen = pygame.display.set_mode(screen_size) #设定游戏窗口的大小(分辨率吧)
pygame.display.set_caption('Snake') #设定游戏窗口的标题
clock = pygame.time.Clock() #创建一个名字为clock的对象来记录时间
# 这是添加的语句
scores = 0 #初始化分数为0
isdead = False #创建死亡判定参数isdead,初始为False

构建在屏幕上打印字体的函数

1
2
3
4
5
6
7
8
9
10
11
def show_text(screen, pos, text, color, font_bold = False, font_size = 60, font_italic = False):   
#获取系统字体,并设置文字大小
cur_font = pygame.font.SysFont("宋体", font_size)
#设置是否加粗属性
cur_font.set_bold(font_bold)
#设置是否斜体属性
cur_font.set_italic(font_italic)
#设置文字内容
text_fmt = cur_font.render(text, 1, color)
#绘制文字
screen.blit(text_fmt, pos)

代码中方法均为pygame库内置。

用函数在屏幕上显示分数

1
2
3
4
5
6
# 显示分数文字
show_text(screen,(50,500),'Scores: '+str(scores),(223,223,223))
# The Pygame.display.update() 该方法可重新绘制屏幕
pygame.display.update()
# The clock.tick() method 设定一秒内刷新的次数(数值越大,蛇运动的越快)
clock.tick(10)

显示死亡文字

1
2
3
4
5
# 显示死亡文字
isdead = snake.isdead()
if isdead: # 判断贪吃蛇是否死亡
show_text(screen,(100,200),'YOU DEAD!',(227,29,18),False,100)
show_text(screen,(150,260),'press space to try again...',(0,0,22),False,30)

运行和调试

最后来一个main函数就可以运行游戏了!

1
main()

项目源代码地址

github.com/ZhangChunXian/learn-python-by-projects/tree/master/项目源代码

最终效果:

写在最后

感谢各位的阅读!你的阅读是我更新的动力源泉。请根据博文质量点击相应的星级。

这是我第一次做python方面的项目。在尚无基础的情况下上手,我翻阅了一些书籍,还有不少大牛的博客,最终写下了这篇博文。有许多不足,望诸位谅解并指出在评论区,也可以通过邮件告诉我,我将逐一听取。感谢各位在我成长过程中给予的帮助。

同时也欢迎各位计算机学习者前来交流。