第六章:贪吃虫
原文:
inventwithpython.com/pygame/chapter6.html
译者:飞龙
如何玩贪吃虫
贪吃虫是 Nibbles 的克隆。玩家开始控制一个不断在屏幕上移动的短蠕虫。玩家无法停止或减慢蠕虫,但他们可以控制它转向的方向。红苹果随机出现在屏幕上,玩家必须移动蠕虫以使其吃掉苹果。每次蠕虫吃掉一个苹果,蠕虫就会增长一个段,并且新的苹果会随机出现在屏幕上。如果蠕虫撞到自己或屏幕边缘,游戏就结束了。
贪吃虫源代码
此源代码可从invpy.com/wormy.py
下载。如果出现任何错误消息,请查看错误消息中提到的行号,并检查代码中是否有任何拼写错误。您还可以将代码复制粘贴到invpy.com/diff/wormy
的 Web 表单中,以查看您的代码与书中代码之间的差异。
# Wormy (a Nibbles clone)
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US
import random, pygame, sys
from pygame.locals import *
FPS = 15
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
CELLSIZE = 20
assert WINDOWWIDTH % CELLSIZE == 0, "Window width must be a multiple of cell size."
assert WINDOWHEIGHT % CELLSIZE == 0, "Window height must be a multiple of cell size."
CELLWIDTH = int(WINDOWWIDTH / CELLSIZE)
CELLHEIGHT = int(WINDOWHEIGHT / CELLSIZE)
# R G B
WHITE = (255, 255, 255)
BLACK = ( 0, 0, 0)
RED = (255, 0, 0)
GREEN = ( 0, 255, 0)
DARKGREEN = ( 0, 155, 0)
DARKGRAY = ( 40, 40, 40)
BGCOLOR = BLACK
UP = 'up'
DOWN = 'down'
LEFT = 'left'
RIGHT = 'right'
HEAD = 0 # syntactic sugar: index of the worm's head
def main():
global FPSCLOCK, DISPLAYSURF, BASICFONT
pygame.init()
FPSCLOCK = pygame.time.Clock()
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
BASICFONT = pygame.font.Font('freesansbold.ttf', 18)
pygame.display.set_caption('Wormy')
showStartScreen()
while True:
runGame()
showGameOverScreen()
def runGame():
# Set a random start point.
startx = random.randint(5, CELLWIDTH - 6)
starty = random.randint(5, CELLHEIGHT - 6)
wormCoords = [{'x': startx, 'y': starty},
{'x': startx - 1, 'y': starty},
{'x': startx - 2, 'y': starty}]
direction = RIGHT
# Start the apple in a random place.
apple = getRandomLocation()
while True: # main game loop
for event in pygame.event.get(): # event handling loop
if event.type == QUIT:
terminate()
elif event.type == KEYDOWN:
if (event.key == K_LEFT or event.key == K_a) and direction != RIGHT:
direction = LEFT
elif (event.key == K_RIGHT or event.key == K_d) and direction != LEFT:
direction = RIGHT
elif (event.key == K_UP or event.key == K_w) and direction != DOWN:
direction = UP
elif (event.key == K_DOWN or event.key == K_s) and direction != UP:
direction = DOWN
elif event.key == K_ESCAPE:
terminate()
# check if the worm has hit itself or the edge
if wormCoords[HEAD]['x'] == -1 or wormCoords[HEAD]['x'] == CELLWIDTH or wormCoords[HEAD]['y'] == -1 or wormCoords[HEAD]['y'] == CELLHEIGHT:
return # game over
for wormBody in wormCoords[1:]:
if wormBody['x'] == wormCoords[HEAD]['x'] and wormBody['y'] == wormCoords[HEAD]['y']:
return # game over
# check if worm has eaten an apply
if wormCoords[HEAD]['x'] == apple['x'] and wormCoords[HEAD]['y'] == apple['y']:
# don't remove worm's tail segment
apple = getRandomLocation() # set a new apple somewhere
else:
del wormCoords[-1] # remove worm's tail segment
# move the worm by adding a segment in the direction it is moving
if direction == UP:
newHead = {'x': wormCoords[HEAD]['x'], 'y': wormCoords[HEAD]['y'] - 1}
elif direction == DOWN:
newHead = {'x': wormCoords[HEAD]['x'], 'y': wormCoords[HEAD]['y'] + 1}
elif direction == LEFT:
newHead = {'x': wormCoords[HEAD]['x'] - 1, 'y': wormCoords[HEAD]['y']}
elif direction == RIGHT:
newHead = {'x': wormCoords[HEAD]['x'] + 1, 'y': wormCoords[HEAD]['y']}
wormCoords.insert(0, newHead)
DISPLAYSURF.fill(BGCOLOR)
drawGrid()
drawWorm(wormCoords)
drawApple(apple)
drawScore(len(wormCoords) - 3)
pygame.display.update()
FPSCLOCK.tick(FPS)
def drawPressKeyMsg():
pressKeySurf = BASICFONT.render('Press a key to play.', True, DARKGRAY)
pressKeyRect = pressKeySurf.get_rect()
pressKeyRect.topleft = (WINDOWWIDTH - 200, WINDOWHEIGHT - 30)
DISPLAYSURF.blit(pressKeySurf, pressKeyRect)
def checkForKeyPress():
if len(pygame.event.get(QUIT)) > 0:
terminate()
keyUpEvents = pygame.event.get(KEYUP)
if len(keyUpEvents) == 0:
return None
if keyUpEvents[0].key == K_ESCAPE:
terminate()
return keyUpEvents[0].key
def showStartScreen():
titleFont = pygame.font.Font('freesansbold.ttf', 100)
titleSurf1 = titleFont.render('Wormy!', True, WHITE, DARKGREEN)
titleSurf2 = titleFont.render('Wormy!', True, GREEN)
degrees1 = 0
degrees2 = 0
while True:
DISPLAYSURF.fill(BGCOLOR)
rotatedSurf1 = pygame.transform.rotate(titleSurf1, degrees1)
rotatedRect1 = rotatedSurf1.get_rect()
rotatedRect1.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
DISPLAYSURF.blit(rotatedSurf1, rotatedRect1)
rotatedSurf2 = pygame.transform.rotate(titleSurf2, degrees2)
rotatedRect2 = rotatedSurf2.get_rect()
rotatedRect2.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
DISPLAYSURF.blit(rotatedSurf2, rotatedRect2)
drawPressKeyMsg()
if checkForKeyPress():
pygame.event.get() # clear event queue
return
pygame.display.update()
FPSCLOCK.tick(FPS)
degrees1 += 3 # rotate by 3 degrees each frame
degrees2 += 7 # rotate by 7 degrees each frame
def terminate():
pygame.quit()
sys.exit()
def getRandomLocation():
return {'x': random.randint(0, CELLWIDTH - 1), 'y': random.randint(0, CELLHEIGHT - 1)}
def showGameOverScreen():
gameOverFont = pygame.font.Font('freesansbold.ttf', 150)
gameSurf = gameOverFont.render('Game', True, WHITE)
overSurf = gameOverFont.render('Over', True, WHITE)
gameRect = gameSurf.get_rect()
overRect = overSurf.get_rect()
gameRect.midtop = (WINDOWWIDTH / 2, 10)
overRect.midtop = (WINDOWWIDTH / 2, gameRect.height + 10 + 25)
DISPLAYSURF.blit(gameSurf, gameRect)
DISPLAYSURF.blit(overSurf, overRect)
drawPressKeyMsg()
pygame.display.update()
pygame.time.wait(500)
checkForKeyPress() # clear out any key presses in the event queue
while True:
if checkForKeyPress():
pygame.event.get() # clear event queue
return
def drawScore(score):
scoreSurf = BASICFONT.render('Score: %s' % (score), True, WHITE)
scoreRect = scoreSurf.get_rect()
scoreRect.topleft = (WINDOWWIDTH - 120, 10)
DISPLAYSURF.blit(scoreSurf, scoreRect)
def drawWorm(wormCoords):
for coord in wormCoords:
x = coord['x'] * CELLSIZE
y = coord['y'] * CELLSIZE
wormSegmentRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
pygame.draw.rect(DISPLAYSURF, DARKGREEN, wormSegmentRect)
wormInnerSegmentRect = pygame.Rect(x + 4, y + 4, CELLSIZE - 8, CELLSIZE - 8)
pygame.draw.rect(DISPLAYSURF, GREEN, wormInnerSegmentRect)
def drawApple(coord):
x = coord['x'] * CELLSIZE
y = coord['y'] * CELLSIZE
appleRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
pygame.draw.rect(DISPLAYSURF, RED, appleRect)
def drawGrid():
for x in range(0, WINDOWWIDTH, CELLSIZE): # draw vertical lines
pygame.draw.line(DISPLAYSURF, DARKGRAY, (x, 0), (x, WINDOWHEIGHT))
for y in range(0, WINDOWHEIGHT, CELLSIZE): # draw horizontal lines
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, y), (WINDOWWIDTH, y))
if __name__ == '__main__':
main()
网格
如果你玩游戏一点点,你会注意到苹果和蠕虫身体的部分总是沿着网格线。我们将这个网格中的每个正方形称为一个单元格(这不一定是网格中的空间的称呼,这只是我想出来的一个名字)。这些单元格有自己的笛卡尔坐标系,其中(0, 0)是左上角的单元格,(31, 23)是右下角的单元格。
设置代码
# Wormy (a Nibbles clone)
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US
import random, pygame, sys
from pygame.locals import *
FPS = 15
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
CELLSIZE = 20
assert WINDOWWIDTH % CELLSIZE == 0, "Window width must be a multiple of cell size."
assert WINDOWHEIGHT % CELLSIZE == 0, "Window height must be a multiple of cell size."
CELLWIDTH = int(WINDOWWIDTH / CELLSIZE)
CELLHEIGHT = int(WINDOWHEIGHT / CELLSIZE)
程序开始时的代码只是设置了游戏中使用的一些常量变量。单元格的宽度和高度存储在CELLSIZE
中。第 13 和 14 行的assert
语句确保单元格完全适合窗口。例如,如果CELLSIZE
为10
,并且WINDOWWIDTH
或WINDOWHEIGHT
常量设置为15
,那么只能容纳 1.5 个单元格。assert
语句确保窗口中只有整数个单元格。
# R G B
WHITE = (255, 255, 255)
BLACK = ( 0, 0, 0)
RED = (255, 0, 0)
GREEN = ( 0, 255, 0)
DARKGREEN = ( 0, 155, 0)
DARKGRAY = ( 40, 40, 40)
BGCOLOR = BLACK
UP = 'up'
DOWN = 'down'
LEFT = 'left'
RIGHT = 'right'
HEAD = 0 # syntactic sugar: index of the worm's head
在第 19 到 32 行设置了更多的常量。HEAD
常量将在本章后面解释。
main()
函数
def main():
global FPSCLOCK, DISPLAYSURF, BASICFONT
pygame.init()
FPSCLOCK = pygame.time.Clock()
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
BASICFONT = pygame.font.Font('freesansbold.ttf', 18)
pygame.display.set_caption('Wormy')
showStartScreen()
while True:
runGame()
showGameOverScreen()
在贪吃虫游戏程序中,我们将代码的主要部分放在一个名为runGame()
的函数中。这是因为我们只想在程序启动时显示“开始画面”(旋转的“贪吃虫”文本动画)一次(通过调用showStartScreen()
函数)。然后我们想调用runGame()
,这将开始一场贪吃虫游戏。当玩家的蠕虫撞到墙壁或自己并导致游戏结束时,此函数将返回。
在那时,我们将通过调用showGameOverScreen()
显示游戏结束画面。当该函数调用返回时,循环将返回到开始,并再次调用runGame()
。第 44 行的while
循环将一直循环,直到程序终止。
一个单独的runGame()
函数
def runGame():
# Set a random start point.
startx = random.randint(5, CELLWIDTH - 6)
starty = random.randint(5, CELLHEIGHT - 6)
wormCoords = [{'x': startx, 'y': starty},
{'x': startx - 1, 'y': starty},
{'x': startx - 2, 'y': starty}]
direction = RIGHT
# Start the apple in a random place.
apple = getRandomLocation()
在游戏开始时,我们希望蠕虫在随机位置开始(但不要太靠近棋盘的边缘),因此我们在startx
和starty
中存储一个随机坐标。(请记住,CELLWIDTH
和CELLHEIGHT
是窗口的宽度和高度,而不是像素的宽度和高度)。
蠕虫的身体将存储在一个字典值列表中。每个蠕虫身体段都将有一个字典值。该字典将具有'x'
和'y'
键,用于该身体段的 XY 坐标。身体的头部将位于startx
和starty
。其他两个身体段将位于头部的左侧一个和两个单元格。
蠕虫的头部将始终是wormCoords[0]
的身体部分。为了使这段代码更易读,我们在第 32 行将HEAD
常量设置为0
,这样我们就可以使用wormCoords[HEAD]
而不是wormCoords[0]
。
事件处理循环
while True: # main game loop
for event in pygame.event.get(): # event handling loop
if event.type == QUIT:
terminate()
elif event.type == KEYDOWN:
if (event.key == K_LEFT or event.key == K_a) and direction != RIGHT:
direction = LEFT
elif (event.key == K_RIGHT or event.key == K_d) and direction != LEFT:
direction = RIGHT
elif (event.key == K_UP or event.key == K_w) and direction != DOWN:
direction = UP
elif (event.key == K_DOWN or event.key == K_s) and direction != UP:
direction = DOWN
elif event.key == K_ESCAPE:
terminate()
第 61 行是主游戏循环的开始,第 62 行是事件处理循环的开始。如果事件是QUIT
事件,那么我们调用terminate()
(我们已经在之前的游戏程序中定义了相同的terminate()
函数)。
否则,如果事件是KEYDOWN
事件,那么我们检查按下的键是否是箭头键或者 WASD 键。我们希望进行额外的检查,以防蛇转向自身。例如,如果蛇正在向左移动,那么如果玩家意外按下右箭头键,蛇就会立即向右移动并撞到自己。
这就是为什么我们要检查direction
变量的当前值。这样,如果玩家意外按下一个会导致蛇立即撞到墙壁的箭头键,我们就忽略那个按键。
碰撞检测
# check if the worm has hit itself or the edge
if wormCoords[HEAD]['x'] == -1 or wormCoords[HEAD]['x'] == CELLWIDTH or wormCoords[HEAD]['y'] == -1 or wormCoords[HEAD]['y'] == CELLHEIGHT:
return # game over
for wormBody in wormCoords[1:]:
if wormBody['x'] == wormCoords[HEAD]['x'] and wormBody['y'] == wormCoords[HEAD]['y']:
return # game over
当蛇头移出网格边缘或者蛇头移动到已经被其他身体段占据的单元格时,蛇就撞到了。
我们可以通过检查蛇头是否移出了网格的边缘来判断。方法是看蛇头的 X 坐标(存储在wormCoords[HEAD]['x']
中)是否为-1
(超出了网格的左边缘)或者等于CELLWIDTH
(超出了右边缘,因为最右边的 X 坐标比CELLWIDTH
少 1)。
如果蛇头的 Y 坐标(存储在wormCoords[HEAD]['y']
中)要么是-1
(超出了顶部边缘),要么是CELLHEIGHT
(超出了底部边缘),那么蛇头也已经移出了网格。
我们只需要在runGame()
中返回来结束当前游戏。当runGame()
返回到main()
中的函数调用时,runGame()
调用后的下一行(第 46 行)是调用showGameOverScreen()
,它会显示大大的“游戏结束”文字。这就是为什么我们在第 79 行有return
语句。
第 80 行循环遍历蛇头后的每个身体段在wormCoords
中(蛇头在索引0
)。这就是为什么for
循环迭代wormCoords[1:]
而不是只迭代wormCoords
。如果身体段的'x'
和'y'
值与蛇头的'x'
和'y'
相同,那么我们也通过在runGame()
函数中返回来结束游戏。
检测与苹果的碰撞
# check if worm has eaten an apply
if wormCoords[HEAD]['x'] == apple['x'] and wormCoords[HEAD]['y'] == apple['y']:
# don't remove worm's tail segment
apple = getRandomLocation() # set a new apple somewhere
else:
del wormCoords[-1] # remove worm's tail segment
我们对蛇头和苹果的 XY 坐标之间进行类似的碰撞检测。如果它们匹配,我们将苹果的坐标设置为一个随机的新位置(从getRandomLocation()
的返回值中获取)。
如果蛇头没有与苹果碰撞,那么我们删除wormCoords
列表中的最后一个身体段。记住,负整数索引从列表末尾开始计数。所以0
是列表中第一个项目的索引,1
是第二个项目的索引,-1
是列表中的最后一个项目的索引,-2
是倒数第二个项目的索引。
第 91 到 100 行的代码(在“移动蛇”部分中描述)将根据蛇的移动方向在wormCoords
中添加一个新的身体段(用于蛇头)。这将使蛇变长一个段。当蛇吃掉苹果时不删除最后一个身体段,蛇的整体长度增加了一个。但是当第 89 行删除最后一个身体段时,大小保持不变,因为紧接着会添加一个新的蛇头段。
移动蛇
# move the worm by adding a segment in the direction it is moving
if direction == UP:
newHead = {'x': wormCoords[HEAD]['x'], 'y': wormCoords[HEAD]['y'] - 1}
elif direction == DOWN:
newHead = {'x': wormCoords[HEAD]['x'], 'y': wormCoords[HEAD]['y'] + 1}
elif direction == LEFT:
newHead = {'x': wormCoords[HEAD]['x'] - 1, 'y': wormCoords[HEAD]['y']}
elif direction == RIGHT:
newHead = {'x': wormCoords[HEAD]['x'] + 1, 'y': wormCoords[HEAD]['y']}
wormCoords.insert(0, newHead)
为了移动蛇,我们在wormCoords
列表的开头添加一个新的身体段。因为身体段被添加到列表的开头,它将成为新的蛇头。新蛇头的坐标将紧邻旧蛇头的坐标。无论是向 X 坐标还是 Y 坐标添加还是减去1
取决于蛇的移动方向。
新的蛇头段被添加到wormCoords
中,使用insert()
列表方法在第 100 行。
insert()
列表方法
与append()
列表方法只能在列表末尾添加项目不同,insert()
列表方法可以在列表中的任何位置添加项目。insert()
的第一个参数是项目应该放置的索引(原本在这个索引及之后的所有项目的索引都会增加一)。如果第一个参数传递的参数大于列表的长度,项目将被简单地添加到列表的末尾(就像append()
一样)。insert()
的第二个参数是要添加的项目值。在交互式 shell 中输入以下内容,看看insert()
是如何工作的:
>>> spam = ['cat', 'dog', 'bat']
>>> spam.insert(0, 'frog')
>>> spam
['frog', 'cat', 'dog', 'bat']
>>> spam.insert(10, 42)
>>> spam
['frog', 'cat', 'dog', 'bat', 42]
>>> spam.insert(2, 'horse')
>>> spam
['frog', 'cat', 'horse', 'dog', 'bat', 42]
>>>
绘制屏幕
DISPLAYSURF.fill(BGCOLOR)
drawGrid()
drawWorm(wormCoords)
drawApple(apple)
drawScore(len(wormCoords) - 3)
pygame.display.update()
FPSCLOCK.tick(FPS)
在runGame()
函数中绘制屏幕的代码非常简单。第 101 行填充整个显示 Surface 的背景颜色。第 102 到 105 行绘制了网格、蠕虫、苹果和分数到显示 Surface 上。然后调用pygame.display.update()
将显示 Surface 绘制到实际的计算机屏幕上。
将“按键开始”文本绘制到屏幕上
def drawPressKeyMsg():
pressKeySurf = BASICFONT.render('Press a key to play.', True, DARKGRAY)
pressKeyRect = pressKeySurf.get_rect()
pressKeyRect.topleft = (WINDOWWIDTH - 200, WINDOWHEIGHT - 30)
DISPLAYSURF.blit(pressKeySurf, pressKeyRect)
当开始屏幕动画正在播放或游戏结束屏幕正在显示时,右下角会有一些小文本,上面写着“按键开始游戏”。我们不想在showStartScreen()
和showGameOverScreen()
中重复代码,所以我们将它放在一个单独的函数中,并从showStartScreen()
和showGameOverScreen()
中调用该函数。
checkForKeyPress()
函数
def checkForKeyPress():
if len(pygame.event.get(QUIT)) > 0:
terminate()
keyUpEvents = pygame.event.get(KEYUP)
if len(keyUpEvents) == 0:
return None
if keyUpEvents[0].key == K_ESCAPE:
terminate()
return keyUpEvents[0].key
这个函数首先检查事件队列中是否有任何QUIT
事件。第 117 行的pygame.event.get()
调用返回事件队列中所有QUIT
事件的列表(因为我们将QUIT
作为参数传递)。如果事件队列中没有QUIT
事件,那么pygame.event.get()
返回的列表将是空列表:[]
第 117 行的len()
调用将在pygame.event.get()
返回空列表时返回0
。如果pygame.event.get()
返回的列表中有多于零个项目(记住,这个列表中的任何项目只会是QUIT
事件,因为我们将QUIT
作为参数传递给pygame.event.get()
),那么第 118 行将调用terminate()
函数,程序将终止。
之后,调用pygame.event.get()
获取事件队列中的任何KEYUP
事件的列表。如果按键事件是 Esc 键的话,那么程序也会在这种情况下终止。否则,checkForKeyPress()
函数将从pygame.event.get()
返回的列表中返回第一个按键事件对象。
开始屏幕
def showStartScreen():
titleFont = pygame.font.Font('freesansbold.ttf', 100)
titleSurf1 = titleFont.render('Wormy!', True, WHITE, DARKGREEN)
titleSurf2 = titleFont.render('Wormy!', True, GREEN)
degrees1 = 0
degrees2 = 0
while True:
DISPLAYSURF.fill(BGCOLOR)
当贪吃虫游戏程序首次运行时,玩家不会自动开始游戏。相反,会出现一个开始屏幕,告诉玩家他们正在运行的程序是什么。开始屏幕还给玩家一个准备游戏开始的机会(否则玩家可能不会准备好,在第一局游戏中就会失败)。
贪吃虫开始屏幕需要两个 Surface 对象,上面绘制了“Wormy!”文本。这是render()
方法在第 130 和 131 行创建的。文本将会很大:第 129 行的Font()
构造函数调用创建了一个大小为 100 点的 Font 对象。第一个“Wormy!”文本将是白色文本,带有深绿色背景,另一个将是绿色文本,带有透明背景。
第 135 行开始了开始屏幕的动画循环。在这个动画期间,两个文本将被旋转并绘制到显示 Surface 对象上。
旋转开始屏幕文本
rotatedSurf1 = pygame.transform.rotate(titleSurf1, degrees1)
rotatedRect1 = rotatedSurf1.get_rect()
rotatedRect1.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
DISPLAYSURF.blit(rotatedSurf1, rotatedRect1)
rotatedSurf2 = pygame.transform.rotate(titleSurf2, degrees2)
rotatedRect2 = rotatedSurf2.get_rect()
rotatedRect2.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
DISPLAYSURF.blit(rotatedSurf2, rotatedRect2)
drawPressKeyMsg()
if checkForKeyPress():
pygame.event.get() # clear event queue
return
pygame.display.update()
FPSCLOCK.tick(FPS)
showStartScreen()
函数将旋转 Surface 对象上的图像。第一个参数是要制作旋转副本的 Surface 对象。第二个参数是要旋转 Surface 的角度。pygame.transform.rotate()
函数不会改变你传递给它的 Surface 对象,而是返回一个新的 Surface 对象,上面绘制了旋转后的图像。
请注意,这个新的 Surface 对象可能会比原来的大,因为所有 Surface 对象都代表矩形区域,旋转后的 Surface 的角会超出原始 Surface 的宽度和高度。下面的图片中有一个黑色矩形以及一个略微旋转的版本。为了制作一个可以容纳旋转后的矩形的 Surface 对象(在下面的图片中是灰色的),它必须比原始黑色矩形的 Surface 对象大:
你旋转的角度以度为单位,这是一个旋转的度量。一个圆有 360 度。完全不旋转是 0 度。逆时针旋转一四分之一是 90 度。要顺时针旋转,传递一个负整数。旋转 360 度是将图像一直旋转,这意味着最终你得到的图像与旋转 0 度时的图像相同。事实上,如果你传递给pygame.transform.rotate()
的旋转参数是 360 或更大,那么 Pygame 会自动从中减去 360,直到得到一个小于 360 的数字。这张图片展示了不同旋转角度的几个例子:
两个旋转后的“Wormy!” Surface 对象在动画循环的每一帧上都被 blitted 到显示 Surface 上的第 140 和 145 行。
在第 147 行,drawPressKeyMsg()
函数调用在显示 Surface 对象的下角绘制“按键开始游戏。”的文本。这个动画循环会一直循环,直到checkForKeyPress()
返回一个不是None
的值,这会在玩家按下一个键时发生。在返回之前,pygame.event.get()
被调用来清除在显示开始画面时在事件队列中积累的任何其他事件。
旋转不完美
你可能会想为什么我们将旋转后的 Surface 存储在一个单独的变量中,而不是只覆盖titleSurf1
和titleSurf2
变量。有两个原因。
首先,旋转 2D 图像永远不是完全完美的。旋转后的图像总是近似的。如果你将图像逆时针旋转 10 度,然后再顺时针旋转 10 度,你得到的图像将不是你最初开始的完全相同的图像。可以把它想象成制作一份复印件,然后再复印第一份复印件,再复印另一份复印件。如果你一直这样做,图像会越来越糟糕,因为轻微的扭曲会累积起来。
(唯一的例外是如果你将图像旋转 90 度的倍数,比如 0、90、180、270 或 360 度。在这种情况下,像素可以旋转而不会出现任何失真。)
其次,如果你旋转一个 2D 图像,那么旋转后的图像会比原始图像稍微大一些。如果你旋转了旋转后的图像,那么下一个旋转后的图像将再次稍微变大。如果你一直这样做,最终图像将变得太大,Pygame 无法处理,你的程序将崩溃并显示错误消息,pygame.error: Width or height is too large。
degrees1 += 3 # rotate by 3 degrees each frame
degrees2 += 7 # rotate by 7 degrees each frame
我们旋转两个“Wormy!”文本 Surface 对象的角度存储在degrees1
和degrees2
中。在每次动画循环迭代中,我们将degrees1
中存储的数字增加3
,degrees2
增加7
。这意味着在下一次动画循环迭代中,白色文本“Wormy!” Surface 对象将再次旋转 3 度,绿色文本“Wormy!” Surface 对象将再次旋转 7 度。这就是为什么一个 Surface 对象旋转得比另一个慢。
def terminate():
pygame.quit()
sys.exit()
terminate()
函数调用pygame.quit()
和sys.exit()
以正确关闭游戏。它与之前游戏程序中的terminate()
函数相同。
决定苹果出现的位置
def getRandomLocation():
return {'x': random.randint(0, CELLWIDTH - 1), 'y': random.randint(0, CELLHEIGHT - 1)}
每当需要苹果的新坐标时,都会调用getRandomLocation()
函数。该函数返回一个带有键'x'
和'y'
的字典,其值设置为随机的 XY 坐标。
游戏结束屏幕
def showGameOverScreen():
gameOverFont = pygame.font.Font('freesansbold.ttf', 150)
gameSurf = gameOverFont.render('Game', True, WHITE)
overSurf = gameOverFont.render('Over', True, WHITE)
gameRect = gameSurf.get_rect()
overRect = overSurf.get_rect()
gameRect.midtop = (WINDOWWIDTH / 2, 10)
overRect.midtop = (WINDOWWIDTH / 2, gameRect.height + 10 + 25)
DISPLAYSURF.blit(gameSurf, gameRect)
DISPLAYSURF.blit(overSurf, overRect)
drawPressKeyMsg()
pygame.display.update()
游戏结束屏幕与开始屏幕类似,只是没有动画。单词“Game”和“Over”被渲染到两个 Surface 对象上,然后绘制在屏幕上。
pygame.time.wait(500)
checkForKeyPress() # clear out any key presses in the event queue
while True:
if checkForKeyPress():
pygame.event.get() # clear event queue
return
游戏结束文本将一直显示在屏幕上,直到玩家按下键。为了确保玩家不会意外地按下键,我们将在第 180 行调用pygame.time.wait()
来暂停半秒钟。(500 参数代表 500 毫秒的暂停,即半秒钟。)
然后,调用checkForKeyPress()
,以便忽略自showGameOverScreen()
函数开始以来产生的任何按键事件。这种暂停和丢弃按键事件是为了防止以下情况发生:假设玩家试图在最后一刻转向屏幕边缘,但按键太晚按下并撞到了棋盘的边缘。如果发生这种情况,那么按键按下将会在showGameOverScreen()
被调用之后发生,那个按键按下会导致游戏结束屏幕几乎立即消失。接下来的游戏会立即开始,并可能让玩家感到惊讶。添加这个暂停有助于使游戏更加“用户友好”。
绘图函数
绘制分数、蠕虫、苹果和网格的代码都放入了单独的函数中。
def drawScore(score):
scoreSurf = BASICFONT.render('Score: %s' % (score), True, WHITE)
scoreRect = scoreSurf.get_rect()
scoreRect.topleft = (WINDOWWIDTH - 120, 10)
DISPLAYSURF.blit(scoreSurf, scoreRect)
drawScore()
函数只是在显示 Surface 对象上渲染和绘制传入其score
参数的分数文本。
def drawWorm(wormCoords):
for coord in wormCoords:
x = coord['x'] * CELLSIZE
y = coord['y'] * CELLSIZE
wormSegmentRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
pygame.draw.rect(DISPLAYSURF, DARKGREEN, wormSegmentRect)
wormInnerSegmentRect = pygame.Rect(x + 4, y + 4, CELLSIZE - 8, CELLSIZE - 8)
pygame.draw.rect(DISPLAYSURF, GREEN, wormInnerSegmentRect)
drawWorm()
函数将为蠕虫身体的每个部分绘制一个绿色框。这些部分被传递到wormCoords
参数中,这是一个带有'x'
键和'y'
键的字典列表。第 196 行的for
循环遍历wormCoords
中的每个字典值。
因为网格坐标占据整个窗口并且从 0,0 像素开始,所以很容易从网格坐标转换为像素坐标。第 197 和 198 行简单地将coord['x']
和coord['y']
坐标乘以CELLSIZE
。
第 199 行创建了一个蠕虫段的 Rect 对象,该对象将传递给第 200 行的pygame.draw.rect()
函数。请记住,网格中的每个单元格的宽度和高度都是CELLSIZE
,因此段的 Rect 对象的大小应该是这样的。第 200 行为段绘制了一个深绿色的矩形。然后在此之上,绘制了一个较小的明亮绿色矩形。这使得蠕虫看起来更漂亮一些。
内部明亮的绿色矩形从单元格的左上角开始向右和向下各 4 个像素。该矩形的宽度和高度比单元格尺寸小 8 个像素,因此右侧和底部也会有 4 个像素的边距。
def drawApple(coord):
x = coord['x'] * CELLSIZE
y = coord['y'] * CELLSIZE
appleRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
pygame.draw.rect(DISPLAYSURF, RED, appleRect)
drawApple()
函数与drawWorm()
非常相似,只是因为红苹果只是填满单元格的一个矩形,所以函数需要做的就是转换为像素坐标(这就是第 206 和 207 行所做的),使用苹果的位置和大小创建 Rect 对象(第 208 行),然后将这个 Rect 对象传递给pygame.draw.rect()
函数。
def drawGrid():
for x in range(0, WINDOWWIDTH, CELLSIZE): # draw vertical lines
pygame.draw.line(DISPLAYSURF, DARKGRAY, (x, 0), (x, WINDOWHEIGHT))
for y in range(0, WINDOWHEIGHT, CELLSIZE): # draw horizontal lines
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, y), (WINDOWWIDTH, y))
为了更容易地可视化单元格的网格,我们调用pygame.draw.line()
来绘制网格的每条垂直和水平线。
通常,要绘制所需的 32 条垂直线,我们需要调用 32 次pygame.draw.line()
,坐标如下:
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 0), (0, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (20, 0), (20, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (40, 0), (40, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (60, 0), (60, WINDOWHEIGHT))
...skipped for brevity...
pygame.draw.line(DISPLAYSURF, DARKGRAY, (560, 0), (560, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (580, 0), (580, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (600, 0), (600, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (620, 0), (620, WINDOWHEIGHT))
我们可以在for
循环内只有一行代码,而不是输入所有这些代码行。注意垂直线的模式是,起点和终点的 X 坐标从0
开始,增加到620
,每次增加20
。Y 坐标始终为起点0
和终点参数WINDOWHEIGHT
。这意味着for
循环应该迭代range(0, 640, 20)
。这就是为什么 213 行的for
循环迭代range(0, WINDOWWIDTH, CELLSIZE)
。
对于水平线,坐标将是:
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 0), (WINDOWWIDTH, 0))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 20), (WINDOWWIDTH, 20))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 40), (WINDOWWIDTH, 40))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 60), (WINDOWWIDTH, 60))
...skipped for brevity...
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 400), (WINDOWWIDTH, 400))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 420), (WINDOWWIDTH, 420))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 440), (WINDOWWIDTH, 440))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 460), (WINDOWWIDTH, 460))
Y 坐标范围从0
到460
,每次增加20
。X 坐标始终为起点0
和终点参数WINDOWWIDTH
。我们也可以在这里使用for
循环,这样我们就不必输入所有这些pygame.draw.line()
调用。
注意到调用所需的规律模式并使用循环是聪明的程序员的技巧,可以帮助我们节省大量的输入。我们本可以输入所有 56 个pygame.draw.line()
调用,程序仍然可以正常工作。但通过稍微聪明一点,我们可以节省很多工作。
if __name__ == '__main__':
main()
在所有函数、常量和全局变量都被定义和创建之后,调用main()
函数来启动游戏。
不要重复使用变量名
再次看一下drawWorm()
函数中的一些代码行:
wormSegmentRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
pygame.draw.rect(DISPLAYSURF, DARKGREEN, wormSegmentRect)
wormInnerSegmentRect = pygame.Rect(x + 4, y + 4, CELLSIZE - 8, CELLSIZE - 8)
pygame.draw.rect(DISPLAYSURF, GREEN, wormInnerSegmentRect)
注意到 199 行和 201 行分别创建了两个不同的 Rect 对象。199 行创建的 Rect 对象存储在wormSegmentRect
局部变量中,并传递给 200 行的pygame.draw.rect()
函数。201 行创建的 Rect 对象存储在wormInnerSegmentRect
局部变量中,并传递给 202 行的pygame.draw.rect()
函数。
每次创建一个变量,都会占用计算机的一小部分内存。你可能会认为重用wormSegmentRect
变量来存储两个 Rect 对象是很聪明的,就像这样:
wormSegmentRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
pygame.draw.rect(DISPLAYSURF, DARKGREEN, wormSegmentRect)
wormSegmentRect = pygame.Rect(x + 4, y + 4, CELLSIZE - 8, CELLSIZE - 8)
pygame.draw.rect(DISPLAYSURF, GREEN, wormInnerSegmentRect)
因为 199 行的pygame.Rect()
返回的 Rect 对象在 200 行后不再需要,我们可以覆盖这个值并重用变量来存储 201 行的pygame.Rect()
返回的 Rect 对象。由于我们现在使用的变量更少,我们节省了内存,对吗?
虽然这在技术上是正确的,但你真的只是节省了一点内存。现代计算机的内存有数十亿字节。所以节省并不是那么大。与此同时,重用变量会降低代码的可读性。如果一个程序员在编写后阅读这段代码,他们会看到wormSegmentRect
被传递给 200 行和 202 行的pygame.draw.rect()
调用。如果他们试图找到第一次给wormSegmentRect
变量赋值的地方,他们会看到 199 行的pygame.Rect()
调用。他们可能没有意识到 199 行的pygame.Rect()
调用返回的 Rect 对象与 202 行的pygame.draw.rect()
调用中传递的对象不同。
像这样的小事情会使你更难理解你的程序是如何工作的。不仅仅是其他程序员看你的代码会感到困惑。当你在写完几周后再看你自己的代码时,你可能会很难记住它是如何工作的。代码的可读性比在这里和那里节省一些内存更重要。
对于额外的编程练习,你可以从invpy.com/buggy/wormy
下载贪吃虫的有 bug 版本,并尝试弄清楚如何修复这些 bug。
第七章:俄罗斯方块
原文:
inventwithpython.com/pygame/chapter7.html
译者:飞龙
怎么玩俄罗斯方块
俄罗斯方块是俄罗斯方块的克隆。不同形状的方块(每个由四个方块组成)从屏幕顶部掉落,玩家必须引导它们下落,形成没有间隙的完整行。当形成完整的一行时,该行消失,上面的每一行都向下移动一行。玩家试图保持形成完整的行,直到屏幕填满,新的下落方块无法适应屏幕。
一些俄罗斯方块术语
在这一章中,我已经为游戏程序中的不同事物想出了一组术语。
-
板 - 板由 10 x 20 个空间组成,方块会落下并堆叠起来。
-
方块 - 方块是板上的单个填充的正方形空间。
-
方块 - 从板的顶部掉落并且玩家可以旋转和定位的东西。每个方块都有一个形状,由 4 个方块组成。
-
形状 - 形状是游戏中不同类型的方块。形状的名称是 T、S、Z、J、L、I 和 O。
-
模板 - 一组形状数据结构的列表,表示了一个形状的所有可能的旋转。这些存储在变量中,名称如
S_SHAPE_TEMPLATE
或J_SHAPE_TEMPLATE
。 -
着陆 - 当一个方块已经到达板的底部或者与板上的方块接触时,我们说这个方块已经着陆。在这一点上,下一个方块应该开始下落。
俄罗斯方块源代码
这个源代码可以从invpy.com/tetromino.py
下载。如果出现任何错误消息,请查看错误消息中提到的行号,并检查你的代码是否有任何拼写错误。你也可以将你的代码复制粘贴到invpy.com/diff/tetromino
的网页表单中,以查看你的代码与书中代码之间的差异。
你还需要将背景音乐文件放在与tetromino.py文件相同的文件夹中。你可以从这里下载它们:
# Tetromino (a Tetris clone)
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US
import random, time, pygame, sys
from pygame.locals import *
FPS = 25
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
BOXSIZE = 20
BOARDWIDTH = 10
BOARDHEIGHT = 20
BLANK = '.'
MOVESIDEWAYSFREQ = 0.15
MOVEDOWNFREQ = 0.1
XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2)
TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5
# R G B
WHITE = (255, 255, 255)
GRAY = (185, 185, 185)
BLACK = ( 0, 0, 0)
RED = (155, 0, 0)
LIGHTRED = (175, 20, 20)
GREEN = ( 0, 155, 0)
LIGHTGREEN = ( 20, 175, 20)
BLUE = ( 0, 0, 155)
LIGHTBLUE = ( 20, 20, 175)
YELLOW = (155, 155, 0)
LIGHTYELLOW = (175, 175, 20)
BORDERCOLOR = BLUE
BGCOLOR = BLACK
TEXTCOLOR = WHITE
TEXTSHADOWCOLOR = GRAY
COLORS = ( BLUE, GREEN, RED, YELLOW)
LIGHTCOLORS = (LIGHTBLUE, LIGHTGREEN, LIGHTRED, LIGHTYELLOW)
assert len(COLORS) == len(LIGHTCOLORS) # each color must have light color
TEMPLATEWIDTH = 5
TEMPLATEHEIGHT = 5
S_SHAPE_TEMPLATE = [['.....',
'.....',
'..OO.',
'.OO..',
'.....'],
['.....',
'..O..',
'..OO.',
'...O.',
'.....']]
Z_SHAPE_TEMPLATE = [['.....',
'.....',
'.OO..',
'..OO.',
'.....'],
['.....',
'..O..',
'.OO..',
'.O...',
'.....']]
I_SHAPE_TEMPLATE = [['..O..',
'..O..',
'..O..',
'..O..',
'.....'],
['.....',
'.....',
'OOOO.',
'.....',
'.....']]
O_SHAPE_TEMPLATE = [['.....',
'.....',
'.OO..',
'.OO..',
'.....']]
J_SHAPE_TEMPLATE = [['.....',
'.O...',
'.OOO.',
'.....',
'.....'],
['.....',
'..OO.',
'..O..',
'..O..',
'.....'],
['.....',
'.....',
'.OOO.',
'...O.',
'.....'],
['.....',
'..O..',
'..O..',
'.OO..',
'.....']]
L_SHAPE_TEMPLATE = [['.....',
'...O.',
'.OOO.',
'.....',
'.....'],
['.....',
'..O..',
'..O..',
'..OO.',
'.....'],
['.....',
'.....',
'.OOO.',
'.O...',
'.....'],
['.....',
'.OO..',
'..O..',
'..O..',
'.....']]
T_SHAPE_TEMPLATE = [['.....',
'..O..',
'.OOO.',
'.....',
'.....'],
['.....',
'..O..',
'..OO.',
'..O..',
'.....'],
['.....',
'.....',
'.OOO.',
'..O..',
'.....'],
['.....',
'..O..',
'.OO..',
'..O..',
'.....']]
SHAPES = {'S': S_SHAPE_TEMPLATE,
'Z': Z_SHAPE_TEMPLATE,
'J': J_SHAPE_TEMPLATE,
'L': L_SHAPE_TEMPLATE,
'I': I_SHAPE_TEMPLATE,
'O': O_SHAPE_TEMPLATE,
'T': T_SHAPE_TEMPLATE}
def main():
global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT
pygame.init()
FPSCLOCK = pygame.time.Clock()
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
BASICFONT = pygame.font.Font('freesansbold.ttf', 18)
BIGFONT = pygame.font.Font('freesansbold.ttf', 100)
pygame.display.set_caption('Tetromino')
showTextScreen('Tetromino')
while True: # game loop
if random.randint(0, 1) == 0:
pygame.mixer.music.load('tetrisb.mid')
else:
pygame.mixer.music.load('tetrisc.mid')
pygame.mixer.music.play(-1, 0.0)
runGame()
pygame.mixer.music.stop()
showTextScreen('Game Over')
def runGame():
# setup variables for the start of the game
board = getBlankBoard()
lastMoveDownTime = time.time()
lastMoveSidewaysTime = time.time()
lastFallTime = time.time()
movingDown = False # note: there is no movingUp variable
movingLeft = False
movingRight = False
score = 0
level, fallFreq = calculateLevelAndFallFreq(score)
fallingPiece = getNewPiece()
nextPiece = getNewPiece()
while True: # main game loop
if fallingPiece == None:
# No falling piece in play, so start a new piece at the top
fallingPiece = nextPiece
nextPiece = getNewPiece()
lastFallTime = time.time() # reset lastFallTime
if not isValidPosition(board, fallingPiece):
return # can't fit a new piece on the board, so game over
checkForQuit()
for event in pygame.event.get(): # event handling loop
if event.type == KEYUP:
if (event.key == K_p):
# Pausing the game
DISPLAYSURF.fill(BGCOLOR)
pygame.mixer.music.stop()
showTextScreen('Paused') # pause until a key press
pygame.mixer.music.play(-1, 0.0)
lastFallTime = time.time()
lastMoveDownTime = time.time()
lastMoveSidewaysTime = time.time()
elif (event.key == K_LEFT or event.key == K_a):
movingLeft = False
elif (event.key == K_RIGHT or event.key == K_d):
movingRight = False
elif (event.key == K_DOWN or event.key == K_s):
movingDown = False
elif event.type == KEYDOWN:
# moving the block sideways
if (event.key == K_LEFT or event.key == K_a) and isValidPosition(board, fallingPiece, adjX=-1):
fallingPiece['x'] -= 1
movingLeft = True
movingRight = False
lastMoveSidewaysTime = time.time()
elif (event.key == K_RIGHT or event.key == K_d) and isValidPosition(board, fallingPiece, adjX=1):
fallingPiece['x'] += 1
movingRight = True
movingLeft = False
lastMoveSidewaysTime = time.time()
# rotating the block (if there is room to rotate)
elif (event.key == K_UP or event.key == K_w):
fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(SHAPES[fallingPiece['shape']])
if not isValidPosition(board, fallingPiece):
fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(SHAPES[fallingPiece['shape']])
elif (event.key == K_q): # rotate the other direction
fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(SHAPES[fallingPiece['shape']])
if not isValidPosition(board, fallingPiece):
fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(SHAPES[fallingPiece['shape']])
# making the block fall faster with the down key
elif (event.key == K_DOWN or event.key == K_s):
movingDown = True
if isValidPosition(board, fallingPiece, adjY=1):
fallingPiece['y'] += 1
lastMoveDownTime = time.time()
# move the current block all the way down
elif event.key == K_SPACE:
movingDown = False
movingLeft = False
movingRight = False
for i in range(1, BOARDHEIGHT):
if not isValidPosition(board, fallingPiece, adjY=i):
break
fallingPiece['y'] += i - 1
# handle moving the block because of user input
if (movingLeft or movingRight) and time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ:
if movingLeft and isValidPosition(board, fallingPiece, adjX=-1):
fallingPiece['x'] -= 1
elif movingRight and isValidPosition(board, fallingPiece, adjX=1):
fallingPiece['x'] += 1
lastMoveSidewaysTime = time.time()
if movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ and isValidPosition(board, fallingPiece, adjY=1):
fallingPiece['y'] += 1
lastMoveDownTime = time.time()
# let the piece fall if it is time to fall
if time.time() - lastFallTime > fallFreq:
# see if the piece has landed
if not isValidPosition(board, fallingPiece, adjY=1):
# falling piece has landed, set it on the board
addToBoard(board, fallingPiece)
score += removeCompleteLines(board)
level, fallFreq = calculateLevelAndFallFreq(score)
fallingPiece = None
else:
# piece did not land, just move the block down
fallingPiece['y'] += 1
lastFallTime = time.time()
# drawing everything on the screen
DISPLAYSURF.fill(BGCOLOR)
drawBoard(board)
drawStatus(score, level)
drawNextPiece(nextPiece)
if fallingPiece != None:
drawPiece(fallingPiece)
pygame.display.update()
FPSCLOCK.tick(FPS)
def makeTextObjs(text, font, color):
surf = font.render(text, True, color)
return surf, surf.get_rect()
def terminate():
pygame.quit()
sys.exit()
def checkForKeyPress():
# Go through event queue looking for a KEYUP event.
# Grab KEYDOWN events to remove them from the event queue.
checkForQuit()
for event in pygame.event.get([KEYDOWN, KEYUP]):
if event.type == KEYDOWN:
continue
return event.key
return None
def showTextScreen(text):
# This function displays large text in the
# center of the screen until a key is pressed.
# Draw the text drop shadow
titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTSHADOWCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))
DISPLAYSURF.blit(titleSurf, titleRect)
# Draw the text
titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) - 3)
DISPLAYSURF.blit(titleSurf, titleRect)
# Draw the additional "Press a key to play." text.
pressKeySurf, pressKeyRect = makeTextObjs('Press a key to play.', BASICFONT, TEXTCOLOR)
pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 100)
DISPLAYSURF.blit(pressKeySurf, pressKeyRect)
while checkForKeyPress() == None:
pygame.display.update()
FPSCLOCK.tick()
def checkForQuit():
for event in pygame.event.get(QUIT): # get all the QUIT events
terminate() # terminate if any QUIT events are present
for event in pygame.event.get(KEYUP): # get all the KEYUP events
if event.key == K_ESCAPE:
terminate() # terminate if the KEYUP event was for the Esc key
pygame.event.post(event) # put the other KEYUP event objects back
def calculateLevelAndFallFreq(score):
# Based on the score, return the level the player is on and
# how many seconds pass until a falling piece falls one space.
level = int(score / 10) + 1
fallFreq = 0.27 - (level * 0.02)
return level, fallFreq
def getNewPiece():
# return a random new piece in a random rotation and color
shape = random.choice(list(SHAPES.keys()))
newPiece = {'shape': shape,
'rotation': random.randint(0, len(SHAPES[shape]) - 1),
'x': int(BOARDWIDTH / 2) - int(TEMPLATEWIDTH / 2),
'y': -2, # start it above the board (i.e. less than 0)
'color': random.randint(0, len(COLORS)-1)}
return newPiece
def addToBoard(board, piece):
# fill in the board based on piece's location, shape, and rotation
for x in range(TEMPLATEWIDTH):
for y in range(TEMPLATEHEIGHT):
if SHAPES[piece['shape']][piece['rotation']][y][x] != BLANK:
board[x + piece['x']][y + piece['y']] = piece['color']
def getBlankBoard():
# create and return a new blank board data structure
board = []
for i in range(BOARDWIDTH):
board.append([BLANK] * BOARDHEIGHT)
return board
def isOnBoard(x, y):
return x >= 0 and x < BOARDWIDTH and y < BOARDHEIGHT
def isValidPosition(board, piece, adjX=0, adjY=0):
# Return True if the piece is within the board and not colliding
for x in range(TEMPLATEWIDTH):
for y in range(TEMPLATEHEIGHT):
isAboveBoard = y + piece['y'] + adjY < 0
if isAboveBoard or SHAPES[piece['shape']][piece['rotation']][y][x] == BLANK:
continue
if not isOnBoard(x + piece['x'] + adjX, y + piece['y'] + adjY):
return False
if board[x + piece['x'] + adjX][y + piece['y'] + adjY] != BLANK:
return False
return True
def isCompleteLine(board, y):
# Return True if the line filled with boxes with no gaps.
for x in range(BOARDWIDTH):
if board[x][y] == BLANK:
return False
return True
def removeCompleteLines(board):
# Remove any completed lines on the board, move everything above them down, and return the number of complete lines.
numLinesRemoved = 0
y = BOARDHEIGHT - 1 # start y at the bottom of the board
while y >= 0:
if isCompleteLine(board, y):
# Remove the line and pull boxes down by one line.
for pullDownY in range(y, 0, -1):
for x in range(BOARDWIDTH):
board[x][pullDownY] = board[x][pullDownY-1]
# Set very top line to blank.
for x in range(BOARDWIDTH):
board[x][0] = BLANK
numLinesRemoved += 1
# Note on the next iteration of the loop, y is the same.
# This is so that if the line that was pulled down is also
# complete, it will be removed.
else:
y -= 1 # move on to check next row up
return numLinesRemoved
def convertToPixelCoords(boxx, boxy):
# Convert the given xy coordinates of the board to xy
# coordinates of the location on the screen.
return (XMARGIN + (boxx * BOXSIZE)), (TOPMARGIN + (boxy * BOXSIZE))
def drawBox(boxx, boxy, color, pixelx=None, pixely=None):
# draw a single box (each tetromino piece has four boxes)
# at xy coordinates on the board. Or, if pixelx & pixely
# are specified, draw to the pixel coordinates stored in
# pixelx & pixely (this is used for the "Next" piece).
if color == BLANK:
return
if pixelx == None and pixely == None:
pixelx, pixely = convertToPixelCoords(boxx, boxy)
pygame.draw.rect(DISPLAYSURF, COLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 1, BOXSIZE - 1))
pygame.draw.rect(DISPLAYSURF, LIGHTCOLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 4, BOXSIZE - 4))
def drawBoard(board):
# draw the border around the board
pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (XMARGIN - 3, TOPMARGIN - 7, (BOARDWIDTH * BOXSIZE) + 8, (BOARDHEIGHT * BOXSIZE) + 8), 5)
# fill the background of the board
pygame.draw.rect(DISPLAYSURF, BGCOLOR, (XMARGIN, TOPMARGIN, BOXSIZE * BOARDWIDTH, BOXSIZE * BOARDHEIGHT))
# draw the individual boxes on the board
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT):
drawBox(x, y, board[x][y])
def drawStatus(score, level):
# draw the score text
scoreSurf = BASICFONT.render('Score: %s' % score, True, TEXTCOLOR)
scoreRect = scoreSurf.get_rect()
scoreRect.topleft = (WINDOWWIDTH - 150, 20)
DISPLAYSURF.blit(scoreSurf, scoreRect)
# draw the level text
levelSurf = BASICFONT.render('Level: %s' % level, True, TEXTCOLOR)
levelRect = levelSurf.get_rect()
levelRect.topleft = (WINDOWWIDTH - 150, 50)
DISPLAYSURF.blit(levelSurf, levelRect)
def drawPiece(piece, pixelx=None, pixely=None):
shapeToDraw = SHAPES[piece['shape']][piece['rotation']]
if pixelx == None and pixely == None:
# if pixelx & pixely hasn't been specified, use the location stored in the piece data structure
pixelx, pixely = convertToPixelCoords(piece['x'], piece['y'])
# draw each of the blocks that make up the piece
for x in range(TEMPLATEWIDTH):
for y in range(TEMPLATEHEIGHT):
if shapeToDraw[y][x] != BLANK:
drawBox(None, None, piece['color'], pixelx + (x * BOXSIZE), pixely + (y * BOXSIZE))
def drawNextPiece(piece):
# draw the "next" text
nextSurf = BASICFONT.render('Next:', True, TEXTCOLOR)
nextRect = nextSurf.get_rect()
nextRect.topleft = (WINDOWWIDTH - 120, 80)
DISPLAYSURF.blit(nextSurf, nextRect)
# draw the "next" piece
drawPiece(piece, pixelx=WINDOWWIDTH-120, pixely=100)
if __name__ == '__main__':
main()
通常的设置代码
# Tetromino (a Tetris clone)
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US
import random, time, pygame, sys
from pygame.locals import *
FPS = 25
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
BOXSIZE = 20
BOARDWIDTH = 10
BOARDHEIGHT = 20
BLANK = '.'
这些是我们俄罗斯方块游戏使用的常量。每个方块都是一个 20 像素宽和高的正方形。板本身是 10 个方块宽和 20 个方块高。BLANK
常量将被用作代表板数据结构中的空白空间的值。
设置按住键的时间常量
MOVESIDEWAYSFREQ = 0.15
MOVEDOWNFREQ = 0.1
每当玩家按下左或右箭头键时,下落的方块应该向左或向右移动一个方块。然而,玩家也可以按住左或右箭头键来持续移动下落的方块。MOVESIDEWAYSFREQ
常量将设置为每 0.15 秒按住左或右箭头键,方块将再移动一个空间。
MOVEDOWNFREQ
常量也是同样的东西,它告诉玩家按住下箭头键时方块下落的频率。
更多设置代码
XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2)
TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5
程序需要计算板的左右两侧有多少像素,以便在程序的后面使用。WINDOWWIDTH
是整个窗口的总宽度。板宽BOARDWIDTH
个方块,每个方块宽BOXSIZE
像素。如果我们从这个值中减去每个方块宽的BOXSIZE
像素(即BOARDWIDTH * BOXSIZE
),我们将得到板左右两侧的边距大小。如果我们将这个值除以2
,那么我们将得到一个边距的大小。由于边距的大小相同,我们可以用XMARGIN
来表示左侧或右侧的边距。
我们可以以类似的方式计算出棋盘顶部和窗口顶部之间的空间大小。棋盘将在窗口底部上方 5 像素处绘制,因此从topmargin
中减去5
来解决这个问题。
# R G B
WHITE = (255, 255, 255)
GRAY = (185, 185, 185)
BLACK = ( 0, 0, 0)
RED = (155, 0, 0)
LIGHTRED = (175, 20, 20)
GREEN = ( 0, 155, 0)
LIGHTGREEN = ( 20, 175, 20)
BLUE = ( 0, 0, 155)
LIGHTBLUE = ( 20, 20, 175)
YELLOW = (155, 155, 0)
LIGHTYELLOW = (175, 175, 20)
BORDERCOLOR = BLUE
BGCOLOR = BLACK
TEXTCOLOR = WHITE
TEXTSHADOWCOLOR = GRAY
COLORS = ( BLUE, GREEN, RED, YELLOW)
LIGHTCOLORS = (LIGHTBLUE, LIGHTGREEN, LIGHTRED, LIGHTYELLOW)
assert len(COLORS) == len(LIGHTCOLORS) # each color must have light color
这些块将有四种颜色:蓝色、绿色、红色和黄色。当我们画盒子时,盒子上会有浅色的细节。这意味着我们还需要创建浅蓝色、浅绿色、浅红色和浅黄色。
这四种颜色将存储在名为COLORS
(用于正常颜色)和LIGHTCOLORS
(用于浅色)的元组中。
设置块模板
TEMPLATEWIDTH = 5
TEMPLATEHEIGHT = 5
S_SHAPE_TEMPLATE = [['.....',
'.....',
'..OO.',
'.OO..',
'.....'],
['.....',
'..O..',
'..OO.',
'...O.',
'.....']]
Z_SHAPE_TEMPLATE = [['.....',
'.....',
'.OO..',
'..OO.',
'.....'],
['.....',
'..O..',
'.OO..',
'.O...',
'.....']]
I_SHAPE_TEMPLATE = [['..O..',
'..O..',
'..O..',
'..O..',
'.....'],
['.....',
'.....',
'OOOO.',
'.....',
'.....']]
O_SHAPE_TEMPLATE = [['.....',
'.....',
'.OO..',
'.OO..',
'.....']]
J_SHAPE_TEMPLATE = [['.....',
'.O...',
'.OOO.',
'.....',
'.....'],
['.....',
'..OO.',
'..O..',
'..O..',
'.....'],
['.....',
'.....',
'.OOO.',
'...O.',
'.....'],
['.....',
'..O..',
'..O..',
'.OO..',
'.....']]
L_SHAPE_TEMPLATE = [['.....',
'...O.',
'.OOO.',
'.....',
'.....'],
['.....',
'..O..',
'..O..',
'..OO.',
'.....'],
['.....',
'.....',
'.OOO.',
'.O...',
'.....'],
['.....',
'.OO..',
'..O..',
'..O..',
'.....']]
T_SHAPE_TEMPLATE = [['.....',
'..O..',
'.OOO.',
'.....',
'.....'],
['.....',
'..O..',
'..OO.',
'..O..',
'.....'],
['.....',
'.....',
'.OOO.',
'..O..',
'.....'],
['.....',
'..O..',
'.OO..',
'..O..',
'.....']]
我们的游戏程序需要知道每个形状的形状,包括它们所有可能的旋转。为了做到这一点,我们将创建字符串的列表的列表。字符串的内部列表将表示形状的单个旋转,就像这样:
['.....',
'.....',
'..OO.',
'.OO..',
'.....']
我们将编写剩下的代码,以便它解释像上面那样的字符串列表来表示形状,其中句点是空格,O 是盒子,就像这样:
将“代码行”跨多行拆分
您可以看到这个列表在文件编辑器中跨越了许多行。这是完全有效的 Python,因为 Python 解释器意识到在看到}
关闭方括号之前,列表还没有完成。缩进不重要,因为 Python 知道在列表中间不会有不同缩进的新块。下面的代码可以正常工作:
spam = ['hello', 3.14, 'world', 42, 10, 'fuzz']
eggs = ['hello', 3.14,
'world'
, 42,
10, 'fuzz']
当然,如果我们将列表中的所有项目排成一行,或者像spam
一样放在一行上,那么eggs
列表的代码将更易读。
通常,在文件编辑器中将一行代码跨多行拆分需要在行尾放置一个\
字符。\
告诉 Python,“这段代码继续到下一行。”(这个斜杠最初是在isValidMove()
函数中的滑动拼图游戏中使用的。)
我们将通过创建这些字符串列表的列表来制作形状的“模板”数据结构,并将它们存储在变量中,比如S_SHAPE_TEMPLATE
。这样,len(S_SHAPE_TEMPLATE)
将表示 S 形状的可能旋转数,S_SHAPE_TEMPLATE[0]
将表示 S 形状的第一个可能旋转。第 47 至 147 行将为每个形状创建“模板”数据结构。
想象一下,在一个小的 5x5 的空白空间板上有可能的块,板上的一些空间填满了盒子。使用S_SHAPE_TEMPLATE[0]
的以下表达式是True
:
S_SHAPE_TEMPLATE[0][2][2] == 'O'
S_SHAPE_TEMPLATE[0][2][3] == 'O'
S_SHAPE_TEMPLATE[0][3][1] == 'O'
S_SHAPE_TEMPLATE[0][3][2] == 'O'
如果我们在纸上表示这个形状,它会看起来像这样:
这是我们如何将 Tetromino 块之类的东西表示为 Python 值,比如字符串和列表。TEMPLATEWIDTH
和TEMPLATEHEIGHT
常量只是设置每个形状旋转的每行和列的大小。 (模板始终为 5x5。)
SHAPES = {'S': S_SHAPE_TEMPLATE,
'Z': Z_SHAPE_TEMPLATE,
'J': J_SHAPE_TEMPLATE,
'L': L_SHAPE_TEMPLATE,
'I': I_SHAPE_TEMPLATE,
'O': O_SHAPE_TEMPLATE,
'T': T_SHAPE_TEMPLATE}
SHAPES
变量将是一个存储所有不同模板的字典。因为每个模板都有单个形状的所有可能旋转,这意味着SHAPES
变量包含每个可能形状的所有可能旋转。这将是包含我们游戏中所有形状数据的数据结构。
main()
函数
def main():
global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT
pygame.init()
FPSCLOCK = pygame.time.Clock()
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
BASICFONT = pygame.font.Font('freesansbold.ttf', 18)
BIGFONT = pygame.font.Font('freesansbold.ttf', 100)
pygame.display.set_caption('Tetromino')
showTextScreen('Tetromino')
main()
函数处理创建一些更多的全局常量,并显示程序运行时出现的开始屏幕。
while True: # game loop
if random.randint(0, 1) == 0:
pygame.mixer.music.load('tetrisb.mid')
else:
pygame.mixer.music.load('tetrisc.mid')
pygame.mixer.music.play(-1, 0.0)
runGame()
pygame.mixer.music.stop()
showTextScreen('Game Over')
实际游戏的代码都在runGame()
中。这里的main()
函数只是随机决定要开始播放什么背景音乐(tetrisb.mid或tetrisc.mid MIDI 音乐文件),然后调用runGame()
开始游戏。当玩家失败时,runGame()
将返回到main()
,然后停止背景音乐并显示游戏结束画面。
当玩家按下键时,显示游戏结束屏幕的showTextScreen()
函数将返回。游戏循环将在第 169 行回到开头,开始另一场游戏。
新游戏的开始
def runGame():
# setup variables for the start of the game
board = getBlankBoard()
lastMoveDownTime = time.time()
lastMoveSidewaysTime = time.time()
lastFallTime = time.time()
movingDown = False # note: there is no movingUp variable
movingLeft = False
movingRight = False
score = 0
level, fallFreq = calculateLevelAndFallFreq(score)
fallingPiece = getNewPiece()
nextPiece = getNewPiece()
在游戏开始之前,棋子开始下落之前,我们需要将一些变量初始化为游戏开始时的值。在第 191 行,fallingPiece
变量将被设置为当前正在下落的可以由玩家旋转的棋子。在第 192 行,nextPiece
变量将被设置为出现在屏幕“下一个”部分的棋子,以便玩家知道在设置下落棋子后下一个棋子是什么。
游戏循环
while True: # main game loop
if fallingPiece == None:
# No falling piece in play, so start a new piece at the top
fallingPiece = nextPiece
nextPiece = getNewPiece()
lastFallTime = time.time() # reset lastFallTime
if not isValidPosition(board, fallingPiece):
return # can't fit a new piece on the board, so game over
checkForQuit()
从第 194 行开始的主游戏循环处理游戏主要部分的所有代码,当棋子下落到底部时。在下落棋子着陆后,fallingPiece
变量被设置为None
。这意味着nextPiece
中的棋子应该被复制到fallingPiece
变量中,并且应该将一个随机的新棋子放入nextPiece
变量中。可以从getNewPiece()
函数生成一个新棋子。lastFallTime
变量也被重置为当前时间,以便棋子将在fallFreq
中的秒数内下落。
getNewPiece()
得到的棋子通常会被放置在板子的上方一点点,通常部分棋子已经在板子上。但是,如果这是一个无效的位置,因为板子已经填满了(在这种情况下,第 201 行的isValidPosition()
调用将返回False
),那么我们知道板子已经满了,玩家应该输掉游戏。当这种情况发生时,runGame()
函数将返回。
事件处理循环
for event in pygame.event.get(): # event handling loop
if event.type == KEYUP:
事件处理循环负责玩家旋转下落棋子、移动下落棋子或暂停游戏时的情况。
暂停游戏
if (event.key == K_p):
# Pausing the game
DISPLAYSURF.fill(BGCOLOR)
pygame.mixer.music.stop()
showTextScreen('Paused') # pause until a key press
pygame.mixer.music.play(-1, 0.0)
lastFallTime = time.time()
lastMoveDownTime = time.time()
lastMoveSidewaysTime = time.time()
如果玩家按下 P 键,则游戏应该暂停。我们需要隐藏板子,否则玩家可以通过暂停游戏并花时间决定移动棋子的位置来作弊。
代码通过调用DISPLAYSURF.fill(BGCOLOR)
来清空显示表面,并停止音乐。调用showTextScreen()
函数显示“暂停”文本,并等待玩家按键继续。
一旦玩家按下键,showTextScreen()
将返回。第 212 行将重新开始背景音乐。此外,由于玩家暂停游戏后可能已经过了很长时间,因此lastFallTime
、lastMoveDownTime
和lastMoveSidewaysTime
变量都应该被重置为当前时间(这在第 213 到 215 行完成)。
使用移动变量处理用户输入
elif (event.key == K_LEFT or event.key == K_a):
movingLeft = False
elif (event.key == K_RIGHT or event.key == K_d):
movingRight = False
elif (event.key == K_DOWN or event.key == K_s):
movingDown = False
松开箭头键(或 WASD 键)将把movingLeft
、movingRight
或movingDown
变量设置回False
,表示玩家不再想朝这些方向移动棋子。稍后的代码将根据这些“移动”变量内的布尔值来处理。请注意,上箭头和 W 键用于旋转棋子,而不是向上移动棋子。这就是为什么没有movingUp
变量。
检查幻灯片或旋转是否有效
elif event.type == KEYDOWN:
# moving the block sideways
if (event.key == K_LEFT or event.key == K_a) and isValidPosition(board, fallingPiece, adjX=-1):
fallingPiece['x'] -= 1
movingLeft = True
movingRight = False
lastMoveSidewaysTime = time.time()
当按下左箭头键(并且向左移动是下落棋子的有效移动,由isValidPosition()
调用确定)时,我们应该通过将fallingPiece['x']
的值减去1
来将位置改变为左边一个空格。isValidPosition()
函数有名为adjX
和adjY
的可选参数。通常,isValidPosition()
函数检查由第二个参数传递的棋子对象提供的位置。然而,有时我们不想检查棋子当前所在的位置,而是在该位置的几个空格之外。
如果我们传入-1
作为adjX
(“调整 X”的简称),那么它不会检查方块数据结构中位置的有效性,而是检查方块向左移动一个空格后的位置。传入1
作为adjX
将检查向右移动一个空格的位置。还有一个adjY
可选参数。传入-1
作为adjY
将检查方块当前位置上方一个空格的位置,传入像3
这样的值作为adjY
将检查方块下方三个空格的位置。
将movingLeft
变量设置为True
,并且为了确保下落的方块不会同时向左和向右移动,将movingRight
变量在第 228 行设置为False
。lastMoveSidewaysTime
变量将在第 229 行更新为当前时间。
这些变量设置使玩家可以按住箭头键不断移动方块。如果movingLeft
变量设置为True
,程序就会知道左箭头键(或 A 键)已经被按下但尚未松开。如果从lastMoveSidewaysTime
存储的时间开始已经过去了 0.15 秒(存储在MOVESIDEWAYSFREQ
中的数字),那么程序就该再次将下落的方块向左移动。
lastMoveSidewaysTime
的工作方式就像模拟章节中的lastClickTime
变量一样。
elif (event.key == K_RIGHT or event.key == K_d) and isValidPosition(board, fallingPiece, adjX=1):
fallingPiece['x'] += 1
movingRight = True
movingLeft = False
lastMoveSidewaysTime = time.time()
第 231 到 235 行的代码几乎与第 225 到 229 行相同,只是处理了当按下右箭头键(或 D 键)时将下落的方块向右移动的情况。
# rotating the block (if there is room to rotate)
elif (event.key == K_UP or event.key == K_w):
fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(SHAPES[fallingPiece['shape']])
按上箭头键(或 W 键)将会将下落的方块旋转到下一个位置。所有代码需要做的就是将fallingPiece
字典中的'rotation'
键的值增加1
。但是,如果增加'rotation'
键的值使其大于总旋转次数,那么“取模”总可能的旋转次数(即len(SHAPES[fallingPiece['shape']]
)),它将“回滚”到0
。
以下是 J 形状的取模示例,它有 4 种可能的旋转:
>>> 0 % 4
0
>>> 1 % 4
1
>>> 2 % 4
2
>>> 3 % 4
3
>>> 5 % 4
1
>>> 6 % 4
2
>>> 7 % 4
3
>>> 8 % 4
0
>>>
if not isValidPosition(board, fallingPiece):
fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(SHAPES[fallingPiece['shape']])
如果新旋转位置无效,因为它与棋盘上已有的一些方块重叠,那么我们希望通过从fallingPiece['rotation']
中减去1
来将其切换回原始旋转。我们还可以对len(SHAPES[fallingPiece['shape']])
取模,以便如果新值为-1
,取模将把它改回列表中的最后一个旋转。以下是对负数进行取模的示例:
>>> -1 % 4
3
elif (event.key == K_q): # rotate the other direction
fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(SHAPES[fallingPiece['shape']])
if not isValidPosition(board, fallingPiece):
fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(SHAPES[fallingPiece['shape']])
第 242 到 245 行与 238 到 241 行的代码做了相同的事情,只是处理了玩家按下 Q 键旋转方块的情况,这时我们需要从fallingPiece['rotation']
中减去1
(在第 243 行完成),而不是加上1
。
# making the block fall faster with the down key
elif (event.key == K_DOWN or event.key == K_s):
movingDown = True
if isValidPosition(board, fallingPiece, adjY=1):
fallingPiece['y'] += 1
lastMoveDownTime = time.time()
如果按下下箭头或 S 键,玩家希望方块下落速度比正常速度更快。第 251 行将方块在棋盘上向下移动一个空格(但仅当它是有效的空格时)。movingDown
变量设置为True
,lastMoveDownTime
重置为当前时间。稍后将检查这些变量,以便只要按住下箭头或 S 键,方块就会以更快的速度下落。
寻找底部
# move the current block all the way down
elif event.key == K_SPACE:
movingDown = False
movingLeft = False
movingRight = False
for i in range(1, BOARDHEIGHT):
if not isValidPosition(board, fallingPiece, adjY=i):
break
fallingPiece['y'] += i - 1
当玩家按下空格键时,下落的方块将立即下落到棋盘上的最低处并停下。程序首先需要找出方块可以移动多少个空格直到停下。
第 256 到 258 行将所有移动变量设置为False
(这样后续的代码会认为用户已经松开了按住的任何箭头键)。这是因为这段代码将把方块移动到绝对底部并开始下一个方块的下落,我们不希望玩家因为按住箭头键而在按下空格键时立即开始移动这些方块而感到惊讶。
找到零件可以掉落的最远距离,我们首先应该调用isValidPosition()
,并为adjY
参数传递整数1
。如果isValidPosition()
返回False
,我们就知道零件无法再下落,已经到达底部了。如果isValidPosition()
返回True
,那么我们就知道它可以再往下落1
格。
在这种情况下,我们应该将adjY
设置为2
调用isValidPosition()
。如果它再次返回True
,我们将使用3
设置adjY
调用isValidPosition()
,依此类推。这就是第 259 行的for
循环处理的:使用递增的整数值调用isValidPosition()
传递给adjY
,直到函数调用返回False
。在那时,我们就知道 i 的值比底部多了一个空格。这就是为什么第 262 行将fallingPiece['y']
增加i - 1
而不是i
。
(还要注意,第 259 行for
语句中range()
的第二个参数设置为BOARDHEIGHT
,因为这是方块在必须触底之前可以下落的最大距离。)
通过按住键移动
# handle moving the block because of user input
if (movingLeft or movingRight) and time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ:
if movingLeft and isValidPosition(board, fallingPiece, adjX=-1):
fallingPiece['x'] -= 1
elif movingRight and isValidPosition(board, fallingPiece, adjX=1):
fallingPiece['x'] += 1
lastMoveSidewaysTime = time.time()
记住,在第 227 行,如果玩家按下左箭头键,movingLeft
变量被设置为True
?(在第 233 行,如果玩家按下右箭头键,movingRight
也被设置为True。)如果用户松开这些键,移动变量也会被设置回
False`(见第 217 行和 219 行)。
当玩家按下左或右箭头键时,lastMoveSidewaysTime
变量也被设置为当前时间(即time.time()
的返回值)。如果玩家继续按住箭头键不放,那么movingLeft
或movingRight
变量仍然会被设置为True
。
如果用户按住键超过 0.15 秒(MOVESIDEWAYSFREQ
中存储的值是浮点数0.15
),那么表达式time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ
会评估为True
。如果用户既按住箭头键又过了 0.15 秒,第 265 行的条件就会为True
,在这种情况下,我们应该将下落的方块向左或向右移动,即使用户没有再次按下箭头键。
这非常有用,因为玩家要让下落的方块在棋盘上移动多个空格,反复按箭头键会很烦人。相反,他们可以按住箭头键,方块会一直移动,直到他们松开键。当发生这种情况时,第 216 行到 221 行的代码会将移动变量设置为False
,第 265 行的条件也会变为False
。这就阻止了下落的方块继续滑动。
为了演示为什么time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ
在MOVESIDEWAYSFREQ
秒数过去后返回True
,运行这个简短的程序:
import time
WAITTIME = 4
begin = time.time()
while True:
now = time.time()
message = '%s, %s, %s' % (begin, now, (now - begin))
if now - begin > WAITTIME:
print(message + ' PASSED WAIT TIME!')
else:
print(message + ' Not yet...')
time.sleep(0.2)
这个程序有一个无限循环,为了终止它,按 Ctrl-C。这个程序的输出看起来会像这样:
1322106392.2, 1322106392.2, 0.0 Not yet...
1322106392.2, 1322106392.42, 0.219000101089 Not yet...
1322106392.2, 1322106392.65, 0.449000120163 Not yet...
1322106392.2, 1322106392.88, 0.680999994278 Not yet...
1322106392.2, 1322106393.11, 0.910000085831 Not yet...
1322106392.2, 1322106393.34, 1.1400001049 Not yet...
1322106392.2, 1322106393.57, 1.3710000515 Not yet...
1322106392.2, 1322106393.83, 1.6360001564 Not yet...
1322106392.2, 1322106394.05, 1.85199999809 Not yet...
1322106392.2, 1322106394.28, 2.08000016212 Not yet...
1322106392.2, 1322106394.51, 2.30900001526 Not yet...
1322106392.2, 1322106394.74, 2.54100012779 Not yet...
1322106392.2, 1322106394.97, 2.76999998093 Not yet...
1322106392.2, 1322106395.2, 2.99800014496 Not yet...
1322106392.2, 1322106395.42, 3.22699999809 Not yet...
1322106392.2, 1322106395.65, 3.45600008965 Not yet...
1322106392.2, 1322106395.89, 3.69200015068 Not yet...
1322106392.2, 1322106396.12, 3.92100000381 Not yet...
1322106392.2, 1322106396.35, 4.14899992943 PASSED WAIT TIME!
1322106392.2, 1322106396.58, 4.3789999485 PASSED WAIT TIME!
1322106392.2, 1322106396.81, 4.60700011253 PASSED WAIT TIME!
1322106392.2, 1322106397.04, 4.83700013161 PASSED WAIT TIME!
1322106392.2, 1322106397.26, 5.06500005722 PASSED WAIT TIME!
Traceback (most recent call last):
File "C:\timetest.py", line 13, in <module>
time.sleep(0.2)
KeyboardInterrupt
输出的每行的第一个数字是程序开始时time.time()
的返回值(这个值永远不会改变)。第二个数字是time.time()
的最新返回值(这个值在每次循环迭代时都会更新)。第三个数字是当前时间减去开始时间。这第三个数字是自begin = time.time()
代码执行以来经过的秒数。
如果这个数字大于 4,代码将开始打印PASSED WAIT TIME!
而不是Not yet...
。这就是我们的游戏程序如何知道自上次运行代码以来经过了一定的时间。
在我们的俄罗斯方块程序中,time.time() - lastMoveSidewaysTime
表达式将计算自上次lastMoveSidewaysTime
设置为当前时间以来经过的秒数。如果这个值大于MOVESIDEWAYSFREQ
中的值,我们就知道是时候让代码将下落的方块再移动一个空间了。
不要忘记将lastMoveSidewaysTime
更新为当前时间!这是我们在第 270 行做的事情。
if movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ and isValidPosition(board, fallingPiece, adjY=1):
fallingPiece['y'] += 1
lastMoveDownTime = time.time()
第 272 到 274 行几乎与第 265 到 270 行做的事情相同,只是将下落的方块向下移动。这有一个单独的移动变量(movingDown
)和“上次”变量(lastMoveDownTime
),以及一个不同的“移动频率”变量(MOVEDOWNFREQ
)。
让方块“自然”下落
# let the piece fall if it is time to fall
if time.time() - lastFallTime > fallFreq:
# see if the piece has landed
if not isValidPosition(board, fallingPiece, adjY=1):
# falling piece has landed, set it on the board
addToBoard(board, fallingPiece)
score += removeCompleteLines(board)
level, fallFreq = calculateLevelAndFallFreq(score)
fallingPiece = None
else:
# piece did not land, just move the block down
fallingPiece['y'] += 1
lastFallTime = time.time()
方块自然下落的速度由lastFallTime
变量跟踪。如果自上次下落一格以来已经过了足够长的时间,第 279 到 288 行将处理将方块下落一格。
如果第 279 行的条件为True
,则表示方块已经落地。调用addToBoard()
将使方块成为棋盘数据结构的一部分(以便未来的方块可以落在上面),而removeCompleteLines()
调用将处理擦除棋盘上的任何完整行并将方块下拉。removeCompleteLines()
函数还返回一个整数值,表示移除了多少行,因此我们将这个数字加到分数上。
因为分数可能已经改变,我们调用calculateLevelAndFallFreq()
函数来更新当前级别和方块下落的频率。最后,我们将fallingPiece
变量设置为None
,表示下一个方块应该成为新的下落方块,并且应该为新的下一个方块生成一个随机的新方块。(这是在游戏循环的开头的第 195 到 199 行完成的。)
如果方块还没有落地,我们只需将其 Y 位置向下移动一个空间(在第 287 行),并将lastFallTime
重置为当前时间(在第 288 行)。
在屏幕上绘制一切
# drawing everything on the screen
DISPLAYSURF.fill(BGCOLOR)
drawBoard(board)
drawStatus(score, level)
drawNextPiece(nextPiece)
if fallingPiece != None:
drawPiece(fallingPiece)
pygame.display.update()
FPSCLOCK.tick(FPS)
现在游戏循环已经处理了所有事件并更新了游戏状态,游戏循环只需要将游戏状态绘制到屏幕上。大部分绘制工作由其他函数处理,因此游戏循环代码只需要调用这些函数。然后调用pygame.display.update()
使显示表面出现在实际的计算机屏幕上,tick()
方法调用会添加一个轻微的暂停,以防游戏运行得太快。
makeTextObjs()
,一个制作文本的快捷函数
def makeTextObjs(text, font, color):
surf = font.render(text, True, color)
return surf, surf.get_rect()
makeTextObjs()
函数只是为我们提供了一个快捷方式。给定文本、字体对象和颜色对象,它为我们调用render()
并返回这个文本的 Surface 和 Rect 对象。这样就省去了我们每次需要它们时编写创建 Surface 和 Rect 对象的代码。
相同的terminate()
函数
def terminate():
pygame.quit()
sys.exit()
terminate()
函数与以前的游戏程序中的工作方式相同。
使用checkForKeyPress()
函数等待按键事件
def checkForKeyPress():
# Go through event queue looking for a KEYUP event.
# Grab KEYDOWN events to remove them from the event queue.
checkForQuit()
for event in pygame.event.get([KEYDOWN, KEYUP]):
if event.type == KEYDOWN:
continue
return event.key
return None
checkForKeyPress()
函数的工作方式几乎与贪吃虫游戏中的工作方式相同。首先它调用checkForQuit()
来处理任何QUIT
事件(或者专门用于 Esc 键的KEYUP
事件),如果有的话就终止程序。然后它从事件队列中提取所有的KEYUP
和KEYDOWN
事件。它忽略任何KEYDOWN
事件(KEYDOWN
只被指定给pygame.event.get()
以清除事件队列中的这些事件)。
如果事件队列中没有KEYUP
事件,则该函数返回None
。
showTextScreen()
,一个通用的文本屏幕函数
def showTextScreen(text):
# This function displays large text in the
# center of the screen until a key is pressed.
# Draw the text drop shadow
titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTSHADOWCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))
DISPLAYSURF.blit(titleSurf, titleRect)
# Draw the text
titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) - 3)
DISPLAYSURF.blit(titleSurf, titleRect)
# Draw the additional "Press a key to play." text.
pressKeySurf, pressKeyRect = makeTextObjs('Press a key to play.', BASICFONT, TEXTCOLOR)
pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 100)
DISPLAYSURF.blit(pressKeySurf, pressKeyRect)
我们将创建一个名为showTextScreen()
的通用函数,而不是为开始屏幕和游戏结束屏幕创建单独的函数。showTextScreen()
函数将绘制我们传递给文本参数的任何文本。此外,文本“按键开始游戏。”也将被显示。
请注意,第 328 至 330 行首先用较暗的阴影颜色绘制文本,然后第 333 至 335 行再次绘制相同的文本,但向左偏移 3 个像素,向上偏移 3 个像素。这会产生一个“投影”效果,使文本看起来更漂亮。您可以通过注释掉第 328 至 330 行来比较差异,以查看没有投影的文本。
showTextScreen()
将用于开始屏幕、游戏结束屏幕,以及暂停屏幕(暂停屏幕在本章后面解释)。
while checkForKeyPress() == None:
pygame.display.update()
FPSCLOCK.tick()
我们希望文本保持在屏幕上,直到用户按下键。这个小循环将不断调用pygame.display.update()
和FPSCLOCK.tick()
,直到checkForKeyPress()
返回一个非None
的值。当用户按下键时,这种情况就会发生。
checkForQuit()
函数
def checkForQuit():
for event in pygame.event.get(QUIT): # get all the QUIT events
terminate() # terminate if any QUIT events are present
for event in pygame.event.get(KEYUP): # get all the KEYUP events
if event.key == K_ESCAPE:
terminate() # terminate if the KEYUP event was for the Esc key
pygame.event.post(event) # put the other KEYUP event objects back
checkForQuit()
函数可用于处理任何导致程序终止的事件。如果事件队列中有任何QUIT
事件(由第 348 和 349 行处理),或者按下 Esc 键的KEYUP
事件,则会发生这种情况。玩家应该能够随时按下 Esc 键退出程序。
因为第 350 行的pygame.event.get()
调用会提取所有的KEYUP
事件(包括不是 Esc 键的键的事件),如果事件不是针对 Esc 键的,我们希望通过调用pygame.event.post()
函数将其放回事件队列中。
calculateLevelAndFallFreq()
函数
def calculateLevelAndFallFreq(score):
# Based on the score, return the level the player is on and
# how many seconds pass until a falling piece falls one space.
level = int(score / 10) + 1
fallFreq = 0.27 - (level * 0.02)
return level, fallFreq
每当玩家完成一行时,他们的分数将增加一分。每增加十分,游戏就会升一级,方块下落速度也会加快。游戏的级别和下落频率都可以根据传递给此函数的分数进行计算。
计算级别时,我们使用int()
函数将分数除以10
后向下取整。因此,如果分数在0
和9
之间,int()
调用将将其舍入为0
。代码中的+ 1
部分是因为我们希望第一个级别是级别 1,而不是级别 0。当分数达到10
时,int(10 / 10)
将计算为 1,+ 1
将使级别为 2。下面是一个图表,显示了分数为 1 到 34 时的级别值:
为了计算下落频率,我们从基本时间0.27
开始(这意味着方块自然下落一次需要 0.27 秒)。然后我们将级别乘以0.02
,并从0.27
的基本时间中减去。因此,在第 1 级,我们从0.27
减去0.02 * 1
(即0.02
)得到0.25
。在第 2 级,我们从0.27
减去0.02 * 2
(即0.04
)得到0.23
。您可以将方程中的级别* 0.02 部分看作“对于每个级别,方块下落速度比上一个级别快 0.02 秒”。
我们还可以制作一个图表,显示游戏每个级别下方块下落的速度:
您可以看到在第 14 级时,下落频率将小于0
。这不会导致我们的代码出现任何错误,因为第 277 行只是检查自上次下落一格以来经过的时间是否大于计算出的下落频率。因此,如果下落频率为负数,那么第 277 行的条件将始终为True
,方块将在游戏循环的每次迭代中下落。从第 14 级开始,方块将无法再下落得更快。
如果FPS
设置为25
,这意味着在达到第 14 级时,方块将以每秒 25 格的速度下落。考虑到游戏板只有 20 格高,这意味着玩家每次只有不到一秒的时间来放置每个方块!
如果您希望方块以较慢的速度开始(如果您明白我的意思)更快地下落,您可以更改calculateLevelAndFallFreq()
使用的方程。例如,假设第 360 行是这样的:
fallFreq = 0.27 - (level * 0.01)
在上述情况下,方块每个级别下落的速度只会比原来快 0.01 秒,而不是 0.02 秒。图表会是这样的(原始线条也在图表中以浅灰色显示):
如您所见,使用这个新方程,第 14 级的难度只会和原始的第 7 级一样难。您可以通过更改calculateLevelAndFallFreq()
中的方程来使游戏变得难或易。
使用getNewPiece()
函数生成方块
def getNewPiece():
# return a random new piece in a random rotation and color
shape = random.choice(list(SHAPES.keys()))
newPiece = {'shape': shape,
'rotation': random.randint(0, len(SHAPES[shape]) - 1),
'x': int(BOARDWIDTH / 2) - int(TEMPLATEWIDTH / 2),
'y': -2, # start it above the board (i.e. less than 0)
'color': random.randint(0, len(COLORS)-1)}
return newPiece
getNewPiece()
函数生成一个位于板顶部的随机方块。首先,为了随机选择方块的形状,我们通过在第 365 行调用list(SHAPES.keys())
来创建所有可能形状的列表。keys()
字典方法返回一个数据类型为dict_keys
的值,必须在传递给random.choice()
之前通过list()
函数转换为列表值。这是因为random.choice()
函数只接受列表值作为其参数。然后,random.choice()
函数随机返回列表中的一个项目的值。
方块数据结构只是一个带有键'shape'
、'rotation'
、'x'
、'y'
和'color'
的字典值。
'rotation'
键的值是一个介于0
到该形状可能的旋转数减 1 之间的随机整数。可以从表达式len(SHAPES[shape])
中找到形状的旋转数。
请注意,我们不会将字符串值的列表(比如存储在常量中的S_SHAPE_TEMPLATE
中的值)存储在每个方块数据结构中,以表示每个方块的盒子。相反,我们只存储一个形状和旋转的索引,这些索引指向PIECES
常量。
'x'
键的值始终设置为板的中间(还考虑到方块本身的宽度,这是从我们的TEMPLATEWIDTH
常量中找到的)。'y'
键的值始终设置为-2
,以使其略高于板。(板的顶行是第 0 行。)
由于COLORS
常量是不同颜色的元组,从0
到COLORS
的长度(减去 1)中选择一个随机数将为我们提供一个方块颜色的随机索引值。
一旦newPiece
字典中的所有值都设置好,getNewPiece()
函数就会返回newPiece
。
将方块添加到板数据结构
def addToBoard(board, piece):
# fill in the board based on piece's location, shape, and rotation
for x in range(TEMPLATEWIDTH):
for y in range(TEMPLATEHEIGHT):
if SHAPES[piece['shape']][piece['rotation']][y][x] != BLANK:
board[x + piece['x']][y + piece['y']] = piece['color']
板数据结构是一个矩形空间的数据表示,用于跟踪先前着陆的方块。当前下落的方块不会在板数据结构上标记。addToBoard()
函数的作用是获取一个方块数据结构,并将其盒子添加到板数据结构中。这是在方块着陆后发生的。
在第 376 和 377 行的嵌套for
循环遍历方块数据结构中的每个空间,如果在空间中找到一个盒子(第 378 行),则将其添加到板上(第 379 行)。
创建新的板数据结构
def getBlankBoard():
# create and return a new blank board data structure
board = []
for i in range(BOARDWIDTH):
board.append([BLANK] * BOARDHEIGHT)
return board
用于板的数据结构相当简单:它是一个值的列表的列表。如果值与BLANK
中的值相同,那么它就是一个空格。如果值是整数,那么它表示的是颜色,该整数在COLORS
常量列表中索引。也就是说,0
是蓝色,1
是绿色,2
是红色,3
是黄色。
为了创建一个空白板,使用列表复制来创建BLANK
值的列表,这代表一列。这是在第 386 行完成的。为板中的每一列创建一个这样的列表(这是第 385 行上的for
循环所做的)。
isOnBoard()
和isValidPosition()
函数
def isOnBoard(x, y):
return x >= 0 and x < BOARDWIDTH and y < BOARDHEIGHT
isOnBoard()
是一个简单的函数,它检查传递的 XY 坐标是否表示存在于板上的有效值。只要 XY 坐标都不小于0
或大于或等于BOARDWIDTH
和BOARDHEIGHT
常量,函数就会返回True
。
def isValidPosition(board, piece, adjX=0, adjY=0):
# Return True if the piece is within the board and not colliding
for x in range(TEMPLATEWIDTH):
for y in range(TEMPLATEHEIGHT):
isAboveBoard = y + piece['y'] + adjY < 0
if isAboveBoard or SHAPES[piece['shape']][piece['rotation']][y][x] == BLANK:
continue
isValidPosition()
函数接收一个板数据结构和一个方块数据结构,并在方块的所有盒子都在板上且不重叠时返回True
。这是通过取方块的 XY 坐标(实际上是方块的 5x5 盒子中右上角盒子的坐标)并添加方块数据结构内的坐标来完成的。以下是一些图片来帮助说明这一点:
处于有效位置的板。 | 处于无效位置的板。 |
---|---|
在左侧的板上,下落方块的(即下落方块的左上角)XY 坐标是(2,3)。但是下落方块坐标系内的盒子有它们自己的坐标。要找到这些盒子的“板”坐标,我们只需将下落方块左上角盒子的“板”坐标和盒子的“方块”坐标相加。
在左侧的板上,下落方块的盒子位于以下“方块”坐标:
(2,2)(3,2)(1,3)(2,3)
当我们将(2,3)坐标(方块在板上的坐标)添加到这些坐标时,看起来是这样的:
(2 + 2,2 + 3)(3 + 2,2 + 3)(1 + 2,3 + 3)(2 + 2,3 + 3)
在添加了(2,3)坐标之后,盒子位于以下“板”坐标:
(4,5)(5,5)(3,6)(4,6)
现在我们可以确定下落方块的盒子在板坐标上的位置,我们可以看看它们是否与已经在板上的盒子重叠。396 和 397 行上的嵌套for
循环遍历了下落方块的每个可能的坐标。
我们想要检查下落方块的盒子是否在板上或与板上的盒子重叠。(尽管有一个例外,即如果盒子在板上方,这是下落方块刚开始下落时可能出现的情况。)398 行创建了一个名为isAboveBoard
的变量,如果下落方块在由 x 和 y 指向的坐标处的盒子在板上方,则设置为True
。否则设置为False
。
399 行上的if
语句检查方块上的空间是否在板上方或为空白。如果其中任何一个为True
,则代码执行continue
语句并进入下一次迭代。(请注意,399 行的末尾是[y][x]
而不是[x][y]
。这是因为PIECES
数据结构中的坐标是颠倒的。请参阅前一节“设置方块模板”)。
if not isOnBoard(x + piece['x'] + adjX, y + piece['y'] + adjY):
return False
if board[x + piece['x'] + adjX][y + piece['y'] + adjY] != BLANK:
return False
return True
401 行上的if
语句检查方块是否位于板上。403 行上的if
语句检查方块所在的板空间是否为空白。如果这些条件中的任何一个为True
,则isValidPosition()
函数将返回False
。请注意,这些if
语句还会调整传递给函数的adjX
和adjY
参数的坐标。
如果代码通过嵌套的for
循环并且没有找到返回False
的原因,那么方块的位置必须是有效的,因此函数在 405 行返回True
。
检查并删除完整行
def isCompleteLine(board, y):
# Return True if the line filled with boxes with no gaps.
for x in range(BOARDWIDTH):
if board[x][y] == BLANK:
return False
return True
isCompleteLine
在由y
参数指定的行上进行了简单的检查。当板上的一行被认为是“完整”的时候,每个空间都被盒子填满。409 行上的for
循环遍历了行中的每个空间。如果空间为空白(这是由它具有与BLANK
常量相同的值引起的),则函数返回False
。
def removeCompleteLines(board):
# Remove any completed lines on the board, move everything above them down, and return the number of complete lines.
numLinesRemoved = 0
y = BOARDHEIGHT - 1 # start y at the bottom of the board
while y >= 0:
removeCompleteLines()
函数将在传递的板数据结构中查找任何完整的行,删除这些行,然后将板上的所有盒子向下移动一行。该函数将返回已删除的行数(由numLinesRemoved
变量跟踪),以便将其添加到得分中。
这个函数的工作方式是通过在循环中运行,从第 419 行开始,y
变量从最低行(即BOARDHEIGHT - 1
)开始。每当由y
指定的行不完整时,y
将递减到下一个更高的行。循环最终在y
达到-1
时停止。
if isCompleteLine(board, y):
# Remove the line and pull boxes down by one line.
for pullDownY in range(y, 0, -1):
for x in range(BOARDWIDTH):
board[x][pullDownY] = board[x][pullDownY-1]
# Set very top line to blank.
for x in range(BOARDWIDTH):
board[x][0] = BLANK
numLinesRemoved += 1
# Note on the next iteration of the loop, y is the same.
# This is so that if the line that was pulled down is also
# complete, it will be removed.
else:
y -= 1 # move on to check next row up
return numLinesRemoved
isCompleteLine()
函数将返回True
,如果y
所指的行是完整的。在这种情况下,程序需要将删除行上面的每一行的值复制到下一个更低的行。这就是第 422 行上的for
循环所做的事情(这就是为什么它调用range()
函数的起始位置是y
,而不是0
。还要注意它使用range()
的三个参数形式,所以它返回的列表从y
开始,到0
结束,并且在每次迭代后“增加”了-1
)。
让我们看下面的例子。为了节省空间,只显示了棋盘的前五行。第 3 行是一个完整的行,这意味着它上面的所有行(第 2、1 和 0 行)都必须被“拉下”。首先,第 2 行被复制到第 3 行。右边的棋盘显示了在完成此操作后棋盘的样子:
这种“下拉”实际上只是将更高行的值复制到下面的行上,即第 424 行。在将第 2 行复制到第 3 行后,然后将第 1 行复制到第 2 行,然后将第 0 行复制到第 1 行:
第 0 行(最顶部的行)没有上面的行可以复制值。但第 0 行不需要复制行,它只需要将所有空格设置为BLANK
。这就是第 426 和 427 行所做的事情。之后,棋盘将从左边下面显示的棋盘变为右边下面显示的棋盘:
在完整的行被移除后,执行到达了从第 419 行开始的while
循环的末尾,所以执行跳回到循环的开始。请注意,在删除行和下拉行时,y
变量根本没有改变。因此,在下一次迭代中,y
变量指向的仍然是之前的行。
这是必要的,因为如果有两行完整的行,那么第二行完整的行将被拉下来,也必须被移除。然后代码将删除这一行,然后进行下一次迭代。只有当没有完成的行时,y
变量才会在第 433 行递减。一旦y
变量被递减到0
,执行将退出while
循环。
从棋盘坐标转换为像素坐标
def convertToPixelCoords(boxx, boxy):
# Convert the given xy coordinates of the board to xy
# coordinates of the location on the screen.
return (XMARGIN + (boxx * BOXSIZE)), (TOPMARGIN + (boxy * BOXSIZE))
这个辅助函数将棋盘的方框坐标转换为像素坐标。这个函数与前面游戏程序中使用的其他“转换坐标”函数的工作方式相同。
在棋盘或屏幕上绘制一个方框
def drawBox(boxx, boxy, color, pixelx=None, pixely=None):
# draw a single box (each tetromino piece has four boxes)
# at xy coordinates on the board. Or, if pixelx & pixely
# are specified, draw to the pixel coordinates stored in
# pixelx & pixely (this is used for the "Next" piece).
if color == BLANK:
return
if pixelx == None and pixely == None:
pixelx, pixely = convertToPixelCoords(boxx, boxy)
pygame.draw.rect(DISPLAYSURF, COLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 1, BOXSIZE - 1))
pygame.draw.rect(DISPLAYSURF, LIGHTCOLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 4, BOXSIZE - 4))
drawBox()
函数在屏幕上绘制一个方框。该函数可以接收boxx
和boxy
参数,用于指定方框应该绘制的棋盘坐标。但是,如果指定了pixelx
和pixely
参数,则这些像素坐标将覆盖boxx
和boxy
参数。pixelx
和pixely
参数用于绘制“下一个”方块的方框,这个方块不在棋盘上。
如果pixelx
和pixely
参数没有设置,则在函数开始时它们将默认设置为None
。然后第 450 行上的if
语句将使用convertToPixelCoords()
的返回值覆盖None
值。这个调用获取由boxx
和boxy
指定的棋盘坐标的像素坐标。
代码不会用颜色填满整个方块的空间。为了在方块之间有黑色轮廓,pygame.draw.rect()
调用中的left
和top
参数会加上+1
,width
和height
参数会减去-1
。为了绘制高亮的方块,首先在第 452 行用较暗的颜色绘制方块。然后在第 453 行在较暗的方块上方绘制一个稍小的方块。
将所有内容绘制到屏幕上
def drawBoard(board):
# draw the border around the board
pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (XMARGIN - 3, TOPMARGIN - 7, (BOARDWIDTH * BOXSIZE) + 8, (BOARDHEIGHT * BOXSIZE) + 8), 5)
# fill the background of the board
pygame.draw.rect(DISPLAYSURF, BGCOLOR, (XMARGIN, TOPMARGIN, BOXSIZE * BOARDWIDTH, BOXSIZE * BOARDHEIGHT))
# draw the individual boxes on the board
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT):
drawBox(x, y, board[x][y])
drawBoard()
函数负责调用棋盘边框和棋盘上所有方块的绘制函数。首先在DISPLAYSURF
上绘制棋盘的边框,然后绘制棋盘的背景颜色。然后对棋盘上的每个空间调用drawBox()
。drawBox()
函数足够智能,如果board[x][y]
设置为BLANK
,它会略过这个方块。
绘制得分和等级文本
def drawStatus(score, level):
# draw the score text
scoreSurf = BASICFONT.render('Score: %s' % score, True, TEXTCOLOR)
scoreRect = scoreSurf.get_rect()
scoreRect.topleft = (WINDOWWIDTH - 150, 20)
DISPLAYSURF.blit(scoreSurf, scoreRect)
# draw the level text
levelSurf = BASICFONT.render('Level: %s' % level, True, TEXTCOLOR)
levelRect = levelSurf.get_rect()
levelRect.topleft = (WINDOWWIDTH - 150, 50)
DISPLAYSURF.blit(levelSurf, levelRect)
drawStatus()
函数负责在屏幕右上角渲染“得分:”和“等级:”信息的文本。
在棋盘上或屏幕其他位置绘制方块
def drawPiece(piece, pixelx=None, pixely=None):
shapeToDraw = SHAPES[piece['shape']][piece['rotation']]
if pixelx == None and pixely == None:
# if pixelx & pixely hasn't been specified, use the location stored in the piece data structure
pixelx, pixely = convertToPixelCoords(piece['x'], piece['y'])
# draw each of the blocks that make up the piece
for x in range(TEMPLATEWIDTH):
for y in range(TEMPLATEHEIGHT):
if shapeToDraw[y][x] != BLANK:
drawBox(None, None, piece['color'], pixelx + (x * BOXSIZE), pixely + (y * BOXSIZE))
drawPiece()
函数将根据传递给它的方块数据结构绘制方块的方框。这个函数将用于绘制下落的方块和“Next”方块。由于方块数据结构将包含所有形状、位置、旋转和颜色信息,因此除了方块数据结构之外,不需要传递其他东西给这个函数。
然而,“Next”方块并没有在棋盘上绘制。在这种情况下,我们忽略存储在方块数据结构内的位置信息,而是让drawPiece()
函数的调用者传入可选的pixelx
和pixely
参数来指定在窗口上绘制方块的确切位置。
如果没有传入pixelx
和pixely
参数,则第 484 和 486 行将使用convertToPixelCoords()
调用的返回值覆盖这些变量。
在第 489 和 490 行的嵌套for
循环将为需要绘制的方块调用drawBox()
。
绘制“Next”方块
def drawNextPiece(piece):
# draw the "next" text
nextSurf = BASICFONT.render('Next:', True, TEXTCOLOR)
nextRect = nextSurf.get_rect()
nextRect.topleft = (WINDOWWIDTH - 120, 80)
DISPLAYSURF.blit(nextSurf, nextRect)
# draw the "next" piece
drawPiece(piece, pixelx=WINDOWWIDTH-120, pixely=100)
if __name__ == '__main__':
main()
drawNextPiece()
在屏幕右上角绘制“Next”方块。它通过调用drawPiece()
函数并传入drawPiece()
的pixelx
和pixely
参数来实现这一点。
这是最后一个函数。在所有函数定义执行完毕后,将运行第 505 和 506 行,然后调用main()
函数开始程序的主要部分。
总结
俄罗斯方块游戏(这是更受欢迎的“俄罗斯方块”的克隆)用英语向别人解释起来相当容易:“方块从棋盘顶部掉落,玩家移动和旋转它们,使它们形成完整的线。完整的线会消失(给玩家得分),上面的线会下移。游戏会一直进行,直到方块填满整个棋盘,玩家输掉游戏。”
用简单的英语解释是一回事,但当我们必须准确告诉计算机要做什么时,就有许多细节需要填写。最初的俄罗斯方块游戏是在 1984 年由苏联的一名人,亚历克斯·帕吉特诺夫设计和编程的。这个游戏简单、有趣、令人上瘾。它是有史以来最受欢迎的视频游戏之一,已经销售了 1 亿份,许多人都创造了自己的克隆和变种。
所有这些都是由一个懂得如何编程的人创造的。
有了正确的想法和一些编程知识,你可以创造出非常有趣的游戏。通过一些练习,你将能够将你的游戏想法变成真正的程序,可能会像俄罗斯方块一样受欢迎!
为了进行额外的编程练习,你可以从invpy.com/buggy/tetromino
下载俄罗斯方块的有 bug 的版本,并尝试弄清楚如何修复这些 bug。
书籍网站上也有俄罗斯方块游戏的变体。“Pentomino”是由五个方块组成的版本。还有“Tetromino for Idiots”,其中所有的方块都只由一个小方块组成。
这些变体可以从以下网址下载:
第八章:松鼠吃松鼠
原文:
inventwithpython.com/pygame/chapter8.html
译者:飞龙
如何玩松鼠吃松鼠
松鼠吃松鼠 loosley 基于游戏“塊鼠大冒險”。玩家控制一个小松鼠,在屏幕上跳来跳去,吃掉比它小的松鼠,避开比它大的松鼠。每当玩家的松鼠吃掉比它小的松鼠时,它就会变得更大。如果玩家的松鼠被比它大的松鼠撞到,它就会失去一个生命点。当松鼠变成一个名为 Omega 松鼠的巨大松鼠时,玩家获胜。如果玩家的松鼠被撞三次,玩家就输了。
我真的不确定我是从哪里得到的一个松鼠互相吃掉的视频游戏的想法。有时候我有点奇怪。
松鼠吃松鼠的设计
这个游戏中有三种数据结构,它们被表示为字典值。这些类型分别是玩家松鼠、敌对松鼠和草对象。游戏中一次只有一个玩家松鼠对象。
注意:在面向对象编程中,“对象”在技术上有特定的含义。Python 确实具有面向对象编程的特性,但本书中没有涉及。从技术上讲,Pygame 对象,如“Rect 对象”或“Surface 对象”都是对象。但在本书中,我将使用术语“对象”来指代“游戏世界中存在的东西”。但实际上,玩家松鼠、敌对松鼠和草“对象”只是字典值。
所有对象的字典值中都有以下键:'x'
、'y'
和'rect'
。'x'
和'y'
键的值给出了对象在游戏世界坐标中左上角的坐标。这些与像素坐标不同(这是'rect'
键的值跟踪的内容)。游戏世界坐标和像素坐标之间的差异将在您学习摄像机概念时进行解释。
此外,玩家松鼠、敌对松鼠和草对象还有其他键,这些键在源代码的开头有一个大的注释进行了解释。
松鼠吃松鼠的源代码
这个源代码可以从invpy.com/squirrel.py
下载。如果出现任何错误消息,请查看错误消息中提到的行号,并检查您的代码是否有任何拼写错误。您还可以将代码复制粘贴到invpy.com/diff/squirrel
的网络表单中,以查看您的代码与书中代码之间的差异。
您还需要下载以下图像文件:
# Squirrel Eat Squirrel (a 2D Katamari Damacy clone)
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US
import random, sys, time, math, pygame
from pygame.locals import *
FPS = 30 # frames per second to update the screen
WINWIDTH = 640 # width of the program's window, in pixels
WINHEIGHT = 480 # height in pixels
HALF_WINWIDTH = int(WINWIDTH / 2)
HALF_WINHEIGHT = int(WINHEIGHT / 2)
GRASSCOLOR = (24, 255, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
CAMERASLACK = 90 # how far from the center the squirrel moves before moving the camera
MOVERATE = 9 # how fast the player moves
BOUNCERATE = 6 # how fast the player bounces (large is slower)
BOUNCEHEIGHT = 30 # how high the player bounces
STARTSIZE = 25 # how big the player starts off
WINSIZE = 300 # how big the player needs to be to win
INVULNTIME = 2 # how long the player is invulnerable after being hit in seconds
GAMEOVERTIME = 4 # how long the "game over" text stays on the screen in seconds
MAXHEALTH = 3 # how much health the player starts with
NUMGRASS = 80 # number of grass objects in the active area
NUMSQUIRRELS = 30 # number of squirrels in the active area
SQUIRRELMINSPEED = 3 # slowest squirrel speed
SQUIRRELMAXSPEED = 7 # fastest squirrel speed
DIRCHANGEFREQ = 2 # % chance of direction change per frame
LEFT = 'left'
RIGHT = 'right'
"""
This program has three data structures to represent the player, enemy squirrels, and grass background objects. The data structures are dictionaries with the following keys:
Keys used by all three data structures:
'x' - the left edge coordinate of the object in the game world (not a pixel coordinate on the screen)
'y' - the top edge coordinate of the object in the game world (not a pixel coordinate on the screen)
'rect' - the pygame.Rect object representing where on the screen the object is located.
Player data structure keys:
'surface' - the pygame.Surface object that stores the image of the squirrel which will be drawn to the screen.
'facing' - either set to LEFT or RIGHT, stores which direction the player is facing.
'size' - the width and height of the player in pixels. (The width & height are always the same.)
'bounce' - represents at what point in a bounce the player is in. 0 means standing (no bounce), up to BOUNCERATE (the completion of the bounce)
'health' - an integer showing how many more times the player can be hit by a larger squirrel before dying.
Enemy Squirrel data structure keys:
'surface' - the pygame.Surface object that stores the image of the squirrel which will be drawn to the screen.
'movex' - how many pixels per frame the squirrel moves horizontally. A negative integer is moving to the left, a positive to the right.
'movey' - how many pixels per frame the squirrel moves vertically. A negative integer is moving up, a positive moving down.
'width' - the width of the squirrel's image, in pixels
'height' - the height of the squirrel's image, in pixels
'bounce' - represents at what point in a bounce the player is in. 0 means standing (no bounce), up to BOUNCERATE (the completion of the bounce)
'bouncerate' - how quickly the squirrel bounces. A lower number means a quicker bounce.
'bounceheight' - how high (in pixels) the squirrel bounces
Grass data structure keys:
'grassImage' - an integer that refers to the index of the pygame.Surface object in GRASSIMAGES used for this grass object
"""
def main():
global FPSCLOCK, DISPLAYSURF, BASICFONT, L_SQUIR_IMG, R_SQUIR_IMG, GRASSIMAGES
pygame.init()
FPSCLOCK = pygame.time.Clock()
pygame.display.set_icon(pygame.image.load('gameicon.png'))
DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT))
pygame.display.set_caption('Squirrel Eat Squirrel')
BASICFONT = pygame.font.Font('freesansbold.ttf', 32)
# load the image files
L_SQUIR_IMG = pygame.image.load('squirrel.png')
R_SQUIR_IMG = pygame.transform.flip(L_SQUIR_IMG, True, False)
GRASSIMAGES = []
for i in range(1, 5):
GRASSIMAGES.append(pygame.image.load('grass%s.png' % i))
while True:
runGame()
def runGame():
# set up variables for the start of a new game
invulnerableMode = False # if the player is invulnerable
invulnerableStartTime = 0 # time the player became invulnerable
gameOverMode = False # if the player has lost
gameOverStartTime = 0 # time the player lost
winMode = False # if the player has won
# create the surfaces to hold game text
gameOverSurf = BASICFONT.render('Game Over', True, WHITE)
gameOverRect = gameOverSurf.get_rect()
gameOverRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
winSurf = BASICFONT.render('You have achieved OMEGA SQUIRREL!', True, WHITE)
winRect = winSurf.get_rect()
winRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
winSurf2 = BASICFONT.render('(Press "r" to restart.)', True, WHITE)
winRect2 = winSurf2.get_rect()
winRect2.center = (HALF_WINWIDTH, HALF_WINHEIGHT + 30)
# camerax and cameray are where the middle of the camera view is
camerax = 0
cameray = 0
grassObjs = [] # stores all the grass objects in the game
squirrelObjs = [] # stores all the non-player squirrel objects
# stores the player object:
playerObj = {'surface': pygame.transform.scale(L_SQUIR_IMG, (STARTSIZE, STARTSIZE)),
'facing': LEFT,
'size': STARTSIZE,
'x': HALF_WINWIDTH,
'y': HALF_WINHEIGHT,
'bounce':0,
'health': MAXHEALTH}
moveLeft = False
moveRight = False
moveUp = False
moveDown = False
# start off with some random grass images on the screen
for i in range(10):
grassObjs.append(makeNewGrass(camerax, cameray))
grassObjs[i]['x'] = random.randint(0, WINWIDTH)
grassObjs[i]['y'] = random.randint(0, WINHEIGHT)
while True: # main game loop
# Check if we should turn off invulnerability
if invulnerableMode and time.time() - invulnerableStartTime > INVULNTIME:
invulnerableMode = False
# move all the squirrels
for sObj in squirrelObjs:
# move the squirrel, and adjust for their bounce
sObj['x'] += sObj['movex']
sObj['y'] += sObj['movey']
sObj['bounce'] += 1
if sObj['bounce'] > sObj['bouncerate']:
sObj['bounce'] = 0 # reset bounce amount
# random chance they change direction
if random.randint(0, 99) < DIRCHANGEFREQ:
sObj['movex'] = getRandomVelocity()
sObj['movey'] = getRandomVelocity()
if sObj['movex'] > 0: # faces right
sObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (sObj['width'], sObj['height']))
else: # faces left
sObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (sObj['width'], sObj['height']))
# go through all the objects and see if any need to be deleted.
for i in range(len(grassObjs) - 1, -1, -1):
if isOutsideActiveArea(camerax, cameray, grassObjs[i]):
del grassObjs[i]
for i in range(len(squirrelObjs) - 1, -1, -1):
if isOutsideActiveArea(camerax, cameray, squirrelObjs[i]):
del squirrelObjs[i]
# add more grass & squirrels if we don't have enough.
while len(grassObjs) < NUMGRASS:
grassObjs.append(makeNewGrass(camerax, cameray))
while len(squirrelObjs) < NUMSQUIRRELS:
squirrelObjs.append(makeNewSquirrel(camerax, cameray))
# adjust camerax and cameray if beyond the "camera slack"
playerCenterx = playerObj['x'] + int(playerObj['size'] / 2)
playerCentery = playerObj['y'] + int(playerObj['size'] / 2)
if (camerax + HALF_WINWIDTH) - playerCenterx > CAMERASLACK:
camerax = playerCenterx + CAMERASLACK - HALF_WINWIDTH
elif playerCenterx – (camerax + HALF_WINWIDTH) > CAMERASLACK:
camerax = playerCenterx – CAMERASLACK - HALF_WINWIDTH
if (cameray + HALF_WINHEIGHT) - playerCentery > CAMERASLACK:
cameray = playerCentery + CAMERASLACK - HALF_WINHEIGHT
elif playerCentery – (cameray + HALF_WINHEIGHT) > CAMERASLACK:
cameray = playerCentery – CAMERASLACK - HALF_WINHEIGHT
# draw the green background
DISPLAYSURF.fill(GRASSCOLOR)
# draw all the grass objects on the screen
for gObj in grassObjs:
gRect = pygame.Rect( (gObj['x'] - camerax,
gObj['y'] - cameray,
gObj['width'],
gObj['height']) )
DISPLAYSURF.blit(GRASSIMAGES[gObj['grassImage']], gRect)
# draw the other squirrels
for sObj in squirrelObjs:
sObj['rect'] = pygame.Rect( (sObj['x'] - camerax,
sObj['y'] - cameray - getBounceAmount(sObj['bounce'], sObj['bouncerate'], sObj['bounceheight']),
sObj['width'],
sObj['height']) )
DISPLAYSURF.blit(sObj['surface'], sObj['rect'])
# draw the player squirrel
flashIsOn = round(time.time(), 1) * 10 % 2 == 1
if not gameOverMode and not (invulnerableMode and flashIsOn):
playerObj['rect'] = pygame.Rect( (playerObj['x'] - camerax,
playerObj['y'] – cameray - getBounceAmount(playerObj['bounce'], BOUNCERATE, BOUNCEHEIGHT),
playerObj['size'],
playerObj['size']) )
DISPLAYSURF.blit(playerObj['surface'], playerObj['rect'])
# draw the health meter
drawHealthMeter(playerObj['health'])
for event in pygame.event.get(): # event handling loop
if event.type == QUIT:
terminate()
elif event.type == KEYDOWN:
if event.key in (K_UP, K_w):
moveDown = False
moveUp = True
elif event.key in (K_DOWN, K_s):
moveUp = False
moveDown = True
elif event.key in (K_LEFT, K_a):
moveRight = False
moveLeft = True
if playerObj['facing'] == RIGHT: # change player image
playerObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (playerObj['size'], playerObj['size']))
playerObj['facing'] = LEFT
elif event.key in (K_RIGHT, K_d):
moveLeft = False
moveRight = True
if playerObj['facing'] == LEFT: # change player image
playerObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (playerObj['size'], playerObj['size']))
playerObj['facing'] = RIGHT
elif winMode and event.key == K_r:
return
elif event.type == KEYUP:
# stop moving the player's squirrel
if event.key in (K_LEFT, K_a):
moveLeft = False
elif event.key in (K_RIGHT, K_d):
moveRight = False
elif event.key in (K_UP, K_w):
moveUp = False
elif event.key in (K_DOWN, K_s):
moveDown = False
elif event.key == K_ESCAPE:
terminate()
if not gameOverMode:
# actually move the player
if moveLeft:
playerObj['x'] -= MOVERATE
if moveRight:
playerObj['x'] += MOVERATE
if moveUp:
playerObj['y'] -= MOVERATE
if moveDown:
playerObj['y'] += MOVERATE
if (moveLeft or moveRight or moveUp or moveDown) or playerObj['bounce'] != 0:
playerObj['bounce'] += 1
if playerObj['bounce'] > BOUNCERATE:
playerObj['bounce'] = 0 # reset bounce amount
# check if the player has collided with any squirrels
for i in range(len(squirrelObjs)-1, -1, -1):
sqObj = squirrelObjs[i]
if 'rect' in sqObj and playerObj['rect'].colliderect(sqObj['rect']):
# a player/squirrel collision has occurred
277.
if sqObj['width'] * sqObj['height'] <= playerObj['size']**2:
# player is larger and eats the squirrel
playerObj['size'] += int( (sqObj['width'] * sqObj['height'])**0.2 ) + 1
del squirrelObjs[i]
if playerObj['facing'] == LEFT:
playerObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (playerObj['size'], playerObj['size']))
if playerObj['facing'] == RIGHT:
playerObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (playerObj['size'], playerObj['size']))
if playerObj['size'] > WINSIZE:
winMode = True # turn on "win mode"
elif not invulnerableMode:
# player is smaller and takes damage
invulnerableMode = True
invulnerableStartTime = time.time()
playerObj['health'] -= 1
if playerObj['health'] == 0:
gameOverMode = True # turn on "game over mode"
gameOverStartTime = time.time()
else:
# game is over, show "game over" text
DISPLAYSURF.blit(gameOverSurf, gameOverRect)
if time.time() - gameOverStartTime > GAMEOVERTIME:
return # end the current game
# check if the player has won.
if winMode:
DISPLAYSURF.blit(winSurf, winRect)
DISPLAYSURF.blit(winSurf2, winRect2)
pygame.display.update()
FPSCLOCK.tick(FPS)
def drawHealthMeter(currentHealth):
for i in range(currentHealth): # draw red health bars
pygame.draw.rect(DISPLAYSURF, RED, (15, 5 + (10 * MAXHEALTH) - i * 10, 20, 10))
for i in range(MAXHEALTH): # draw the white outlines
pygame.draw.rect(DISPLAYSURF, WHITE, (15, 5 + (10 * MAXHEALTH) - i * 10, 20, 10), 1)
def terminate():
pygame.quit()
sys.exit()
def getBounceAmount(currentBounce, bounceRate, bounceHeight):
# Returns the number of pixels to offset based on the bounce.
# Larger bounceRate means a slower bounce.
# Larger bounceHeight means a higher bounce.
# currentBounce will always be less than bounceRate
return int(math.sin( (math.pi / float(bounceRate)) * currentBounce ) * bounceHeight)
def getRandomVelocity():
speed = random.randint(SQUIRRELMINSPEED, SQUIRRELMAXSPEED)
if random.randint(0, 1) == 0:
return speed
else:
return -speed
def getRandomOffCameraPos(camerax, cameray, objWidth, objHeight):
# create a Rect of the camera view
cameraRect = pygame.Rect(camerax, cameray, WINWIDTH, WINHEIGHT)
while True:
x = random.randint(camerax - WINWIDTH, camerax + (2 * WINWIDTH))
y = random.randint(cameray - WINHEIGHT, cameray + (2 * WINHEIGHT))
349. # create a Rect object with the random coordinates and use colliderect()
# to make sure the right edge isn't in the camera view.
objRect = pygame.Rect(x, y, objWidth, objHeight)
if not objRect.colliderect(cameraRect):
return x, y
def makeNewSquirrel(camerax, cameray):
sq = {}
generalSize = random.randint(5, 25)
multiplier = random.randint(1, 3)
sq['width'] = (generalSize + random.randint(0, 10)) * multiplier
sq['height'] = (generalSize + random.randint(0, 10)) * multiplier
sq['x'], sq['y'] = getRandomOffCameraPos(camerax, cameray, sq['width'], sq['height'])
sq['movex'] = getRandomVelocity()
sq['movey'] = getRandomVelocity()
if sq['movex'] < 0: # squirrel is facing left
sq['surface'] = pygame.transform.scale(L_SQUIR_IMG, (sq['width'], sq['height']))
else: # squirrel is facing right
sq['surface'] = pygame.transform.scale(R_SQUIR_IMG, (sq['width'], sq['height']))
sq['bounce'] = 0
sq['bouncerate'] = random.randint(10, 18)
sq['bounceheight'] = random.randint(10, 50)
return sq
def makeNewGrass(camerax, cameray):
gr = {}
gr['grassImage'] = random.randint(0, len(GRASSIMAGES) - 1)
gr['width'] = GRASSIMAGES[0].get_width()
gr['height'] = GRASSIMAGES[0].get_height()
gr['x'], gr['y'] = getRandomOffCameraPos(camerax, cameray, gr['width'], gr['height'])
gr['rect'] = pygame.Rect( (gr['x'], gr['y'], gr['width'], gr['height']) )
return gr
def isOutsideActiveArea(camerax, cameray, obj):
# Return False if camerax and cameray are more than
# a half-window length beyond the edge of the window.
boundsLeftEdge = camerax - WINWIDTH
boundsTopEdge = cameray - WINHEIGHT
boundsRect = pygame.Rect(boundsLeftEdge, boundsTopEdge, WINWIDTH * 3, WINHEIGHT * 3)
objRect = pygame.Rect(obj['x'], obj['y'], obj['width'], obj['height'])
return not boundsRect.colliderect(objRect)
if __name__ == '__main__':
main()
通常的设置代码
# Squirrel Eat Squirrel (a 2D Katamari Damacy clone)
# By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection)
# http://inventwithpython.com/pygame
# Creative Commons BY-NC-SA 3.0 US
import random, sys, time, math, pygame
from pygame.locals import *
FPS = 30 # frames per second to update the screen
WINWIDTH = 640 # width of the program's window, in pixels
WINHEIGHT = 480 # height in pixels
HALF_WINWIDTH = int(WINWIDTH / 2)
HALF_WINHEIGHT = int(WINHEIGHT / 2)
GRASSCOLOR = (24, 255, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
程序的开头分配了几个常量变量。这个程序经常使用窗口宽度和高度的一半,HALF_WINWIDTH
和HALF_WINHEIGHT
变量存储了这些数字。
CAMERASLACK = 90 # how far from the center the squirrel moves before moving the camera
“相机松弛”稍后会进行解释。基本上,这意味着当玩家松鼠离窗口中心 90 像素时,相机将开始跟随玩家松鼠移动。
MOVERATE = 9 # how fast the player moves
BOUNCERATE = 6 # how fast the player bounces (large is slower)
BOUNCEHEIGHT = 30 # how high the player bounces
STARTSIZE = 25 # how big the player starts off
WINSIZE = 300 # how big the player needs to be to win
INVULNTIME = 2 # how long the player is invulnerable after being hit in seconds
GAMEOVERTIME = 4 # how long the "game over" text stays on the screen in seconds
MAXHEALTH = 3 # how much health the player starts with
NUMGRASS = 80 # number of grass objects in the active area
NUMSQUIRRELS = 30 # number of squirrels in the active area
SQUIRRELMINSPEED = 3 # slowest squirrel speed
SQUIRRELMAXSPEED = 7 # fastest squirrel speed
DIRCHANGEFREQ = 2 # % chance of direction change per frame
LEFT = 'left'
RIGHT = 'right'
这些常量旁边的注释解释了常量变量的用途。
描述数据结构
"""
This program has three data structures to represent the player, enemy squirrels, and grass background objects. The data structures are dictionaries with the following keys:
Keys used by all three data structures:
'x' - the left edge coordinate of the object in the game world (not a pixel coordinate on the screen)
'y' - the top edge coordinate of the object in the game world (not a pixel coordinate on the screen)
'rect' - the pygame.Rect object representing where on the screen the object is located.
Player data structure keys:
'surface' - the pygame.Surface object that stores the image of the squirrel which will be drawn to the screen.
'facing' - either set to LEFT or RIGHT, stores which direction the player is facing.
'size' - the width and height of the player in pixels. (The width & height are always the same.)
'bounce' - represents at what point in a bounce the player is in. 0 means standing (no bounce), up to BOUNCERATE (the completion of the bounce)
'health' - an integer showing how many more times the player can be hit by a larger squirrel before dying.
Enemy Squirrel data structure keys:
'surface' - the pygame.Surface object that stores the image of the squirrel which will be drawn to the screen.
'movex' - how many pixels per frame the squirrel moves horizontally. A negative integer is moving to the left, a positive to the right.
'movey' - how many pixels per frame the squirrel moves vertically. A negative integer is moving up, a positive moving down.
'width' - the width of the squirrel's image, in pixels
'height' - the height of the squirrel's image, in pixels
'bounce' - represents at what point in a bounce the player is in. 0 means standing (no bounce), up to BOUNCERATE (the completion of the bounce)
'bouncerate' - how quickly the squirrel bounces. A lower number means a quicker bounce.
'bounceheight' - how high (in pixels) the squirrel bounces
Grass data structure keys:
'grassImage' - an integer that refers to the index of the pygame.Surface object in GRASSIMAGES used for this grass object
"""
从第 37 行到第 61 行的注释是一个大的、多行的字符串。它们描述了玩家松鼠、敌对松鼠和草对象的键。在 Python 中,一个独立的多行字符串值可以作为多行注释。
main()
函数
def main():
global FPSCLOCK, DISPLAYSURF, BASICFONT, L_SQUIR_IMG, R_SQUIR_IMG, GRASSIMAGES
pygame.init()
FPSCLOCK = pygame.time.Clock()
pygame.display.set_icon(pygame.image.load('gameicon.png'))
DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT))
pygame.display.set_caption('Squirrel Eat Squirrel')
BASICFONT = pygame.font.Font('freesansbold.ttf', 32)
main()
函数的前几行是我们以前游戏程序中看到的相同的设置代码。pygame.display.set_icon()
是一个 Pygame 函数,用于设置窗口标题栏中的图标(就像pygame.display.set_caption()
设置标题栏中的标题文本一样)。pygame.display.set_icon()
的单个参数是一个小图像的 Surface 对象。理想的图像尺寸是 32 x 32 像素,尽管您可以使用其他尺寸的图像。图像将被压缩成较小的尺寸,以用作窗口的图标。
pygame.transform.flip()
函数
# load the image files
L_SQUIR_IMG = pygame.image.load('squirrel.png')
R_SQUIR_IMG = pygame.transform.flip(L_SQUIR_IMG, True, False)
GRASSIMAGES = []
for i in range(1, 5):
GRASSIMAGES.append(pygame.image.load('grass%s.png' % i))
玩家和敌对松鼠的图像是从第 74 行的squirrel.png中加载的。确保这个 PNG 文件与squirrel.py在同一个文件夹中,否则你会得到错误 pygame.error: Couldn't open squirrel.png。
squirrel.png中的图像(您可以从invpy.com/squirrel.png
下载)是一只面向左的松鼠。我们还需要一个包含面向右的松鼠图片的 Surface 对象。我们可以调用pygame.transform.flip()
函数,而不是创建第二个 PNG 图像文件。这个函数有三个参数:要翻转的图像的 Surface 对象,一个布尔值进行水平翻转,一个布尔值进行垂直翻转。通过将第二个参数传递为True
,第三个参数传递为False
,返回的 Surface 对象具有面向右的松鼠的图像。我们传递的L_SQUIR_IMG
中的原始 Surface 对象保持不变。
以下是图像水平和垂直翻转的示例:
原始 | 水平翻转 | 垂直翻转 | 水平和垂直翻转 |
---|---|---|---|
while True:
runGame()
在main()
中的设置完成后,游戏开始调用runGame()
。
比通常更详细的游戏状态
def runGame():
# set up variables for the start of a new game
invulnerableMode = False # if the player is invulnerable
invulnerableStartTime = 0 # time the player became invulnerable
gameOverMode = False # if the player has lost
gameOverStartTime = 0 # time the player lost
winMode = False # if the player has won
松鼠吃松鼠游戏有很多跟踪游戏状态的变量。这些变量将在稍后在代码中使用时进行更详细的解释。
通常的文本创建代码
# create the surfaces to hold game text
gameOverSurf = BASICFONT.render('Game Over', True, WHITE)
gameOverRect = gameOverSurf.get_rect()
gameOverRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
winSurf = BASICFONT.render('You have achieved OMEGA SQUIRREL!', True, WHITE)
winRect = winSurf.get_rect()
winRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
winSurf2 = BASICFONT.render('(Press "r" to restart.)', True, WHITE)
winRect2 = winSurf2.get_rect()
winRect2.center = (HALF_WINWIDTH, HALF_WINHEIGHT + 30)
这些变量包含屏幕上游戏结束后出现的“游戏结束”,“你已经获得 OMEGA 松鼠!”和“(按r
重新开始)”文本的 Surface 对象。
摄像机
# camerax and cameray are where the middle of the camera view is
camerax = 0
cameray = 0
camerax
和cameray
变量跟踪“摄像机”的游戏坐标。想象游戏世界是一个无限的二维空间。当然,这永远无法适应任何屏幕。我们只能在屏幕上绘制无限 2D 空间的一部分。我们称这一部分的区域为摄像机,因为就像我们的屏幕只是摄像机所看到的游戏世界的区域。这是游戏世界(一个无限的绿色领域)和摄像机可以查看的区域的图片:
正如你所看到的,游戏世界的 XY 坐标将永远变大和变小。游戏世界的原点是游戏世界坐标为(0,0)的地方。你可以看到三只松鼠的位置(在游戏世界坐标中)分别为(-384,-84),(384,306)和(585,-234)。
但是我们只能在屏幕上显示 640 x 480 像素的区域(尽管如果我们向pygame.display.set_mode()
函数传递不同的数字,这可能会改变),所以我们需要跟踪摄像机原点在游戏世界坐标中的位置。在上面的图片中,摄像机在游戏世界坐标中的位置是(-486,-330)。
下面的图片显示了相同的领域和松鼠,只是一切都是以摄像机坐标给出的:
相机可以看到的区域(称为相机视野)的中心(即其原点)位于游戏世界坐标(-486, -330)。由于相机看到的内容显示在玩家的屏幕上,因此“相机”坐标与“像素”坐标相同。要找出松鼠的像素坐标(即它们在屏幕上出现的位置),需要用松鼠的游戏坐标减去相机原点的游戏坐标。
左边的松鼠在游戏世界坐标为(-384, -84),但在屏幕上的像素坐标为(102, 246)。(对于 X 坐标,-384 - -486 = 102,对于 Y 坐标,-84 - -330 = 246。)
当我们对其他两只松鼠进行相同的计算以找到它们的像素坐标时,我们发现它们存在于屏幕范围之外。这就是为什么它们不会出现在相机的视野中。
“活动区域”
“活动区域”只是我想出来描述游戏世界的区域的一个名字,相机视野加上相机区域大小的周围区域:
计算某物是否在活动区域内的方法在本章后面的isOutsideActiveArea()
函数的解释中有说明。当我们创建新的敌对松鼠或草对象时,我们不希望它们被创建在相机的视野内,因为这样看起来它们就像从无处冒出来一样。
但我们也不希望将它们创建得离相机太远,因为那样它们可能永远不会漫游到相机的视野中。在活动区域内但在相机之外是松鼠和草对象可以安全创建的地方。
此外,当松鼠和草对象超出活动区域的边界时,它们距离足够远,可以删除,以便它们不再占用内存。那么远的对象不再需要,因为它们很少可能再次出现在相机的视野中。
如果你曾经在超级任天堂上玩过超级马里奥世界,有一个很好的 YouTube 视频解释了超级马里奥世界的相机系统是如何工作的。你可以在invpy.com/mariocamera
找到这个视频。
跟踪游戏世界中事物的位置
grassObjs = [] # stores all the grass objects in the game
squirrelObjs = [] # stores all the non-player squirrel objects
# stores the player object:
playerObj = {'surface': pygame.transform.scale(L_SQUIR_IMG, (STARTSIZE, STARTSIZE)),
'facing': LEFT,
'size': STARTSIZE,
'x': HALF_WINWIDTH,
'y': HALF_WINHEIGHT,
'bounce':0,
'health': MAXHEALTH}
moveLeft = False
moveRight = False
moveUp = False
moveDown = False
grassObjs
变量保存了游戏中所有草对象的列表。随着新的草对象的创建,它们被添加到这个列表中。当草对象被删除时,它们将从此列表中移除。squirrelObjs
变量和敌对松鼠对象也是如此。
playerObj
变量不是一个列表,而只是字典值本身。
第 120 至 123 行的移动变量跟踪着哪个箭头键(或 WASD 键)被按下,就像在之前的一些游戏程序中一样。
从一些草开始
# start off with some random grass images on the screen
for i in range(10):
grassObjs.append(makeNewGrass(camerax, cameray))
grassObjs[i]['x'] = random.randint(0, WINWIDTH)
grassObjs[i]['y'] = random.randint(0, WINHEIGHT)
活动区域应该从屏幕上可见的一些草对象开始。makeNewGrass()
函数将创建并返回一个草对象,该对象随机位于活动区域但在相机视野之外的某个地方。这是我们调用makeNewGrass()
时通常想要的,但由于我们希望确保前几个草对象在屏幕上,X 和 Y 坐标被覆盖。
游戏循环
while True: # main game loop
游戏循环,就像以前的游戏程序中的游戏循环一样,将处理事件,更新游戏状态,并将所有内容绘制到屏幕上。
检查是否禁用无敌状态
# Check if we should turn off invulnerability
if invulnerableMode and time.time() - invulnerableStartTime > INVULNTIME:
invulnerableMode = False
当玩家被敌对松鼠击中但没有死亡时,我们会让玩家在几秒钟内处于无敌状态(因为INVULNTIME
常量设置为2
)。在此期间,玩家的松鼠会闪烁,并且不会受到其他松鼠的伤害。如果“无敌模式”时间结束,第 134 行将把invulnerableMode
设置为False
。
移动敌对松鼠
# move all the squirrels
for sObj in squirrelObjs:
# move the squirrel, and adjust for their bounce
sObj['x'] += sObj['movex']
sObj['y'] += sObj['movey']
敌方松鼠都根据它们的'movex'和'movey'键中的值移动。如果这些值是正数,松鼠向右或向下移动。如果这些值是负数,它们向左或向上移动。值越大,它们在游戏循环中的每次迭代中移动得越远(这意味着它们移动得更快)。
第 137 行的for
循环将应用此移动代码到squirrelObjs
列表中的每个敌方松鼠对象。首先,第 139 和 140 行将调整它们的'x'和'y'键的值。
sObj['bounce'] += 1
if sObj['bounce'] > sObj['bouncerate']:
sObj['bounce'] = 0 # reset bounce amount
sObj['bounce']
中的值在每次松鼠的游戏循环迭代中递增。当这个值为0
时,松鼠在其弹跳的最开始。当这个值等于sObj['bouncerate']
中的值时,该值就结束了。(这就是为什么较小的sObj['bouncerate']
值会导致更快的弹跳。如果sObj['bouncerate']
是3
,那么松鼠只需要三次游戏循环迭代就能完成一次完整的弹跳。如果sObj['bouncerate']
是10
,那么就需要十次迭代。)
当sObj['bounce']
大于sObj['bouncerate']
时,它需要被重置为0
。这就是第 142 和 143 行的作用。
# random chance they change direction
if random.randint(0, 99) < DIRCHANGEFREQ:
sObj['movex'] = getRandomVelocity()
sObj['movey'] = getRandomVelocity()
if sObj['movex'] > 0: # faces right
sObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (sObj['width'], sObj['height']))
else: # faces left
sObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (sObj['width'], sObj['height']))
在游戏循环的每次迭代中,有 2%的几率松鼠会随机改变速度和方向。在第 146 行,random.randint(0, 99)
的调用会随机选择 100 个可能的整数中的一个整数。如果这个数字小于DIRCHANGEFREQ
(我们在第 33 行设置为2
),那么sObj['movex']
和sObj['movey']
将被设置为新值。
因为这意味着松鼠可能已经改变了方向,所以sObj['surface']
中的 Surface 对象应该被一个新的替换,它应该正确地面向左或右,并且按照松鼠的大小进行缩放。这就是第 149 到 152 行的作用。请注意,第 150 行获取了一个从R_SQUIR_IMG
缩放的 Surface 对象,第 152 行获取了一个从L_SQUIR_IMG
缩放的 Surface 对象。
删除远处的草和松鼠对象
# go through all the objects and see if any need to be deleted.
for i in range(len(grassObjs) - 1, -1, -1):
if isOutsideActiveArea(camerax, cameray, grassObjs[i]):
del grassObjs[i]
for i in range(len(squirrelObjs) - 1, -1, -1):
if isOutsideActiveArea(camerax, cameray, squirrelObjs[i]):
del squirrelObjs[i]
在游戏循环的每次迭代中,代码将检查所有草和敌方松鼠对象,看它们是否在“活动区域”之外。isOutsideActiveArea()
函数接受摄像机的当前坐标(存储在camerax
和cameray
中)和草/敌方松鼠对象,并在对象不在活动区域时返回True
。
如果是这种情况,这个对象将在第 158 行(对于草对象)或第 161 行(对于松鼠对象)被删除。这就是当玩家离它们足够远时(或者当敌方松鼠离玩家足够远时),松鼠和草对象被删除的方式。这确保了玩家附近始终有一定数量的松鼠和草对象。
在列表中删除项目时,以相反顺序迭代列表
删除松鼠和草对象是使用del
运算符完成的。但是,请注意,第 156 行和 159 行的for
循环向range()
函数传递参数,以便编号从最后一项的索引开始,然后递减-1
(与通常的递增1
相反),直到达到数字-1
。我们是按照与通常情况下相反的方式迭代列表的索引。这是因为我们正在迭代我们也正在删除项目的列表。
要看为什么需要这种反向顺序,假设我们有以下列表值:
animals = ['cat', 'mouse', 'dog', 'horse']
所以我们想要编写代码来从列表中删除字符串'dog'的任何实例。我们可能会想要编写如下代码:
for i in range(len(animals)):
if animals[i] == 'dog':
del animals[i]
但是如果我们运行这段代码,我们将得到一个IndexError
错误,看起来像这样:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of range
要看为什么会出现这个错误,让我们走一遍代码。首先,animals
列表将被设置为['cat', 'mouse', 'dog', 'horse']
,len(animals)
将返回4
。这意味着对range(4)
的调用将导致for
循环使用值0
、1
、2
和3
进行迭代。
当i
设置为2
时,for
循环迭代,if
语句的条件将为True
,del animals[i]
语句将删除animals[2]
。这意味着之后动物列表将是['cat', 'mouse', 'horse']
。在'dog'
之后的所有项的索引都向下移动了一个位置,因为'dog'
值被删除了。
但是在下一次for
循环迭代中,i
设置为3
。但animals[3]
超出了边界,因为动物列表的有效索引不再是0
到3
,而是0
到2
。对range()
的原始调用是针对包含 4 个项目的列表。列表长度发生了变化,但for
循环设置为原始长度。
然而,如果我们从列表的最后一个索引迭代到0
,我们就不会遇到这个问题。以下程序删除了animals
列表中的'dog'
字符串,而不会引发IndexError
错误:
animals = ['cat', 'mouse', 'dog', 'horse']
for i in range(len(animals) - 1, -1, -1):
if animals[i] == 'dog':
del animals[i]
这段代码之所以不会引发错误,是因为for
循环迭代了3
、2
、1
和0
。在第一次迭代中,代码检查animals[3]
是否等于'dog'
。它不是(animals[3]
是'horse'
),所以代码继续下一次迭代。然后检查animals[2]
是否等于'dog'
。是的,所以删除animals[2]
。
删除animals[2]
后,animals
列表设置为['cat', 'mouse', 'horse']。在下一次迭代中,i
设置为1
。animals[1]
处有一个值('mouse'
值),因此不会引起错误。列表中的所有项在'dog'
之后向下移动一个位置并不重要,因为我们从列表末尾开始并向前移动,所有这些项都已经被检查过了。
同样,我们可以从grassObjs
和squirrelObjs
列表中删除草和松鼠对象而不会出错,因为在第 156 和 159 行的for
循环中以相反的顺序迭代。
添加新的草和松鼠对象
# add more grass & squirrels if we don't have enough.
while len(grassObjs) < NUMGRASS:
grassObjs.append(makeNewGrass(camerax, cameray))
while len(squirrelObjs) < NUMSQUIRRELS:
squirrelObjs.append(makeNewSquirrel(camerax, cameray))
记住,NUMGRASS
常量在程序开始时设置为80
,NUMSQUIRRELS
常量设置为30
?这些变量被设置为确保活动区域中始终有足够的草和松鼠对象。如果grassObjs
或squirrelObjs
的长度低于NUMGRASS
或NUMSQUIRRELS
,则会创建新的草和松鼠对象。创建这些对象的makeNewGrass()
和makeNewSquirrel()
函数将在本章后面进行解释。
相机松弛,移动相机视图
# adjust camerax and cameray if beyond the "camera slack"
playerCenterx = playerObj['x'] + int(playerObj['size'] / 2)
playerCentery = playerObj['y'] + int(playerObj['size'] / 2)
if (camerax + HALF_WINWIDTH) - playerCenterx > CAMERASLACK:
camerax = playerCenterx + CAMERASLACK - HALF_WINWIDTH
elif playerCenterx – (camerax + HALF_WINWIDTH) > CAMERASLACK:
camerax = playerCenterx – CAMERASLACK - HALF_WINWIDTH
if (cameray + HALF_WINHEIGHT) - playerCentery > CAMERASLACK:
cameray = playerCentery + CAMERASLACK - HALF_WINHEIGHT
elif playerCentery – (cameray + HALF_WINHEIGHT) > CAMERASLACK:
cameray = playerCentery – CAMERASLACK - HALF_WINHEIGHT
玩家移动时,相机的位置(存储为camerax
和cameray
变量中的整数)需要更新。我将玩家在相机更新之前可以移动的像素数称为“相机松弛”。第 19 行将CAMERASLACK
常量设置为90
,这意味着我们的程序将在相机位置更新以跟随松鼠之前,玩家松鼠可以从中心移动 90 像素。
为了理解第 172、174、176 和 178 行if
语句中使用的方程式,您应该注意,(camerax + HALF_WINWIDTH)
和(cameray + HALF_WINHEIGHT)
是当前位于屏幕中心的 XY 游戏世界坐标。playerCenterx
和playerCentery
设置为玩家松鼠位置的中心,也是游戏世界坐标。
对于第 172 行,如果中心 X 坐标减去玩家中心 X 坐标大于CAMERASLACK
值,这意味着玩家在相机中心的右侧的像素数比相机松弛允许的要多。camerax
值需要更新,以便玩家松鼠正好在相机松弛的边缘。这就是为什么第 173 行将camerax
设置为playerCenterx + CAMERASLACK – HALF_WINWIDTH
。请注意,更改的是camerax
变量,而不是playerObj['x']
值。我们想要移动相机,而不是玩家。
其他三个if
语句对左、上和下侧采用类似的逻辑。
绘制背景、草地、松鼠和健康仪表
# draw the green background
DISPLAYSURF.fill(GRASSCOLOR)
第 182 行开始绘制显示 Surface 对象内容的代码。首先,第 182 行绘制背景的绿色。这将覆盖 Surface 的所有先前内容,以便我们可以从头开始绘制帧。
# draw all the grass objects on the screen
for gObj in grassObjs:
gRect = pygame.Rect( (gObj['x'] - camerax,
gObj['y'] - cameray,
gObj['width'],
gObj['height']) )
DISPLAYSURF.blit(GRASSIMAGES[gObj['grassImage']], gRect)
第 185 行的for
循环遍历grassObjs
列表中的所有草地对象,并从中存储的 x、y、宽度和高度信息创建一个 Rect 对象。这个 Rect 对象存储在一个名为gRect
的变量中。在第 190 行,gRect
在blit()
方法调用中用于在显示 Surface 上绘制草地图像。请注意,gObj['grassImage']
只包含一个整数,它是GRASSIMAGES
的索引。GRASSIMAGES
是一个包含所有草地图像的 Surface 对象的列表。Surface 对象占用的内存比单个整数多得多,并且所有具有相似gObj['grassImage']
值的草地对象看起来都是相同的。因此,只有将每个草地图像存储一次在GRASSIMAGES
中,并简单地在草地对象本身中存储整数,才有意义。
# draw the other squirrels
for sObj in squirrelObjs:
sObj['rect'] = pygame.Rect( (sObj['x'] - camerax,
sObj['y'] - cameray - getBounceAmount(sObj['bounce'], sObj['bouncerate'], sObj['bounceheight']),
sObj['width'],
sObj['height']) )
DISPLAYSURF.blit(sObj['surface'], sObj['rect'])
绘制所有敌对松鼠游戏对象的for
循环类似于之前的for
循环,只是它创建的 Rect 对象保存在松鼠字典的'rect'
键的值中。代码之所以这样做是因为我们稍后将使用这个 Rect 对象来检查敌对松鼠是否与玩家松鼠发生了碰撞。
请注意,Rect 构造函数的顶部参数不仅仅是sObj['y'] - cameray
,而是sObj['y'] - cameray - getBounceAmount(sObj['bounce'], sObj['bouncerate'], sObj['bounceheight'])
。getBounceAmount()
函数将返回应该提高的顶部值的像素数。
此外,松鼠图像的 Surface 对象没有共同的列表,就像草地游戏对象和GRASSIMAGES
一样。每个敌对松鼠游戏对象都有自己存储在'surface'
键中的 Surface 对象。这是因为松鼠图像可以按比例缩放到不同的大小。
# draw the player squirrel
flashIsOn = round(time.time(), 1) * 10 % 2 == 1
在绘制草地和敌对松鼠之后,代码将绘制玩家的松鼠。然而,有一种情况下我们会跳过绘制玩家的松鼠。当玩家与较大的敌对松鼠发生碰撞时,玩家会受到伤害并闪烁一小段时间,以表明玩家是暂时无敌的。这种闪烁效果是通过在游戏循环的一些迭代中绘制玩家松鼠但在其他迭代中不绘制来实现的。
玩家松鼠将在游戏循环迭代中绘制十分之一秒,然后在游戏循环迭代中的十分之一秒内不绘制。只要玩家是无敌的(在代码中意味着invulnerableMode
变量设置为True
),我们的代码将使闪烁持续两秒,因为2
存储在第 25 行的INVULNTIME
常量变量中。
为了确定闪烁是否打开,第 202 行从time.time()
获取当前时间。让我们使用这个函数调用返回1323926893.622
的例子。这个值传递给round()
,它将其四舍五入到小数点后一位(因为1
作为round()
的第二个参数传递)。这意味着round()
将返回值1323926893.6
。
然后将这个值乘以10
,变成13239268936
。一旦我们将其作为整数,我们可以首先使用在“记忆拼图”章节中讨论的“模二”技巧来查看它是偶数还是奇数。13239268936 % 2
计算结果为0
,这意味着flashIsOn
将被设置为False
,因为0 == 1
是False
。
实际上,time.time()
将继续返回值,最终将False
放入flashIsOn
,直到1323926893.700
,即下一个十分之一秒。这就是为什么flashIsOn
变量在十分之一秒内将不断为False
,然后在下一个十分之一秒内为True
(无论在那十分之一秒内发生多少次迭代)。
if not gameOverMode and not (invulnerableMode and flashIsOn):
playerObj['rect'] = pygame.Rect( (playerObj['x'] - camerax,
playerObj['y'] – cameray - getBounceAmount(playerObj['bounce'], BOUNCERATE, BOUNCEHEIGHT),
playerObj['size'],
playerObj['size']) )
DISPLAYSURF.blit(playerObj['surface'], playerObj['rect'])
在绘制玩家松鼠之前必须有三件事是True
。游戏必须正在进行中(即gameOverMode
为False
),玩家不能是无敌的,也不能在闪烁(即invulnerableMode
和flashIsOn
为False
)。
绘制玩家松鼠的代码几乎与绘制敌对松鼠的代码相同。
# draw the health meter
drawHealthMeter(playerObj['health'])
drawHealthMeter()
函数在屏幕左上角绘制指示器,告诉玩家玩家松鼠在死亡之前可以被击中多少次。这个函数将在本章后面解释。
事件处理循环
for event in pygame.event.get(): # event handling loop
if event.type == QUIT:
terminate()
事件处理循环中首先检查的是是否生成了QUIT
事件。如果是,则应终止程序。
elif event.type == KEYDOWN:
if event.key in (K_UP, K_w):
moveDown = False
moveUp = True
elif event.key in (K_DOWN, K_s):
moveUp = False
moveDown = True
如果按下了上下箭头键(或它们的 WASD 等效键),则该方向的移动变量(moveRight
,moveDown
等)应设置为True
,相反方向的移动变量应设置为False
。
elif event.key in (K_LEFT, K_a):
moveRight = False
moveLeft = True
if playerObj['facing'] == RIGHT: # change player image
playerObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (playerObj['size'], playerObj['size']))
playerObj['facing'] = LEFT
elif event.key in (K_RIGHT, K_d):
moveLeft = False
moveRight = True
if playerObj['facing'] == LEFT: # change player image
playerObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (playerObj['size'], playerObj['size']))
playerObj['facing'] = RIGHT
moveLeft
和moveRight
变量在按下左箭头或右箭头键时也应该被设置。此外,playerObj['facing']
中的值应该更新为LEFT
或RIGHT
。如果玩家松鼠现在面对着一个新的方向,playerObj['surface']
的值应该被替换为正确缩放的松鼠面对新方向的图像。
如果按下左箭头键,则运行第 229 行,并检查玩家松鼠是否面向右侧。如果是这样,那么玩家松鼠图像的新缩放表面对象将存储在playerObj['surface']
中。第 232 行的elif
语句处理相反的情况。
elif winMode and event.key == K_r:
return
如果玩家通过变得足够大而赢得了游戏(在这种情况下,winMode
将被设置为True
),并且按下了 R 键,则runGame()
应该返回。这将结束当前游戏,并且下一次调用runGame()
时将开始新游戏。
elif event.type == KEYUP:
# stop moving the player's squirrel
if event.key in (K_LEFT, K_a):
moveLeft = False
elif event.key in (K_RIGHT, K_d):
moveRight = False
elif event.key in (K_UP, K_w):
moveUp = False
elif event.key in (K_DOWN, K_s):
moveDown = False
如果玩家松开任何箭头键或 WASD 键,则代码应该将该方向的移动变量设置为False
。这将阻止松鼠继续朝着那个方向移动。
elif event.key == K_ESCAPE:
terminate()
如果按下的键是 Esc 键,则终止程序。
移动玩家,并考虑反弹
if not gameOverMode:
# actually move the player
if moveLeft:
playerObj['x'] -= MOVERATE
if moveRight:
playerObj['x'] += MOVERATE
if moveUp:
playerObj['y'] -= MOVERATE
if moveDown:
playerObj['y'] += MOVERATE
在第 255 行的if
语句中的代码只有在游戏没有结束时才会移动玩家的松鼠。(这就是为什么在玩家的松鼠死亡后按箭头键没有效果。)根据哪个移动变量设置为True
,playerObj
字典的playerObj['x']
和playerObj['y']
的值应该改变MOVERATE
。(这就是为什么MOVERATE
中的较大值会使松鼠移动得更快。)
if (moveLeft or moveRight or moveUp or moveDown) or playerObj['bounce'] != 0:
playerObj['bounce'] += 1
if playerObj['bounce'] > BOUNCERATE:
playerObj['bounce'] = 0 # reset bounce amount
playerObj['bounce']
中的值跟踪玩家在反弹中的位置。这个变量存储一个从0
到BOUNCERATE
的整数值。就像敌对松鼠的反弹值一样,playerObj['bounce']
值为0
意味着玩家松鼠在反弹开始时,值为BOUNCERATE
意味着玩家松鼠在反弹结束时。
玩家松鼠在玩家移动时或者如果玩家停止移动但松鼠还没有完成当前的反弹时会反弹。这个条件在第 266 行的if
语句中捕获。如果任何移动变量设置为True
或当前的playerObj['bounce']
不是0
(这意味着玩家当前正在反弹),则应在第 267 行递增该变量。
因为playerObj['bounce']
变量应该只在0
到BOUNCERATE
的范围内,如果递增它使其大于BOUNCERATE
,则应将其重置为0
。
碰撞检测:吃或被吃
# check if the player has collided with any squirrels
for i in range(len(squirrelObjs)-1, -1, -1):
sqObj = squirrelObjs[i]
第 273 行的for
循环将在squirrelObjs
中的每个敌对松鼠游戏对象上运行代码。请注意,第 273 行中range()
的参数从squirrelObjs
的最后一个索引开始递减。这是因为此for
循环中的代码可能会删除其中一些敌对松鼠游戏对象(如果玩家的松鼠最终吃掉它们),因此重要的是从末尾向前迭代。之前解释过的原因在“在删除列表中的项目时,以相反顺序迭代列表”部分中已经解释过。
if 'rect' in sqObj and playerObj['rect'].colliderect(sqObj['rect']):
# a player/squirrel collision has occurred
277.
if sqObj['width'] * sqObj['height'] <= playerObj['size']**2:
# player is larger and eats the squirrel
playerObj['size'] += int( (sqObj['width'] * sqObj['height'])**0.2 ) + 1
del squirrelObjs[i]
如果玩家的松鼠与其碰撞的敌对松鼠的大小相等或更大,则玩家的松鼠将吃掉那只松鼠并变大。在玩家对象的'size'
键中添加的数字(即增长)是根据第 280 行的敌对松鼠的大小计算的。下面是显示不同大小松鼠的增长的图表。请注意,更大的松鼠会导致更多的增长:
因此,根据图表,吃掉一个宽度和高度为 45(即 1600 像素)的松鼠会使玩家变宽 5 像素,变高 5 像素。
第 281 行从squirrelObjs
列表中删除了被吃掉的松鼠对象,这样它就不会再出现在屏幕上或更新其位置。
if playerObj['facing'] == LEFT:
playerObj['surface'] = pygame.transform.scale(L_SQUIR_IMG, (playerObj['size'], playerObj['size']))
if playerObj['facing'] == RIGHT:
playerObj['surface'] = pygame.transform.scale(R_SQUIR_IMG, (playerObj['size'], playerObj['size']))
玩家的松鼠形象现在需要更新,因为松鼠变大了。这可以通过将原始松鼠图像传递给pygame.transform.scale()
函数来完成,该函数将返回图像的放大版本。根据playerObj['facing']
是否等于LEFT
或RIGHT
来确定我们将哪个原始松鼠图像传递给函数。
if playerObj['size'] > WINSIZE:
winMode = True # turn on "win mode"
玩家赢得游戏的方式是使松鼠的大小大于WINSIZE
常量变量中存储的整数。如果是这样,winMode
变量将设置为True
。此函数的其他部分将处理显示祝贺文本并检查玩家是否按下 R 键重新开始游戏。
elif not invulnerableMode:
# player is smaller and takes damage
invulnerableMode = True
invulnerableStartTime = time.time()
playerObj['health'] -= 1
if playerObj['health'] == 0:
gameOverMode = True # turn on "game over mode"
gameOverStartTime = time.time()
如果玩家的面积不等于或大于敌对松鼠的面积,并且invulnerableMode
没有设置为True
,那么玩家将受到与这只更大松鼠碰撞的伤害。
为了防止玩家立即受到同一只松鼠的多次伤害,我们将通过在第 293 行将invulnerableMode
设置为True
来使玩家暂时免疫进一步的松鼠攻击。第 294 行将invulnerableStartTime
设置为当前时间(由time.time()
返回),以便第 133 行和第 134 行知道何时将invulnerableMode
设置为False
。
第 295 行将玩家的生命值减少1
。因为玩家的生命值现在可能为0
,所以第 296 行检查这一点,如果是,则将gameOverMode
设置为True
,并将gameOverStartTime
设置为当前时间。
游戏结束画面
else:
# game is over, show "game over" text
DISPLAYSURF.blit(gameOverSurf, gameOverRect)
if time.time() - gameOverStartTime > GAMEOVERTIME:
return # end the current game
当玩家死亡时,屏幕上将显示“游戏结束”文本(在gameOverSurf
变量中的 Surface 对象上),显示的时间为GAMEOVERTIME
常量中的秒数。一旦经过了这段时间,runGame()
函数将返回。
这样一来,玩家死亡后,在下一局游戏开始之前,敌对松鼠可以继续动画并在屏幕上移动几秒钟。《松鼠吃松鼠》的“游戏结束画面”不会等到玩家按键再开始新游戏。
获胜
# check if the player has won.
if winMode:
DISPLAYSURF.blit(winSurf, winRect)
DISPLAYSURF.blit(winSurf2, winRect2)
pygame.display.update()
FPSCLOCK.tick(FPS)
如果玩家达到一定的大小(由WINSIZE
常量决定),则在第 289 行将winMode
变量设置为True
。当玩家获胜时,屏幕上会出现“你已经获得 OMEGA 松鼠!”文本(存储在winSurf
变量中的 Surface 对象)和“(按r
重新开始)”文本(存储在winSurf2
变量中的 Surface 对象)。游戏会一直进行,直到用户按下 R 键,此时程序执行将从runGame()
返回。R 键的事件处理代码在第 238 行和第 239 行完成。
绘制图形健康仪表
def drawHealthMeter(currentHealth):
for i in range(currentHealth): # draw red health bars
pygame.draw.rect(DISPLAYSURF, RED, (15, 5 + (10 * MAXHEALTH) - i * 10, 20, 10))
for i in range(MAXHEALTH): # draw the white outlines
pygame.draw.rect(DISPLAYSURF, WHITE, (15, 5 + (10 * MAXHEALTH) - i * 10, 20, 10), 1)
要绘制健康仪表,首先在第 317 行的for
循环中绘制填充的红色矩形以表示玩家的健康量。然后在第 319 行的for
循环中,为玩家可能拥有的所有健康量(存储在MAXHEALTH
常量中的整数值)绘制一个未填充的白色矩形。请注意,pygame.display.update()
函数在drawHealthMeter()
中未被调用。
相同的旧terminate()
函数
def terminate():
pygame.quit()
sys.exit()
terminate()
函数与以前的游戏程序中的相同。
正弦函数的数学
def getBounceAmount(currentBounce, bounceRate, bounceHeight):
# Returns the number of pixels to offset based on the bounce.
# Larger bounceRate means a slower bounce.
# Larger bounceHeight means a higher bounce.
# currentBounce will always be less than bounceRate
return int(math.sin( (math.pi / float(bounceRate)) * currentBounce ) * bounceHeight)
有一个数学函数(类似于编程中的函数,因为它们都基于其参数“返回”或“评估”为一个数字)称为正弦(发音类似于“标志”,通常缩写为“sin”)。你可能在数学课上学过它,但如果你没有,这里将会解释。Python 将这个数学函数作为math
模块中的 Python 函数。你可以将 int 或 float 值传递给math.sin()
,它将返回一个称为“正弦值”的浮点值。
在交互式 shell 中,让我们看看math.sin()
对一些值返回什么:
>>> import math
>>> math.sin(1)
0.8414709848078965
>>> math.sin(2)
0.90929742682568171
>>> math.sin(3)
0.14112000805986721
>>> math.sin(4)
-0.7568024953079282
>>> math.sin(5)
-0.95892427466313845
预测math.sin()
根据我们传递的值返回什么值似乎非常困难(这可能让你想知道math.sin()
有什么用)。但是,如果我们在图表上绘制整数1
到10
的正弦值,我们会得到这个:
你可以在math.sin()
返回的值中看到一种波浪形的模式。如果你找出整数之外的更多数字的正弦值(例如1.5
和2.5
等),然后用线连接这些点,你就可以更容易地看到这种波浪形的模式:
实际上,如果你不断添加更多的数据点到这个图表中,你会看到正弦波看起来像这样:
请注意,math.sin(0)
返回0
,然后逐渐增加,直到math.sin(3.14 / 2)
返回1
,然后开始减少,直到math.sin(3.14)
返回0
。数字3.14
是数学中的一个特殊数字,称为圆周率(与美味的“派”发音相同)。这个值也存储在math
模块中的常量变量pi
中(这就是为什么第 333 行使用变量math.pi
),它在技术上是浮点值3.1415926535897931
。由于我们想要松鼠看起来像波浪,我们只关注math.sin()
对参数0
到3.14
的返回值:
让我们看看getBounceAmount()
的返回值,并确切地弄清楚它的作用。
return int(math.sin( (math.pi / float(bounceRate)) * currentBounce ) * bounceHeight)
请记住,在第 21 行,我们将BOUNCERATE
常量设置为6
。这意味着我们的代码将只将playerObj['bounce']
从0
增加到6
,并且我们希望将从0
到3.14
的浮点值范围分成6
部分,我们可以通过简单的除法来实现:3.14 / 6 = 0.5235
。在图表上,“正弦波动跳跃”的3.14
长度的6
个相等部分中的每个部分都是0.5235
。
您可以看到当playerObj['bounce']
为3
(在0
和6
之间)时,传递给math.sin()
调用的值为math.pi / 6 * 3
,即1.5707
(在0
和3.1415
之间的中间值)。然后math.sin(1.5707)
将返回1.0
,这是正弦波的最高点(正弦波的最高点发生在波的一半处)。
随着playerObj['bounce']
的值递增,getBounceAmount()
函数将返回与正弦波从0
到3.14
具有相同弹跳形状的值。如果要使弹跳更高,则增加BOUNCEHEIGHT
常量。如果要使弹跳更慢,则增加BOUNCERATE
常量。
正弦函数是三角学数学中的一个概念。如果您想了解更多关于正弦波的信息,维基百科页面有详细信息:en.wikipedia.org/wiki/Sine
与 Python 2 版本的向后兼容性
我们调用float()
将bounceRate
转换为浮点数的原因很简单,即使这样程序也可以在 Python 2 版本中运行。在 Python 3 版本中,即使操作数都是整数,除法运算符也将评估为浮点值,如下所示:
>>> # Python version 3
...
>>> 10 / 5
2.0
>>> 10 / 4
2.5
>>>
然而,在 Python 2 版本中,如果操作数中有一个是整数,则/
除法运算符只会评估为浮点值。如果两个操作数都是整数,则 Python 2 的除法运算符将评估为整数值(如有需要四舍五入),如下所示:
>>> # Python version 2
...
>>> 10 / 5
2
>>> 10 / 4
2
>>> 10 / 4.0
2.5
>>> 10.0 / 4
2.5
>>> 10.0 / 4.0
2.5
但是,如果我们总是使用float()
函数将其中一个值转换为浮点值,那么无论哪个版本的 Python 运行此源代码,除法运算符都将评估为浮点值。进行这些更改以使我们的代码与旧版本的软件兼容称为向后兼容性。保持向后兼容性很重要,因为并非每个人都会始终运行最新版本的软件,您希望确保您编写的代码与尽可能多的计算机兼容。
您并非总是可以使您的 Python 3 代码向后兼容 Python 2,但如果可能的话,您应该这样做。否则,当使用 Python 2 的人尝试运行您的游戏时,将会收到错误消息,并认为您的程序有错误。
Python 2 和 Python 3 之间的一些区别列表可以在inventwithpython.com/appendixa.html
找到。
getRandomVelocity()
函数
def getRandomVelocity():
speed = random.randint(SQUIRRELMINSPEED, SQUIRRELMAXSPEED)
if random.randint(0, 1) == 0:
return speed
else:
return -speed
getRandomVelocity()
函数用于随机确定敌对松鼠的移动速度。此速度的范围设置在SQUIRRELMINSPEED
和SQUIRRELMAXSPEED
常量中,但除此之外,速度要么为负(表示松鼠向左或向上移动),要么为正(表示松鼠向右或向下移动)。随机速度为正或负的机会是五五开。
寻找添加新松鼠和草的位置
def getRandomOffCameraPos(camerax, cameray, objWidth, objHeight):
# create a Rect of the camera view
cameraRect = pygame.Rect(camerax, cameray, WINWIDTH, WINHEIGHT)
while True:
x = random.randint(camerax - WINWIDTH, camerax + (2 * WINWIDTH))
y = random.randint(cameray - WINHEIGHT, cameray + (2 * WINHEIGHT))
349. # create a Rect object with the random coordinates and use colliderect()
# to make sure the right edge isn't in the camera view.
objRect = pygame.Rect(x, y, objWidth, objHeight)
if not objRect.colliderect(cameraRect):
return x, y
当游戏世界中创建新的松鼠或草对象时,我们希望它在活动区域内(靠近玩家的松鼠),但不在摄像机的视野内(这样它就不会突然出现在屏幕上)。为此,我们创建一个代表摄像机区域的 Rect 对象(使用camerax
,cameray
,WINWIDTH
和WINHEIGHT
常量)。
接下来,我们随机生成 XY 坐标的数字,这些数字将位于活动区域内。活动区域的左边和顶部边缘分别为camerax - WINWIDTH
和cameray - WINHEIGHT
。活动区域的宽度和高度也是WINWIDTH
和WINHEIGHT
的三倍,如您在此图像中所见(其中WINWIDTH
设置为 640 像素,WINHEIGHT
设置为 480 像素):
这意味着右边和底边将分别为camerax + (2 * WINWIDTH)
和cameray + (2 * WINHEIGHT)
。第 352 行将检查随机 XY 坐标是否会与相机视图的矩形对象发生碰撞。如果没有,那么这些坐标将被返回。如果有,那么第 346 行的while
循环将继续生成新的坐标,直到找到可接受的坐标为止。
创建敌对松鼠数据结构
def makeNewSquirrel(camerax, cameray):
sq = {}
generalSize = random.randint(5, 25)
multiplier = random.randint(1, 3)
sq['width'] = (generalSize + random.randint(0, 10)) * multiplier
sq['height'] = (generalSize + random.randint(0, 10)) * multiplier
sq['x'], sq['y'] = getRandomOffCameraPos(camerax, cameray, sq['width'], sq['height'])
sq['movex'] = getRandomVelocity()
sq['movey'] = getRandomVelocity()
创建敌对松鼠游戏对象类似于制作草地游戏对象。每个敌对松鼠的数据也存储在字典中。第 360 行和 361 行将宽度和高度设置为随机大小。使用generalSize
变量是为了确保每只松鼠的宽度和高度不会相差太大。否则,对于宽度和高度完全随机的数字可能会给我们非常高而瘦的松鼠或非常短而宽的松鼠。松鼠的宽度和高度是这个一般大小,加上从0
到10
的随机数(用于轻微变化),然后乘以multiplier
变量。
松鼠的原始 XY 坐标位置将是相机无法看到的随机位置,以防止松鼠只是在屏幕上“突然出现”。
速度和方向也是由getRandomVelocity()
函数随机选择的。
翻转松鼠图像
if sq['movex'] < 0: # squirrel is facing left
sq['surface'] = pygame.transform.scale(L_SQUIR_IMG, (sq['width'], sq['height']))
else: # squirrel is facing right
sq['surface'] = pygame.transform.scale(R_SQUIR_IMG, (sq['width'], sq['height']))
sq['bounce'] = 0
sq['bouncerate'] = random.randint(10, 18)
sq['bounceheight'] = random.randint(10, 50)
return sq
L_SQUIR_IMG
和R_SQUIR_IMG
常量包含左侧和右侧松鼠图像的 Surface 对象。将使用pygame.transform.scale()
函数创建新的 Surface 对象,以匹配松鼠的宽度和高度(分别存储在sq['width']
和sq['height']
中)。
之后,三个与弹跳相关的值是随机生成的(除了sq['bounce']
,它是0
,因为松鼠总是从弹跳的开始),并且在第 372 行返回字典。
创建草地数据结构
def makeNewGrass(camerax, cameray):
gr = {}
gr['grassImage'] = random.randint(0, len(GRASSIMAGES) - 1)
gr['width'] = GRASSIMAGES[0].get_width()
gr['height'] = GRASSIMAGES[0].get_height()
gr['x'], gr['y'] = getRandomOffCameraPos(camerax, cameray, gr['width'], gr['height'])
gr['rect'] = pygame.Rect( (gr['x'], gr['y'], gr['width'], gr['height']) )
return gr
草地游戏对象是带有通常的'x'
、'y'
、'width'
、'height'
和'rect'
键的字典,但也有一个'grassImage'
键,它是0
到GRASSIMAGES
列表长度减一的数字。这个数字将决定草地游戏对象使用什么图像。例如,如果草地对象的'grassImage'
键的值为3
,那么它将使用存储在GRASSIMAGES[3]
的 Surface 对象作为其图像。
检查是否在活动区域外
def isOutsideActiveArea(camerax, cameray, obj):
# Return False if camerax and cameray are more than
# a half-window length beyond the edge of the window.
boundsLeftEdge = camerax - WINWIDTH
boundsTopEdge = cameray - WINHEIGHT
boundsRect = pygame.Rect(boundsLeftEdge, boundsTopEdge, WINWIDTH * 3, WINHEIGHT * 3)
objRect = pygame.Rect(obj['x'], obj['y'], obj['width'], obj['height'])
return not boundsRect.colliderect(objRect)
如果您传递给isOutsideActiveArea()
的对象在由camerax
和cameray
参数规定的“活动区域”之外,它将返回True
。请记住,活动区域是相机视图周围大小为相机视图的区域(其宽度和高度由WINWIDTH
和WINHEIGHT
设置),如下所示:
我们可以创建一个代表活动区域的 Rect 对象,通过将camerax - WINWIDTH
作为左边值和cameray - WINHEIGHT
作为顶部边值,然后WINWIDTH * 3
和WINHEIGHT * 3
作为宽度和高度。一旦我们将活动区域表示为 Rect 对象,我们就可以使用colliderect()
方法来确定obj
参数中的对象是否与活动区域 Rect 对象发生碰撞(即在其中)。
由于玩家松鼠、敌对松鼠和草地对象都有'x'
、'y'
、'width'
和'height'
键,因此isOutsideActiveArea()
代码可以处理任何类型的这些游戏对象。
if __name__ == '__main__':
main()
最后,在定义了所有函数之后,程序将运行main()
函数并启动游戏。
总结
《松鼠吃松鼠》是我们的第一个游戏,其中有多个敌人同时在棋盘上移动。拥有多个敌人的关键是使用具有相同键的字典值,以便在游戏循环的迭代中对它们中的每一个运行相同的代码。
相机的概念也被引入了。在我们之前的游戏中并不需要相机,因为整个游戏世界都可以放在一个屏幕上。然而,当你制作自己的游戏涉及到玩家在一个大型游戏世界中移动时,你将需要编写代码来处理游戏世界坐标系和屏幕像素坐标系之间的转换。
最后,数学正弦函数被引入,以实现真实的松鼠跳跃(无论每次跳跃有多高或多远)。你并不需要了解很多数学知识来进行编程。在大多数情况下,只需要了解加法、乘法和负数就可以了。然而,如果你学习数学,你会经常发现数学有很多用途,可以让你的游戏更酷。
为了进行额外的编程练习,你可以从invpy.com/buggy/squirrel
下载 Squirrel Eat Squirrel 的有 bug 版本,并尝试弄清楚如何修复这些 bug。