在这个游戏中,玩家通过控制屏幕中的精灵,躲开前方随机出现的敌人。玩家躲避敌人的时间越久,得到的分数就越高。
为了好玩,我们还会为游戏加入一些作弊模式。如果玩家按下了X键,每一个敌人的速度就会降低到超慢。如果玩家按下了Z键,敌人就会反转方向,沿着屏幕向上移动而不是往下落。
目录
(一)回顾pygame的基本数据类型
- pygame.Rect:Rect对象表示一个矩形空间的位置和大小。位置可以通过Rect对象的topleft(或者topright、bottomleft和bottomright)属性来确定。这些属性是表示X坐标和Y坐标的整数的一个元组。矩形的大小可以通过width属性和height属性来决定,这些属性表示矩形区域的长和高是多少像素。Rect对象有一个colliderect()方法,用来检查矩形是否和其他Rect对象有碰撞。
- pygame.Surface:Surface对象是带颜色的像素的区域。Surface对象表示一个矩形图像,而Rect对象只表示一个矩形的空间和位置。Surface对象有一个blit()方法,它将一个Surface对象上的图像绘制到另一个Surface对象之上。由pygame.display.set_mode()函数返回的Surface对象是特殊的,因为当调用pygame.display.update()函数时,在该Surface对象上绘制的任何物体都会显示在用户屏幕上。
- pygame.event.Event:当用户提供键盘、鼠标或其他类型的输入时,pygame.event模块会创建Event对象。pygame.event.get()函数返回这些Event对象的一个列表。可以通过查看Event对象的type属性,来查看事件的类型。QUIT、KEYDOWN和MOUSEBUTTONUP是一些事件类型的示例。
- pygame.font.Font:pygame.font模块拥有一个Font数据类型,用于表示pygame中的文本的字体。传递给pygame.font.SysFont()函数的参数是表示字体名称的一个字符串以及表示字体大小的一个整数。然而,通常传递None作为字体名称以获取默认系统字体。
- pygame.time.Clock:pygame.time模块中的Clock对象有助于避免程序运行得过快。Clock对象有一个tick()方法,它接收的参数表示想要游戏运行速度是每秒多少帧(FPS)。FPS越高,游戏运行越快。
(二)导入模块
import pygame, random, sys
from pygame.locals import *
pygame.locals模块提供了几个供pygame使用的常量,如事件类型(QUIT和KEYDOWN等)和键盘按键(K_ESCAPE和K_LEFT)等。通过使用from pygame.locals import *语句,我们可以在源代码中只输入QUIT,而不必输入pygame.locals.QUIT。*代表所有pygame.locals导出的对象名称。
(三)创建常量
WINDOWWIDTH = 600
WINDOWHEIGHT = 600
TEXTCOLOR = (0, 0, 0)
BACKGROUNDCOLOR = (255, 255, 255)
以上代码为窗口大小、文本颜色和背景颜色设置了常量。将常量赋值给变量,要比直接输入常量更具有描述性,而且也便于修改。
接下来我们为FPS设置了常量,这是想要让游戏按照每秒多少帧的速度运行:
FPS = 60
帧(frame是在游戏循环的单次迭代中所绘制的一个屏幕)。
后面跟代码中,我们将FPS传递给mainClock.tick方法,以便函数可以知道程序要暂停多久。
下面我们设置了更多的常量,用来描述落下的敌人。
BADDIEMINSIZE = 10
BADDIEMAXSIZE = 40
BADDIEMINSPEED = 1
BADDIEMAXSPEED = 8
ADDNEWBADDIERATE = 6
敌人的宽度和高度均在BADDIEMINSIZE和BADVIEMAXSIZE之间。在游戏循环的每次迭代中,敌人从屏幕上落下的速率在每秒BADDIEMINSPEED到BADDIEMINSPEED多个像素之间。游戏循环每经过ADDNEWBADDIERATE次迭代之后,将在窗口的顶部增加一个新的敌人。
如果玩家的角色是移动的,在游戏循环的每次迭代中,PLAYERMOVERATE将保存玩家的角色在窗口中移动的像素数。
PLAYERMOVERATE = 5
通过增大这个数字,就可以加快角色的移动速度。
(四)定义函数
我们将为这个游戏创建几个函数。terminate()和waitForPlayerToPressKey()函数将分别终止和暂停程序,playerHasHitBaddie()函数将会记录玩家和敌人的碰撞,drawText()函数将会把得分和其他的文本绘制到屏幕上。
1)终止和暂停游戏
def terminate():
pygame.quit()
sys.exit()
把pygame.quit()和sys.exit()放到一个函数里面。
有时,我们想要暂停游戏,直到玩家按下一个键再继续游戏。这里体现在Dodger标题文本刚出现的时候,需要玩家随意按下一个键来开始游戏,或者在出现Game Over且游戏结束的时候。我们创建了一个waitForPlayerToPressKey()函数:
def waitForPlayerToPressKey():
while True:
for event in pygame.event.get():
if event.type == QUIT:
terminate()
if event.type == KEYDOWN:
if event.key == K_ESCAPE: # Pressing ESC quits.
terminate()
return
在这个函数中有一个无限循环,只有当接收到一个KEYDOWN或QUIT事件时,才会跳出该循环。否则代码将包初循环,看上去就像是冻结了,直到玩家按下一个键。
2)记录和敌人的碰撞
如果玩家的角色和一个敌人碰撞,playerHasHitBaddie()函数将返回True。
def playerHasHitBaddie(playerRect, baddies):
for b in baddies:
if playerRect.colliderect(b['rect']):
return True
return False
baddies参数是“baddie”字典数据结构的一个列表。其中每一个字典都有rect键,该键的值是表示敌人大小和位置的一个Rect对象。
playerRect也是一个Rect对象。playerRect对象和baddies列表中的Rect对象如果发生碰撞,则该方法返回True。
如果未发生碰撞,该函数返回False。
3)将文本绘制到窗口
在窗口绘制文本包含了几个步骤,我们把这些步骤放到drawText()函数中去。当我们呢需要在屏幕上显示文本的时候,只需要调用drawText()函数即可:
def drawText(text, font, surface, x, y):
textobj = font.render(text, 1, TEXTCOLOR)
textrect = textobj.get_rect()
textrect.topleft = (x, y)
surface.blit(textobj, textrect)
首先调用render()方法创建了一个Surface对象,文本以特定的字体渲染其上。
接下来调用Surface对象的get_rect()方法来获取Surface对象的大小和位置,返回一个Rect对象的大小和位置。
然后重新设置这个Rect对象的topleft属性,来改变它的位置。
最后,将其上已经绘制了文本的Surface,绘制到了作为参数传递给drawText()函数的Surface上。
(五)初始化pygame并设置窗口
# Set up pygame, the window, and the mouse cursor.
pygame.init()
mainClock = pygame.time.Clock()
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
pygame.display.set_caption('Dodger')
pygame.mouse.set_visible(False)
初始化、以及创建一个pygame.time.Clock()对象、创建Surface对象、设置窗口的标题,和前面的相同,这里就不赘述了。
pygame.display.set_mode()函数的第2个参数是可选的。我们可以传递pygame.FULLSCREEN常量,使得窗口占据整个屏幕,而不是显示为一个小窗口。即修改为:
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),pygame.FULLSCREEN)
在Dodger中,鼠标光标应该是不可见的。我们想要鼠标能够在屏幕上移动玩家的角色,但是,鼠标的光标可能会挡住角色的图像,只需要一行代码,就可以让鼠标不可见:
pygame.mouse.set_visible(False)
告诉pygame,让鼠标光标不可见。
(六)设置Font、Sound和Image对象
# Set up the fonts.
font = pygame.font.SysFont(None, 48)
# Set up sounds.
gameOverSound = pygame.mixer.Sound('pickup.wav')
pygame.mixer.music.load('www.mp3')
# Set up images.
playerImage = pygame.image.load('player.png')
playerRect = playerImage.get_rect()
baddieImage = pygame.image.load('cherry.png')
调用pygame.font.SysFont()函数创建了一个Font对象,跟随系统字体,设置字体的大小为48.
加载游戏背景音乐和玩家碰到敌人时播放的音乐。
最后加载图像文件,使用get_rect()方法获取Rect对象的大小和位置,赋值给playerRect对象。
(七)显示开始界面
# Show the "Start" screen.
windowSurface.fill(BACKGROUNDCOLOR)
drawText('Dodger', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
drawText('Press a key to start.', font, windowSurface,
(WINDOWWIDTH / 3) - 30, (WINDOWHEIGHT / 3) + 50)
pygame.display.update()
waitForPlayerToPressKey()
topScore = 0
topScore变量用来记录最高分,后面代码中将这个最高分显示在屏幕上。
(八)游戏循环
游戏循环的代码通过修改玩家和敌人的位置、处理pygame生成的事件以及在屏幕上绘制游戏世界,来不断地更新游戏世界地状态。所有这些事情会在1秒钟内发生很多次,这使得游戏“实时”地运行。
每进行一次while循环,score就会增加1,score增加地频率也就是游戏循环地频率。
while True:
# Set up the start of the game.
baddies = []
score = 0
playerRect.topleft = (WINDOWWIDTH / 2, WINDOWHEIGHT - 50)
moveLeft = moveRight = moveUp = moveDown = False
reverseCheat = slowCheat = False
baddieAddCounter = 0
pygame.mixer.music.play(-1, 0.0)
while True: # The game loop runs while the game part is playing.
score += 1 # Increase score.
for event in pygame.event.get():
if event.type == QUIT:
terminate()
if event.type == KEYDOWN:
if event.key == K_z:
reverseCheat = True
if event.key == K_x:
slowCheat = True
if event.key == K_LEFT or event.key == K_a:
moveRight = False
moveLeft = True
if event.key == K_RIGHT or event.key == K_d:
moveLeft = False
moveRight = True
if event.key == K_UP or event.key == K_w:
moveDown = False
moveUp = True
if event.key == K_DOWN or event.key == K_s:
moveUp = False
moveDown = True
if event.type == KEYUP:
if event.key == K_z:
reverseCheat = False
score = 0
if event.key == K_x:
slowCheat = False
score = 0
if event.key == K_ESCAPE:
terminate()
if event.key == K_LEFT or event.key == K_a:
moveLeft = False
if event.key == K_RIGHT or event.key == K_d:
moveRight = False
if event.key == K_UP or event.key == K_w:
moveUp = False
if event.key == K_DOWN or event.key == K_s:
moveDown = False
if event.type == MOUSEMOTION:
# If the mouse moves, move the player to the cursor.
playerRect.centerx = event.pos[0]
playerRect.centery = event.pos[1]
# Add new baddies at the top of the screen, if needed.
if not reverseCheat and not slowCheat:
baddieAddCounter += 1
if baddieAddCounter == ADDNEWBADDIERATE:
baddieAddCounter = 0
baddieSize = random.randint(BADDIEMINSIZE, BADDIEMAXSIZE)
newBaddie = {'rect': pygame.Rect(random.randint(0,
WINDOWWIDTH - baddieSize), 0 - baddieSize,
baddieSize, baddieSize),
'speed': random.randint(BADDIEMINSPEED,
BADDIEMAXSPEED),
'surface':pygame.transform.scale(baddieImage,
(baddieSize, baddieSize)),
}
baddies.append(newBaddie)
# Move the player around.
if moveLeft and playerRect.left > 0:
playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
if moveRight and playerRect.right < WINDOWWIDTH:
playerRect.move_ip(PLAYERMOVERATE, 0)
if moveUp and playerRect.top > 0:
playerRect.move_ip(0, -1 * PLAYERMOVERATE)
if moveDown and playerRect.bottom < WINDOWHEIGHT:
playerRect.move_ip(0, PLAYERMOVERATE)
# Move the baddies down.
for b in baddies:
if not reverseCheat and not slowCheat:
b['rect'].move_ip(0, b['speed'])
elif reverseCheat:
b['rect'].move_ip(0, -5)
elif slowCheat:
b['rect'].move_ip(0, 1)
# Delete baddies that have fallen past the bottom.
for b in baddies[:]:
if b['rect'].top > WINDOWHEIGHT:
baddies.remove(b)
# Draw the game world on the window.
windowSurface.fill(BACKGROUNDCOLOR)
# Draw the score and top score.
drawText('Score: %s' % (score), font, windowSurface, 10, 0)
drawText('Top Score: %s' % (topScore), font, windowSurface,
10, 40)
# Draw the player's rectangle.
windowSurface.blit(playerImage, playerRect)
# Draw each baddie.
for b in baddies:
windowSurface.blit(b['surface'], b['rect'])
pygame.display.update()
# Check if any of the baddies have hit the player.
if playerHasHitBaddie(playerRect, baddies):
if score > topScore:
topScore = score # Set new top score.
break
mainClock.tick(FPS)
# Stop the game and show the "Game Over" screen.
pygame.mixer.music.stop()
gameOverSound.play()
drawText('GAME OVER', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
drawText('Press a key to play again.', font, windowSurface,
(WINDOWWIDTH / 3) - 80, (WINDOWHEIGHT / 3) + 50)
pygame.display.update()
waitForPlayerToPressKey()
gameOverSound.stop()
playerRect为玩家图像的位置和大小的Rect对象。
1)事件处理
有4中不同的事件要处理:QUIT、KEYDOWN、KEYUP、和MOUSEMOTION。
处理键盘事件
pygame.event.get()方法在事件队列中获取事件,获取事件之后便将该事件从队列中移除
for event in pygame.event.get():
if event.type == QUIT:
terminate()
if event.type == KEYDOWN:
if event.key == K_z:
reverseCheat = True
if event.key == K_x:
slowCheat = True
if event.key == K_LEFT or event.key == K_a:
moveRight = False
moveLeft = True
if event.key == K_RIGHT or event.key == K_d:
moveLeft = False
moveRight = True
if event.key == K_UP or event.key == K_w:
moveDown = False
moveUp = True
if event.key == K_DOWN or event.key == K_s:
moveUp = False
moveDown = True
if event.type == KEYUP:
if event.key == K_z:
reverseCheat = False
score = 0
if event.key == K_x:
slowCheat = False
score = 0
if event.key == K_ESCAPE:
terminate()
if event.key == K_LEFT or event.key == K_a:
moveLeft = False
if event.key == K_RIGHT or event.key == K_d:
moveRight = False
if event.key == K_UP or event.key == K_w:
moveUp = False
if event.key == K_DOWN or event.key == K_s:
moveDown = False
KEYDOWN事件实现上下左右的移动,同时增加了Z和X键。
按Z键实现敌人反向运动,释放Z键,将解除反向作弊模式。
按X键降低敌人移动速度,释放X键,敌人移动速度 回复正常。
处理鼠标事件
如果玩家点击鼠标按键,Dodger不会做任何事情,但是当玩家移动鼠标的时候,游戏会做出相应。这就使得玩家在游戏中可以有两种方法来控制玩家角色:鼠标和键盘。
if event.type == MOUSEMOTION:
# If the mouse moves, move the player to the cursor.
playerRect.centerx = event.pos[0]
playerRect.centery = event.pos[1]
MOUSEMOTION类型的Event对象,也有一个名为pos的属性,表示鼠标事件的位置。pos属性保存了一个元组,是鼠标光标在窗口中移动到的位置的X坐标和Y坐标。如果事件的类型是MOUSEMOTION,玩家的角色将移动到鼠标光标的位置。
2)增加新的敌人
# Add new baddies at the top of the screen, if needed.
if not reverseCheat and not slowCheat:
baddieAddCounter += 1
if baddieAddCounter == ADDNEWBADDIERATE:
baddieAddCounter = 0
baddieSize = random.randint(BADDIEMINSIZE, BADDIEMAXSIZE)
newBaddie = {'rect': pygame.Rect(random.randint(0,
WINDOWWIDTH - baddieSize), 0 - baddieSize,
baddieSize, baddieSize),
'speed': random.randint(BADDIEMINSPEED,
BADDIEMAXSPEED),
'surface':pygame.transform.scale(baddieImage,
(baddieSize, baddieSize)),
}
baddies.append(newBaddie)
在游戏循环的每一次迭代中,变量baddieAddCounter都会加1.
只有作弊模式未启用才会这么做。记住,只要分别按下Z键和X键,就会把reverseCheat和slowCheat设置为True。
当按下X或者Z键时,baddieAddCounter就不会增加。因此也不会有新的敌人会出现在屏幕的顶部。
当baddieAddCounter等于ADDNEWBADDIERATE中的值时,就会在屏幕增加一个新的敌人。ADDNEWBADDIERATE变量在程序最开始的地方进行的定义,表示敌人增加的速率。
3)移动玩家角色和敌人
当pygame触发了KEYDOWN 和KEYUP事件时,把4个移动变量moveLeft、moveRight、moveUp和moveDown分别设置为True和False。
move_ip()方法将会把Rect对象的位置水平地或垂直地移动多个像素。move_ip()的第1个参数是要将Rect对象向右移动多少个像素(如果要向左移动,就给这个参数传入负值)。第2个参数是要将Rect对象向下移动多少个像素(要向上移动,就给这个参数传入负值)。
# Move the player around.
if moveLeft and playerRect.left > 0:
playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
if moveRight and playerRect.right < WINDOWWIDTH:
playerRect.move_ip(PLAYERMOVERATE, 0)
if moveUp and playerRect.top > 0:
playerRect.move_ip(0, -1 * PLAYERMOVERATE)
if moveDown and playerRect.bottom < WINDOWHEIGHT:
playerRect.move_ip(0, PLAYERMOVERATE)
# Move the baddies down.
for b in baddies:
if not reverseCheat and not slowCheat:
b['rect'].move_ip(0, b['speed'])
elif reverseCheat:
b['rect'].move_ip(0, -5)
elif slowCheat:
b['rect'].move_ip(0, 1)
实现作弊模式
elif reverseCheat:
b['rect'].move_ip(0, -5)
elif slowCheat:
b['rect'].move_ip(0, 1)
如果激活了反向作弊模式,那么敌人应该以5个像素的速度向上移动。
如果没有激活任何的作弊模式,那么向下移动敌人位置的像素数目就等于它的速度(该值存储在'speed'键中)。
删除敌人
任何跌落到窗口底边之下的敌人都应该从baddies列表中删除。记住,当遍历一个列表时,不能增加或删除元素以修改列表中的内容。所以for循环遍历的不是baddies列表,而是这个baddies列表的一个副本。
# Delete baddies that have fallen past the bottom.
for b in baddies[:]:
if b['rect'].top > WINDOWHEIGHT:
baddies.remove(b)
4)绘制窗口
# Draw the game world on the window.
windowSurface.fill(BACKGROUNDCOLOR)
# Draw the score and top score.
drawText('Score: %s' % (score), font, windowSurface, 10, 0)
drawText('Top Score: %s' % (topScore), font, windowSurface,
10, 40)
# Draw the player's rectangle.
windowSurface.blit(playerImage, playerRect)
# Draw each baddie.
for b in baddies:
windowSurface.blit(b['surface'], b['rect'])
pygame.display.update()
# Check if any of the baddies have hit the player.
if playerHasHitBaddie(playerRect, baddies):
if score > topScore:
topScore = score # Set new top score.
break
mainClock.tick(FPS)
# Stop the game and show the "Game Over" screen.
pygame.mixer.music.stop()
gameOverSound.play()
drawText('GAME OVER', font, windowSurface, (WINDOWWIDTH / 3),
(WINDOWHEIGHT / 3))
drawText('Press a key to play again.', font, windowSurface,
(WINDOWWIDTH / 3) - 80, (WINDOWHEIGHT / 3) + 50)
pygame.display.update()
waitForPlayerToPressKey()
gameOverSound.stop()
要注意的是,最后调用waitForPlayerToPressKey()函数等待的过程中,要结束播放"Game Over"。所以调用stop()方法来实现。
以上就是我们的图形化游戏。
(九)修改Dodger游戏
你可能觉得这个游戏太简单或者太难。但是这个游戏很容易修改,因为我们使用的是常量,而不是直接输入值。现在,我们要修改游戏的话,我们只需要修改在常量中设置的值。
- 降低游戏整体运行速度:减小FPS变量;
- 降低敌人速度:减小BADDIEMAXSPEED.
当基本的游戏保持相同时,可以修改任何的常量,以便对游戏的行为产生显著的影响。不断为这些常量尝试新的值,直到找到一套最喜欢的值。
参考:
- 菜鸟教程
- http://inventwithpython.com/invent4thed/chapter3.html
- 《Python游戏编程快速上手》第四版,AI Sweigart著,李强 译
- https://www.pygame.org/docs/ref/locals.html