文章目录
贪吃蛇
贪吃蛇终会吃掉自己,恰如人类的欲望。
Pygame不像random一样是Python自带的,所以我们要手动把它安装进python里。
以下基于电脑端,手机端的读者我也建议耐心地看完这一章,理解游戏实现的逻辑。
pip
pip是一个Python库管理工具,一般你安装Python后就有了。
在cmd中输入pip
,查看pip是否存在:
像这样蹦出一推东西就说明pip存在,你可以直接查看下面的安装Pygame部分。
否则它会说:'pip' 不是内部或外部命令,也不是可运行的程序或批处理文件。
,接下来跟着我的步骤解决这个问题:
按照上一节函数部分说的方式找到Python目录下的Scripts文件夹,复制此路径:
可以看到pip安静地躺在这里。
打开我的电脑
或者win10上叫做此电脑
,空白处鼠标右键,点击属性
:
点击打开的页面里的高级系统设置
:
点击新开窗口的环境变量
,在系统变量
里双击Path
:
点击新建
,粘贴刚才复制的路径,然后点击确定。再到cmd里输入pip就看看到那一大堆东西了。
安装Pygame
我们可以使用pip便捷安装pygame了!
在cmd里输入pip install pygame
,敲下回车,然后等待安装完成。
不用怀疑电脑是不是出问题了,pip就是这么慢。
使用换源方式安装:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pygame
,会快很多。
-i https://pypi.tuna.tsinghua.edu.cn/simple
告诉pip去后面这个镜像网站里下载,常用的镜像网站:
-
清华:
https://pypi.tuna.tsinghua.edu.cn/simple
-
阿里云:
http://mirrors.aliyun.com/pypi/simple/
-
中国科技大学:
https://pypi.mirrors.ustc.edu.cn/simple/
-
华中理工大学:
http://pypi.hustunique.com/
-
山东理工大学:
http://pypi.sdutlinux.org/
-
豆瓣:
http://pypi.douban.com/simple/
喝口水,休息休息,接下来是个“大工程”。
新手容易遇到的错误:
-
用了中文符号
-
括号不匹配(正括号和反括号数量不一致)
-
缩进错误
-
条件判断时用
=
而不是==
。
开始
各长其仪,周则复始。
我们来尝试第一个Pygame程序:
#pygame_hello.py
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((500,500))
textFont = pygame.font.Font(None,30)
def hello():
textSurface = textFont.render("Hello World!",True,(255,255,255))
window.blit(textSurface,(200,200))
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
hello()
pygame.display.update()
pygame.quit()
敲出这段代码,运行后你会看到一个黑色背景的小窗口,中间是那段咒语。
接下来我会给出详细讲解:
import pygame
import sys
导入pygame和sys,后面要用这里面的函数。
pygame.init()
window = pygame.display.set_mode((500,500))
pygame.init()
会进行一些准备工作(初始化),每个pygame程序都要先初始化,我们不用关心它具体做了些什么。
下一行创建了主窗口window,并设置了窗口的大小:宽和高。
textFont = pygame.font.Font(None,30)
def hello():
textSurface = textFont.render("Hello World!",True,(255,255,255))
window.blit(textSurface,(200,200))
textFont = pygame.font.Font(None,30)
获得了一个字体,我们要用它来绘制文字。
hello()
函数里我们使用设置了文字内容,抗锯齿(True),颜色。
最后一行将设置好的文字textSurface
绘制在了上面我们创建的窗口window
中,(200,200)
是文字的位置,单位像素。
你可能会有点迷糊,但你要明确:
-
等于号
=
左边是变量名,它的值是等号右边的结果。 -
括号
(
左边是函数,它做了一些事情,可能返回一个东西给等号左边的变量,也可能改变了什么东西。 -
函数括号里面是一些参数,如果还不明白参数作用,翻看上一节函数部分。
关于颜色
pygame采用三个0到255的整数来表示颜色,分别代表红(red)、绿(green)、蓝(blue),值越大,颜色越深,即(0,0,0)
代表黑色,(255,255,255)
代表白色。
这种颜色表示方式称为rgb颜色。
还有四个元素的颜色表示(255,255,255,255)
,第四个元素表示透明度(alpha),值越小越透明,这种表示法称为rgba颜色。
Pygame两种颜色表示都支持。
while True:
window.fill((0,0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
hello()
pygame.display.update()
这是Pygame主循环,每一次循环就是一帧。
帧是游戏中一个重要的概念,代表显示屏幕的一次刷新。
游戏的运行就是每秒钟多少次的屏幕刷新让你觉得自己控制的角色在动,子弹在飞,怪物在追……。
Pygame中的每一帧,就是其主循环(while)的每次循环。
while True:
window.fill((0,0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
window.fill((0,0,0))
给窗口涂了个黑色的背景颜色。
Pygame里鼠标的点击、键盘的按下之类的都被称作事件,可以用pygame.event.get()
获得当前的事件,它返回一个事件列表。
pygame.QUIT
是个Pygame里存的一个变量,储存着退出事件,就是当你按下窗口右上角的X触发的事件。
Pygame里有大量的保存着事件的变量,后面我们会看到更多。
for
遍历所有事件列表,如果发现有个事件的类型是退出事件(event.type == pygame.QUIT
),就调用sys.exit()
结束程序,当然循环也结束了。
while True:
......
hello()
pygame.display.update()
pygame.quit()
每一帧都调用hello()
函数,把文本画上去。
每一帧所有东西画好了,就交给pygame.display.update()
刷新窗口。
循环结束后,pygame.quit()
会结束Pygame。
Pygame里的事件
例如上面的pygame.QUIT
,下面展示了部分事件,省略pygame.
:
QUIT | KEYDOWN | KEYUP | MOUSEMOTION | MOUSEBUTTONUP | MOUSEBUTTONDOWN |
---|---|---|---|---|---|
退出 | 键盘按键按下 | 键盘按键抬起 | 鼠标移动 | 鼠标按键抬起 | 鼠标按键按下 |
我们来尝试这段代码来感受事件的使用:
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((500,500))
textFont = pygame.font.Font(None,30)
num = 0
def hello():
textSurface = textFont.render("%d"%(num),True,(255,255,255))
window.blit(textSurface,(200,200))
while True:
window.fill((0,0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.MOUSEBUTTONUP:
num += 1
hello()
pygame.display.update()
pygame.quit()
试着用之前学过的内容理解上面的代码干了什么,不理解就运行看看。
看看有什么新东西:
num = 0
神秘的新变量。
textSurface = textFont.render("%d"%(num),True,(255,255,255))
原来的咒语变成了新变量。
while True:
......
elif event.type == pygame.MOUSEBUTTONUP:
num += 1
如果发现鼠标按键抬起事件(event.type == pygame.MOUSEBUTTONUP
),num就增加1 。
发现了吧,这是一个升级版的手指抽筋模拟器,我把它保存为手指抽筋模拟器pygame版.py
。
不要问我为什么总和手指抽筋过不去。
你可以在https://gitee.com/ZiKang12138/course看到本系列中的游戏源码。
游戏世界
混沌初开,乾坤始奠。
我们来创建贪吃蛇的基本元素:方块。
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((500,500))
def paint():
pygame.draw.rect(window,(255,255,255),((200,300),(30,30)))
while True:
window.fill((0,0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
paint()
pygame.display.update()
pygame.quit()
原来的hello
函数变成了paint
函数。
pygame.draw.rect
的作用是画一个方块,它接受三个参数:
-
画在哪个窗口上,这里是
window
-
rgb颜色,这里是白色
(255,255,255)
-
一个元组或列表,包含位置和大小:位置:
(200,300)
,大小:(30,30)
。使用列表也行,元组也行,后面我会统称为数组。
Pygame常用的绘画函数(省略pygame.draw.)
rect | polygon | circle | ellipse | line | aaline |
---|---|---|---|---|---|
矩形 | 多边形 | 圆形 | 椭圆 | 直线 | 平滑的线 |
Pygame坐标系
上面已经是我们第二次遇到位置了,第一次是设置文字位置。
中学我们学过笛卡尔坐标系,原点(0,0)
在中间,右方向是x轴正方向,上方向是y轴正方向,空间的每个点都可以用一个数组表示:
而Pygame的坐标系的原点(0,0)
在窗口的左上角,右方向为x轴正方向,下方向为y轴正方向。
所以方块位置(200,300)
的具体含义是:方块的左上角距离窗口最左边200像素,距离顶部300像素。
运动
看见这个小方块就不可抑制地想让他动起来,而动起来很简单,每帧修改小方块的位置就行了。我们可以这样修改paint
函数:
head = [200,200]
def paint():
head[0] -= 2
pygame.draw.rect(window,(255,255,255),(head,(20,20)))
新设了一个数组变量head
,用作pygame.draw.rect
里的位置参数。paint
函数会在每一帧调用,所以head[0] -= 2
会在每帧里将head
的第一个元素,也就是x轴位置减少2,这会使小方块持续向左运动。
修改完后运行试试。
你会发现方快快速地向左运动,一闪而过。这是因为pygame会在尽可能高的帧率下运行。如果你的小方块是慢慢运动,那说明你该换电脑了。
我们可以用pygame.time.Clock().tick(FPS)
,来限制帧率,下面是修改后的代码:
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((500,500))
clock = pygame.time.Clock()
FPS = 30
speed = 2
head = [200,200]
def paint():
head[0] -= speed
pygame.draw.rect(window,(255,255,255),(head,(30,30)))
while True:
window.fill((0,0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
paint()
pygame.display.update()
clock.tick(FPS)
pygame.quit()
clock = pygame.time.Clock()
创建帧率控制工具。
clock.tick(FPS)
在每帧结束时暂停一段时间,使帧率控制在FPS这个整数上。
但这对于玩家没有意义,我们要让小方块在玩家的控制下移动。我们可以用到上面事件里说的键盘按下事件:
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((500,500))
textFont = pygame.font.Font(None,30)
clock = pygame.time.Clock()
direction = "left"
speed = 3
FPS = 30
head = [200,200]
def paint():
if direction=="left":
head[0]-=speed
elif direction=="right":
head[0]+=speed
elif direction=="up":
head[1]-=speed
elif direction=="down":
head[1]+=speed
pygame.draw.rect(window,(255,255,255),(head,(20,20)))
while True:
window.fill((0,0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_a:
direction = "left"
elif event.key == pygame.K_d:
direction = "right"
elif event.key == pygame.K_s:
direction = "down"
elif event.key == pygame.K_w:
direction = "up"
paint()
pygame.display.update()
clock.tick(FPS)
pygame.quit()
不要被这个长度唬住了,这段代码在逻辑上非常简单。
direction = "left"
新设了一个变量direction
储存了一个字符串来表示方块的运动方向。
def paint():
if direction=="left":
head[0]-=speed
elif direction=="right":
head[0]+=speed
elif direction=="up":
head[1]-=speed
elif direction=="down":
head[1]+=speed
根据direction
的值修改head
:
while True:
......
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_a:
direction = "left"
elif event.key == pygame.K_d:
direction = "right"
elif event.key == pygame.K_s:
direction = "down"
elif event.key == pygame.K_w:
direction = "up"
检查键盘事件并改为相应的方向,event.key
表示键盘事件。
pygame.K_a
是键盘按下a键事件,pygame.K_d
,是键盘按下b键事件,以此类推。
于是玩家可以点击wsad来控制小方块的上下左右运动。
网格
但我们可怜的小蛇应该是一格一格运动。
我们只要把小蛇身体的最后一格删除,在它运动方向上增加一格,就完成了运动。这比整个身体整体移动要简单的多。
比如运动方向为"up"
:
我们不需要真的把窗口切成网格,只需要方块每次移动它本身大小的距离,然后降低帧率就营造成”一格一格移动“的感觉了,为此我们只需要修改两个变量:
speed = 20
FPS = 4
实际上speed
就是方块的大小,所以代码表示方块大小的地方都可以用speed替换。
再运行试试,方块是不是一格一格地移动。
“这只有蛇头没有身体!”
那我们给他加上身体。在head
变量下面添加新变量:
head = [200,200]
body = [[head[0]+speed,head[1]]]
[[head[0]+speed,head[1]]]
这一层套一层的括号到底做了什么,让我们把它计算出来:head[0]+speed
就等于220,所以body
实际就是[[220,200]]
,这储存着head
右边一格的位置,我们可以继续往body
里面添加位置来使蛇身变长。
paint
函数也要修改,把身体画出来:
def paint():
body.pop(-1)
body.insert(0,[head[0],head[1]])
if direction=="left":
......
pygame.draw.rect(window,(200,200,200),(head,(speed,speed)))
for pos in body:
pygame.draw.rect(window,(255,255,255),(pos,(speed,speed)))
上一节列表部分我们知道了pop用来删除列表指定索引位置的元素,所以body.pop(-1)
删除了body
最后一个元素。
body.insert(0,[head[0],head[1]])
又往body的最前面插入了当前head
的位置,之后的判断语句会使head
的位置改变,这就完成了上面说的:”删除身体的最后一个,在运动方向上增加一格“。
我们改变一个头的颜色来区分头和身体:pygame.draw.rect(window,(200,200,200),(head,(speed,speed)))
最后遍历body
,每拿到个位置便绘制一个白方块。
修改完后运行试试,你有一个正常移动的蛇了。
有坑小心!
为什么这么写body.insert(0,[head[0],head[1]])
,而不是直接body.insert(0,head)
?
试一试:
a = 1
b = a
print(b)
a = 0
print(b)
很明显结果是:
1
1
那用列表呢?
a = [1,2,3]
b = a
print(b)
a[2] = 666
print(b)
结果是:
[1, 2, 3]
[1, 666, 3]
修改a
的元素也改变了b
的元素,这被称为列表的浅拷贝。我们现在不需要知道为什么,只要知道遇到列表的值的传递时要谨慎点。
吃苹果,长身体!
蛇为什么要吃苹果?不知道,但我们还是把红色小方块称为苹果吧。
修改代码,画出苹果:
import pygame
import sys
import random
......
body = [[head[0]+20,head[1]]]
apples = []
def createApple():
while True:
newApple = [random.randint(0,500-speed)//speed *speed,random.randint(0,500-speed)//speed *speed]
if newApple not in apples and newApple != head and newApple not in body:
apples.append(newApple)
break
def paint():
......
for pos in body:
pygame.draw.rect(window,(255,255,255),(pos,(speed,speed)))
for apple in apples:
pygame.draw.rect(window,(255,0,0),(apple,(speed,speed)))
while True:
window.fill((0,0,0))
......
导入random
库。
创建新变量apples
来储存苹果的位置。
新函数createApple
,进入循环:
newApple =[random.randint(0,500-speed)//speed*speed,random.randint(0,500-speed)//speed*speed]
创建了一个苹果的随机位置,random.randint
函数我们在猜大小的游戏里用过,它返回两个整数之间的一个随机整数。因为坐标表示小方块左上角的位置,所以在窗口500像素、位置是(500,500)时,小方块实际上不在窗口中,为了显示整个小方块所以把位置限制在0到500-speed
之间。
//
运算符在第二节运算符的表格里,表示除法然后取整,这里除以speed
取整,使苹果在“网格”上,再乘以speed
转换回真正的坐标,不妨试试看到底会输出什么:
print(random.randint(0,500-20)//20*20) #别忘记speed是20
直到这一堆东西newApple not in apples and newApple != head and newApple not in body
为真就把生成的苹果位置添加进apples
里,然后退出循环。
这一堆东西由两个and
连接,即表示三个部分都为真时结果才是真,这三个部分意义:
-
newAppe not in apples
:创建的随机位置newApp
不在apples
里,避免苹果重叠。 -
newAppe != head
:newApple
不等于head
,避免刚产生苹果就被吃了。 -
newApple not in body
:newApple
不在身体里。
最后在paint
函数里,我们把苹果画出来。
记得在循环之前创建苹果:
createApple()
while True:
......
你也可以在开始时多创建几个。
修改完后运行代码,你会看到红色的苹果出现了:
但是贪吃蛇只会与苹果“插肩而过”,吃不到。
修改paint
函数,让他能吃到苹果:
def paint():
body.insert(0,[head[0],head[1]])
if direction=="left":
head[0]-=speed
elif direction=="right":
head[0]+=speed
elif direction=="up":
head[1]-=speed
elif direction=="down":
head[1]+=speed
if head not in apples:
body.pop(-1)
else:
apples.remove(head)
createApple()
pygame.draw.rect(window,(200,200,200),(head,(speed,speed)))
for pos in body:
pygame.draw.rect(window,(255,255,255),(pos,(speed,speed)))
for apple in apples:
pygame.draw.rect(window,(255,0,0),(apple,(speed,speed)))
主要修改的地方:
def paint():
......
if head not in apples:
body.pop(-1)
else:
apples.remove(head)
createApple()
......
如果head
不在在apples
里,即头的位置不等于任何一个苹果的位置,那么就删除身体的最后一个元素,这就是普通的移动。
否则头在一个苹果的位置上,删除apples
里的这个位置,实际上就是删除了这个位置的苹果,再创建一个新苹果。没有删除body
的元素,但前面head
的位置添加进了body
,所以实际上就是增加了身体的长度。
运行看看,你的蛇学会成长了。
游戏结束
当越来越长的蛇蹿出窗口或者咬到自己,游戏结束,屏幕上打出分数(吃掉苹果的数量)。
创建变量GameOver
来保存游戏状态:
GameOver = False
apple = []
......
在paint
里判断head
的位置:
def paint():
global GameOver
......
elif direction=="down":
head[1]+=speed
if head[0]<0 or head[0]>500-speed or head[1]<0 or head[1]>500-speed or head in body:
GameOver = True
if head not in apples:
......
关键字global
用来声明全局变量。
全局变量与局部变量
试一试:
a = 1
def f():
a = 2
f()
print(a)
1
结果是1,应为函数内部的变量默认为局部变量,它不能影响外部变量的值,在函数里使用global
声明变量为全局变量就可以在函数里修改外部的值:
a = 1
def f():
global a
a = 2
f()
print(a)
2
在主循环中检查游戏状态(是否结束了):
while True:
window.fill((0,0,0))
for event in pygame.event.get():
......
if GameOver:
textFont = pygame.font.Font(None,60)
textSurface = textFont.render("GameOver! score:%d"%(len(body)-1),True,(255,255,255))
window.blit(textSurface,(35,200))
else:
paint()
pygame.display.update()
clock.tick(FPS)
pygame.quit()
如果游戏结束了,就显示文字,否则继续执行paint
函数。
len(body)
函数返回body
列表的长度。
贪吃蛇完整代码
# 贪吃蛇_pygame.py
import pygame
import sys
import random
pygame.init()
window = pygame.display.set_mode((500,500))
textFont = pygame.font.Font(None,30)
clock = pygame.time.Clock()
direction = "left"
speed = 20
FPS = 4
head = [200,200]
body = [[head[0]+speed,head[1]]]
GameOver = False
apples = []
def createApple():
while True:
newApple = [random.randint(0,500-speed)//speed*speed,random.randint(0,500-speed)//speed*speed]
if newApple not in apples and newApple != head and newApple not in body:
apples.append(newApple)
break
def paint():
global GameOver
body.insert(0,[head[0],head[1]])
if direction=="left":
head[0]-=speed
elif direction=="right":
head[0]+=speed
elif direction=="up":
head[1]-=speed
elif direction=="down":
head[1]+=speed
if head[0]<0 or head[0]>500-speed or head[1]<0 or head[1]>500-speed or head in body:
GameOver = True
if head not in apples:
body.pop(-1)
else:
apples.remove(head)
createApple()
if not GameOver:
pygame.draw.rect(window,(200,200,200),(head,(speed,speed)))
for pos in body:
pygame.draw.rect(window,(255,255,255),(pos,(speed,speed)))
for apple in apples:
pygame.draw.rect(window,(255,0,0),(apple,(speed,speed)))
createApple()
while True:
window.fill((0,0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_a:
direction = "left"
elif event.key == pygame.K_d:
direction = "right"
elif event.key == pygame.K_s:
direction = "down"
elif event.key == pygame.K_w:
direction = "up"
if GameOver:
textFont = pygame.font.Font(None,60)
textSurface = textFont.render("GameOver! score:%d"%(len(body)-1),True,(255,255,255))
window.blit(textSurface,(35,200))
else:
paint()
pygame.display.update()
clock.tick(FPS)
pygame.quit()
本系列中所有游戏的源码可以在https://gitee.com/ZiKang12138/course中查看、下载。