贪吃虫是Nibbles的克隆。玩家开始控制一个不断在屏幕上移动的短蠕虫。玩家无法停止或减慢蠕虫,但他们可以控制它转向的方向。红苹果随机出现在屏幕上,玩家必须移动蠕虫以使其吃掉苹果。每次蠕虫吃掉一个苹果,蠕虫就会增长一个段,并且新的苹果会随机出现在屏幕上。如果蠕虫撞到自己或屏幕边缘,游戏就结束了。
如果你玩游戏一点点,你会注意到苹果和蠕虫身体的部分总是沿着网格线。我们将这个网格中的每个正方形称为一个单元格(这不一定是网格中的空间的称呼,这只是我想出来的一个名字)。这些单元格有自己的笛卡尔坐标系,其中(0,0)是左上角的单元格,(31,23)是右下角的单元格。
#RGBWHITE=(255,255,255)BLACK=(0,0,0)RED=(255,0,0)GREEN=(0,255,0)DARKGREEN=(0,155,0)DARKGRAY=(40,40,40)BGCOLOR=BLACKUP='up'DOWN='down'LEFT='left'RIGHT='right'HEAD=0#syntacticsugar:indexoftheworm'shead在第19到32行设置了更多的常量。HEAD常量将在本章后面解释。
defmain():globalFPSCLOCK,DISPLAYSURF,BASICFONTpygame.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()whileTrue:runGame()showGameOverScreen()在贪吃虫游戏程序中,我们将代码的主要部分放在一个名为runGame()的函数中。这是因为我们只想在程序启动时显示“开始画面”(旋转的“贪吃虫”文本动画)一次(通过调用showStartScreen()函数)。然后我们想调用runGame(),这将开始一场贪吃虫游戏。当玩家的蠕虫撞到墙壁或自己并导致游戏结束时,此函数将返回。
在那时,我们将通过调用showGameOverScreen()显示游戏结束画面。当该函数调用返回时,循环将返回到开始,并再次调用runGame()。第44行的while循环将一直循环,直到程序终止。
defrunGame():#Setarandomstartpoint.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#Starttheappleinarandomplace.apple=getRandomLocation()在游戏开始时,我们希望蠕虫在随机位置开始(但不要太靠近棋盘的边缘),因此我们在startx和starty中存储一个随机坐标。(请记住,CELLWIDTH和CELLHEIGHT是窗口的宽度和高度,而不是像素的宽度和高度)。
蠕虫的身体将存储在一个字典值列表中。每个蠕虫身体段都将有一个字典值。该字典将具有'x'和'y'键,用于该身体段的XY坐标。身体的头部将位于startx和starty。其他两个身体段将位于头部的左侧一个和两个单元格。
蠕虫的头部将始终是wormCoords[0]的身体部分。为了使这段代码更易读,我们在第32行将HEAD常量设置为0,这样我们就可以使用wormCoords[HEAD]而不是wormCoords[0]。
whileTrue:#maingameloopforeventinpygame.event.get():#eventhandlingloopifevent.type==QUIT:terminate()elifevent.type==KEYDOWN:if(event.key==K_LEFTorevent.key==K_a)anddirection!=RIGHT:direction=LEFTelif(event.key==K_RIGHTorevent.key==K_d)anddirection!=LEFT:direction=RIGHTelif(event.key==K_UPorevent.key==K_w)anddirection!=DOWN:direction=UPelif(event.key==K_DOWNorevent.key==K_s)anddirection!=UP:direction=DOWNelifevent.key==K_ESCAPE:terminate()第61行是主游戏循环的开始,第62行是事件处理循环的开始。如果事件是QUIT事件,那么我们调用terminate()(我们已经在之前的游戏程序中定义了相同的terminate()函数)。
否则,如果事件是KEYDOWN事件,那么我们检查按下的键是否是箭头键或者WASD键。我们希望进行额外的检查,以防蛇转向自身。例如,如果蛇正在向左移动,那么如果玩家意外按下右箭头键,蛇就会立即向右移动并撞到自己。
这就是为什么我们要检查direction变量的当前值。这样,如果玩家意外按下一个会导致蛇立即撞到墙壁的箭头键,我们就忽略那个按键。
#checkifthewormhashititselfortheedgeifwormCoords[HEAD]['x']==-1orwormCoords[HEAD]['x']==CELLWIDTHorwormCoords[HEAD]['y']==-1orwormCoords[HEAD]['y']==CELLHEIGHT:return#gameoverforwormBodyinwormCoords[1:]:ifwormBody['x']==wormCoords[HEAD]['x']andwormBody['y']==wormCoords[HEAD]['y']:return#gameover当蛇头移出网格边缘或者蛇头移动到已经被其他身体段占据的单元格时,蛇就撞到了。
我们可以通过检查蛇头是否移出了网格的边缘来判断。方法是看蛇头的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()函数中返回来结束游戏。
#checkifwormhaseatenanapplyifwormCoords[HEAD]['x']==apple['x']andwormCoords[HEAD]['y']==apple['y']:#don'tremoveworm'stailsegmentapple=getRandomLocation()#setanewapplesomewhereelse:delwormCoords[-1]#removeworm'stailsegment我们对蛇头和苹果的XY坐标之间进行类似的碰撞检测。如果它们匹配,我们将苹果的坐标设置为一个随机的新位置(从getRandomLocation()的返回值中获取)。
如果蛇头没有与苹果碰撞,那么我们删除wormCoords列表中的最后一个身体段。记住,负整数索引从列表末尾开始计数。所以0是列表中第一个项目的索引,1是第二个项目的索引,-1是列表中的最后一个项目的索引,-2是倒数第二个项目的索引。
第91到100行的代码(在“移动蛇”部分中描述)将根据蛇的移动方向在wormCoords中添加一个新的身体段(用于蛇头)。这将使蛇变长一个段。当蛇吃掉苹果时不删除最后一个身体段,蛇的整体长度增加了一个。但是当第89行删除最后一个身体段时,大小保持不变,因为紧接着会添加一个新的蛇头段。
#movethewormbyaddingasegmentinthedirectionitismovingifdirection==UP:newHead={'x':wormCoords[HEAD]['x'],'y':wormCoords[HEAD]['y']-1}elifdirection==DOWN:newHead={'x':wormCoords[HEAD]['x'],'y':wormCoords[HEAD]['y']+1}elifdirection==LEFT:newHead={'x':wormCoords[HEAD]['x']-1,'y':wormCoords[HEAD]['y']}elifdirection==RIGHT:newHead={'x':wormCoords[HEAD]['x']+1,'y':wormCoords[HEAD]['y']}wormCoords.insert(0,newHead)为了移动蛇,我们在wormCoords列表的开头添加一个新的身体段。因为身体段被添加到列表的开头,它将成为新的蛇头。新蛇头的坐标将紧邻旧蛇头的坐标。无论是向X坐标还是Y坐标添加还是减去1取决于蛇的移动方向。
新的蛇头段被添加到wormCoords中,使用insert()列表方法在第100行。
与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绘制到实际的计算机屏幕上。
defdrawPressKeyMsg():pressKeySurf=BASICFONT.render('Pressakeytoplay.',True,DARKGRAY)pressKeyRect=pressKeySurf.get_rect()pressKeyRect.topleft=(WINDOWWIDTH-200,WINDOWHEIGHT-30)DISPLAYSURF.blit(pressKeySurf,pressKeyRect)当开始屏幕动画正在播放或游戏结束屏幕正在显示时,右下角会有一些小文本,上面写着“按键开始游戏”。我们不想在showStartScreen()和showGameOverScreen()中重复代码,所以我们将它放在一个单独的函数中,并从showStartScreen()和showGameOverScreen()中调用该函数。
defcheckForKeyPress():iflen(pygame.event.get(QUIT))>0:terminate()keyUpEvents=pygame.event.get(KEYUP)iflen(keyUpEvents)==0:returnNoneifkeyUpEvents[0].key==K_ESCAPE:terminate()returnkeyUpEvents[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()返回的列表中返回第一个按键事件对象。
defshowStartScreen():titleFont=pygame.font.Font('freesansbold.ttf',100)titleSurf1=titleFont.render('Wormy!',True,WHITE,DARKGREEN)titleSurf2=titleFont.render('Wormy!',True,GREEN)degrees1=0degrees2=0whileTrue: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()ifcheckForKeyPress():pygame.event.get()#cleareventqueuereturnpygame.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:Widthorheightistoolarge。
degrees1+=3#rotateby3degreeseachframedegrees2+=7#rotateby7degreeseachframe我们旋转两个“Wormy!”文本Surface对象的角度存储在degrees1和degrees2中。在每次动画循环迭代中,我们将degrees1中存储的数字增加3,degrees2增加7。这意味着在下一次动画循环迭代中,白色文本“Wormy!”Surface对象将再次旋转3度,绿色文本“Wormy!”Surface对象将再次旋转7度。这就是为什么一个Surface对象旋转得比另一个慢。
defterminate():pygame.quit()sys.exit()terminate()函数调用pygame.quit()和sys.exit()以正确关闭游戏。它与之前游戏程序中的terminate()函数相同。
defgetRandomLocation():return{'x':random.randint(0,CELLWIDTH-1),'y':random.randint(0,CELLHEIGHT-1)}每当需要苹果的新坐标时,都会调用getRandomLocation()函数。该函数返回一个带有键'x'和'y'的字典,其值设置为随机的XY坐标。
defshowGameOverScreen():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()#clearoutanykeypressesintheeventqueuewhileTrue:ifcheckForKeyPress():pygame.event.get()#cleareventqueuereturn游戏结束文本将一直显示在屏幕上,直到玩家按下键。为了确保玩家不会意外地按下键,我们将在第180行调用pygame.time.wait()来暂停半秒钟。(500参数代表500毫秒的暂停,即半秒钟。)
然后,调用checkForKeyPress(),以便忽略自showGameOverScreen()函数开始以来产生的任何按键事件。这种暂停和丢弃按键事件是为了防止以下情况发生:假设玩家试图在最后一刻转向屏幕边缘,但按键太晚按下并撞到了棋盘的边缘。如果发生这种情况,那么按键按下将会在showGameOverScreen()被调用之后发生,那个按键按下会导致游戏结束屏幕几乎立即消失。接下来的游戏会立即开始,并可能让玩家感到惊讶。添加这个暂停有助于使游戏更加“用户友好”。
绘制分数、蠕虫、苹果和网格的代码都放入了单独的函数中。
defdrawScore(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参数的分数文本。
defdrawWorm(wormCoords):forcoordinwormCoords:x=coord['x']*CELLSIZEy=coord['y']*CELLSIZEwormSegmentRect=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个像素的边距。
defdrawApple(coord):x=coord['x']*CELLSIZEy=coord['y']*CELLSIZEappleRect=pygame.Rect(x,y,CELLSIZE,CELLSIZE)pygame.draw.rect(DISPLAYSURF,RED,appleRect)drawApple()函数与drawWorm()非常相似,只是因为红苹果只是填满单元格的一个矩形,所以函数需要做的就是转换为像素坐标(这就是第206和207行所做的),使用苹果的位置和大小创建Rect对象(第208行),然后将这个Rect对象传递给pygame.draw.rect()函数。
defdrawGrid():forxinrange(0,WINDOWWIDTH,CELLSIZE):#drawverticallinespygame.draw.line(DISPLAYSURF,DARKGRAY,(x,0),(x,WINDOWHEIGHT))foryinrange(0,WINDOWHEIGHT,CELLSIZE):#drawhorizontallinespygame.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))...skippedforbrevity...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))...skippedforbrevity...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()调用中传递的对象不同。
像这样的小事情会使你更难理解你的程序是如何工作的。不仅仅是其他程序员看你的代码会感到困惑。当你在写完几周后再看你自己的代码时,你可能会很难记住它是如何工作的。代码的可读性比在这里和那里节省一些内存更重要。
俄罗斯方块是俄罗斯方块的克隆。不同形状的方块(每个由四个方块组成)从屏幕顶部掉落,玩家必须引导它们下落,形成没有间隙的完整行。当形成完整的一行时,该行消失,上面的每一行都向下移动一行。玩家试图保持形成完整的行,直到屏幕填满,新的下落方块无法适应屏幕。
在这一章中,我已经为游戏程序中的不同事物想出了一组术语。
板-板由10x20个空间组成,方块会落下并堆叠起来。
方块-方块是板上的单个填充的正方形空间。
方块-从板的顶部掉落并且玩家可以旋转和定位的东西。每个方块都有一个形状,由4个方块组成。
形状-形状是游戏中不同类型的方块。形状的名称是T、S、Z、J、L、I和O。
模板-一组形状数据结构的列表,表示了一个形状的所有可能的旋转。这些存储在变量中,名称如S_SHAPE_TEMPLATE或J_SHAPE_TEMPLATE。
着陆-当一个方块已经到达板的底部或者与板上的方块接触时,我们说这个方块已经着陆。在这一点上,下一个方块应该开始下落。
你还需要将背景音乐文件放在与tetromino.py文件相同的文件夹中。你可以从这里下载它们:
MOVESIDEWAYSFREQ=0.15MOVEDOWNFREQ=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来解决这个问题。
#RGBWHITE=(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=BLUEBGCOLOR=BLACKTEXTCOLOR=WHITETEXTSHADOWCOLOR=GRAYCOLORS=(BLUE,GREEN,RED,YELLOW)LIGHTCOLORS=(LIGHTBLUE,LIGHTGREEN,LIGHTRED,LIGHTYELLOW)assertlen(COLORS)==len(LIGHTCOLORS)#eachcolormusthavelightcolor这些块将有四种颜色:蓝色、绿色、红色和黄色。当我们画盒子时,盒子上会有浅色的细节。这意味着我们还需要创建浅蓝色、浅绿色、浅红色和浅黄色。
这四种颜色将存储在名为COLORS(用于正常颜色)和LIGHTCOLORS(用于浅色)的元组中。
['.....','.....','..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变量包含每个可能形状的所有可能旋转。这将是包含我们游戏中所有形状数据的数据结构。
defmain():globalFPSCLOCK,DISPLAYSURF,BASICFONT,BIGFONTpygame.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()函数处理创建一些更多的全局常量,并显示程序运行时出现的开始屏幕。
whileTrue:#gameloopifrandom.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('GameOver')实际游戏的代码都在runGame()中。这里的main()函数只是随机决定要开始播放什么背景音乐(tetrisb.mid或tetrisc.midMIDI音乐文件),然后调用runGame()开始游戏。当玩家失败时,runGame()将返回到main(),然后停止背景音乐并显示游戏结束画面。
当玩家按下键时,显示游戏结束屏幕的showTextScreen()函数将返回。游戏循环将在第169行回到开头,开始另一场游戏。
defrunGame():#setupvariablesforthestartofthegameboard=getBlankBoard()lastMoveDownTime=time.time()lastMoveSidewaysTime=time.time()lastFallTime=time.time()movingDown=False#note:thereisnomovingUpvariablemovingLeft=FalsemovingRight=Falsescore=0level,fallFreq=calculateLevelAndFallFreq(score)fallingPiece=getNewPiece()nextPiece=getNewPiece()在游戏开始之前,棋子开始下落之前,我们需要将一些变量初始化为游戏开始时的值。在第191行,fallingPiece变量将被设置为当前正在下落的可以由玩家旋转的棋子。在第192行,nextPiece变量将被设置为出现在屏幕“下一个”部分的棋子,以便玩家知道在设置下落棋子后下一个棋子是什么。
getNewPiece()得到的棋子通常会被放置在板子的上方一点点,通常部分棋子已经在板子上。但是,如果这是一个无效的位置,因为板子已经填满了(在这种情况下,第201行的isValidPosition()调用将返回False),那么我们知道板子已经满了,玩家应该输掉游戏。当这种情况发生时,runGame()函数将返回。
foreventinpygame.event.get():#eventhandlingloopifevent.type==KEYUP:事件处理循环负责玩家旋转下落棋子、移动下落棋子或暂停游戏时的情况。
代码通过调用DISPLAYSURF.fill(BGCOLOR)来清空显示表面,并停止音乐。调用showTextScreen()函数显示“暂停”文本,并等待玩家按键继续。
elif(event.key==K_LEFTorevent.key==K_a):movingLeft=Falseelif(event.key==K_RIGHTorevent.key==K_d):movingRight=Falseelif(event.key==K_DOWNorevent.key==K_s):movingDown=False松开箭头键(或WASD键)将把movingLeft、movingRight或movingDown变量设置回False,表示玩家不再想朝这些方向移动棋子。稍后的代码将根据这些“移动”变量内的布尔值来处理。请注意,上箭头和W键用于旋转棋子,而不是向上移动棋子。这就是为什么没有movingUp变量。
elifevent.type==KEYDOWN:#movingtheblocksidewaysif(event.key==K_LEFTorevent.key==K_a)andisValidPosition(board,fallingPiece,adjX=-1):fallingPiece['x']-=1movingLeft=TruemovingRight=FalselastMoveSidewaysTime=time.time()当按下左箭头键(并且向左移动是下落棋子的有效移动,由isValidPosition()调用确定)时,我们应该通过将fallingPiece['x']的值减去1来将位置改变为左边一个空格。isValidPosition()函数有名为adjX和adjY的可选参数。通常,isValidPosition()函数检查由第二个参数传递的棋子对象提供的位置。然而,有时我们不想检查棋子当前所在的位置,而是在该位置的几个空格之外。
如果我们传入-1作为adjX(“调整X”的简称),那么它不会检查方块数据结构中位置的有效性,而是检查方块向左移动一个空格后的位置。传入1作为adjX将检查向右移动一个空格的位置。还有一个adjY可选参数。传入-1作为adjY将检查方块当前位置上方一个空格的位置,传入像3这样的值作为adjY将检查方块下方三个空格的位置。
lastMoveSidewaysTime的工作方式就像模拟章节中的lastClickTime变量一样。
elif(event.key==K_RIGHTorevent.key==K_d)andisValidPosition(board,fallingPiece,adjX=1):fallingPiece['x']+=1movingRight=TruemovingLeft=FalselastMoveSidewaysTime=time.time()第231到235行的代码几乎与第225到229行相同,只是处理了当按下右箭头键(或D键)时将下落的方块向右移动的情况。
#rotatingtheblock(ifthereisroomtorotate)elif(event.key==K_UPorevent.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%40>>>1%41>>>2%42>>>3%43>>>5%41>>>6%42>>>7%43>>>8%40>>>ifnotisValidPosition(board,fallingPiece):fallingPiece['rotation']=(fallingPiece['rotation']-1)%len(SHAPES[fallingPiece['shape']])如果新旋转位置无效,因为它与棋盘上已有的一些方块重叠,那么我们希望通过从fallingPiece['rotation']中减去1来将其切换回原始旋转。我们还可以对len(SHAPES[fallingPiece['shape']])取模,以便如果新值为-1,取模将把它改回列表中的最后一个旋转。以下是对负数进行取模的示例:
>>>-1%43elif(event.key==K_q):#rotatetheotherdirectionfallingPiece['rotation']=(fallingPiece['rotation']-1)%len(SHAPES[fallingPiece['shape']])ifnotisValidPosition(board,fallingPiece):fallingPiece['rotation']=(fallingPiece['rotation']+1)%len(SHAPES[fallingPiece['shape']])第242到245行与238到241行的代码做了相同的事情,只是处理了玩家按下Q键旋转方块的情况,这时我们需要从fallingPiece['rotation']中减去1(在第243行完成),而不是加上1。
#movethecurrentblockallthewaydownelifevent.key==K_SPACE:movingDown=FalsemovingLeft=FalsemovingRight=Falseforiinrange(1,BOARDHEIGHT):ifnotisValidPosition(board,fallingPiece,adjY=i):breakfallingPiece['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,因为这是方块在必须触底之前可以下落的最大距离。)
#handlemovingtheblockbecauseofuserinputif(movingLeftormovingRight)andtime.time()-lastMoveSidewaysTime>MOVESIDEWAYSFREQ:ifmovingLeftandisValidPosition(board,fallingPiece,adjX=-1):fallingPiece['x']-=1elifmovingRightandisValidPosition(board,fallingPiece,adjX=1):fallingPiece['x']+=1lastMoveSidewaysTime=time.time()记住,在第227行,如果玩家按下左箭头键,movingLeft变量被设置为True?(在第233行,如果玩家按下右箭头键,movingRight也被设置为True。)如果用户松开这些键,移动变量也会被设置回False`(见第217行和219行)。
如果用户按住键超过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,运行这个简短的程序:
importtimeWAITTIME=4begin=time.time()whileTrue:now=time.time()message='%s,%s,%s'%(begin,now,(now-begin))ifnow-begin>WAITTIME:print(message+'PASSEDWAITTIME!')else:print(message+'Notyet...')time.sleep(0.2)这个程序有一个无限循环,为了终止它,按Ctrl-C。这个程序的输出看起来会像这样:
ifmovingDownandtime.time()-lastMoveDownTime>MOVEDOWNFREQandisValidPosition(board,fallingPiece,adjY=1):fallingPiece['y']+=1lastMoveDownTime=time.time()第272到274行几乎与第265到270行做的事情相同,只是将下落的方块向下移动。这有一个单独的移动变量(movingDown)和“上次”变量(lastMoveDownTime),以及一个不同的“移动频率”变量(MOVEDOWNFREQ)。
如果第279行的条件为True,则表示方块已经落地。调用addToBoard()将使方块成为棋盘数据结构的一部分(以便未来的方块可以落在上面),而removeCompleteLines()调用将处理擦除棋盘上的任何完整行并将方块下拉。removeCompleteLines()函数还返回一个整数值,表示移除了多少行,因此我们将这个数字加到分数上。
因为分数可能已经改变,我们调用calculateLevelAndFallFreq()函数来更新当前级别和方块下落的频率。最后,我们将fallingPiece变量设置为None,表示下一个方块应该成为新的下落方块,并且应该为新的下一个方块生成一个随机的新方块。(这是在游戏循环的开头的第195到199行完成的。)
#drawingeverythingonthescreenDISPLAYSURF.fill(BGCOLOR)drawBoard(board)drawStatus(score,level)drawNextPiece(nextPiece)iffallingPiece!=None:drawPiece(fallingPiece)pygame.display.update()FPSCLOCK.tick(FPS)现在游戏循环已经处理了所有事件并更新了游戏状态,游戏循环只需要将游戏状态绘制到屏幕上。大部分绘制工作由其他函数处理,因此游戏循环代码只需要调用这些函数。然后调用pygame.display.update()使显示表面出现在实际的计算机屏幕上,tick()方法调用会添加一个轻微的暂停,以防游戏运行得太快。
defmakeTextObjs(text,font,color):surf=font.render(text,True,color)returnsurf,surf.get_rect()makeTextObjs()函数只是为我们提供了一个快捷方式。给定文本、字体对象和颜色对象,它为我们调用render()并返回这个文本的Surface和Rect对象。这样就省去了我们每次需要它们时编写创建Surface和Rect对象的代码。
defterminate():pygame.quit()sys.exit()terminate()函数与以前的游戏程序中的工作方式相同。
defcheckForKeyPress():#GothrougheventqueuelookingforaKEYUPevent.#GrabKEYDOWNeventstoremovethemfromtheeventqueue.checkForQuit()foreventinpygame.event.get([KEYDOWN,KEYUP]):ifevent.type==KEYDOWN:continuereturnevent.keyreturnNonecheckForKeyPress()函数的工作方式几乎与贪吃虫游戏中的工作方式相同。首先它调用checkForQuit()来处理任何QUIT事件(或者专门用于Esc键的KEYUP事件),如果有的话就终止程序。然后它从事件队列中提取所有的KEYUP和KEYDOWN事件。它忽略任何KEYDOWN事件(KEYDOWN只被指定给pygame.event.get()以清除事件队列中的这些事件)。
如果事件队列中没有KEYUP事件,则该函数返回None。
defshowTextScreen(text):#Thisfunctiondisplayslargetextinthe#centerofthescreenuntilakeyispressed.#DrawthetextdropshadowtitleSurf,titleRect=makeTextObjs(text,BIGFONT,TEXTSHADOWCOLOR)titleRect.center=(int(WINDOWWIDTH/2),int(WINDOWHEIGHT/2))DISPLAYSURF.blit(titleSurf,titleRect)#DrawthetexttitleSurf,titleRect=makeTextObjs(text,BIGFONT,TEXTCOLOR)titleRect.center=(int(WINDOWWIDTH/2)-3,int(WINDOWHEIGHT/2)-3)DISPLAYSURF.blit(titleSurf,titleRect)#Drawtheadditional"Pressakeytoplay."text.pressKeySurf,pressKeyRect=makeTextObjs('Pressakeytoplay.',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()将用于开始屏幕、游戏结束屏幕,以及暂停屏幕(暂停屏幕在本章后面解释)。
whilecheckForKeyPress()==None:pygame.display.update()FPSCLOCK.tick()我们希望文本保持在屏幕上,直到用户按下键。这个小循环将不断调用pygame.display.update()和FPSCLOCK.tick(),直到checkForKeyPress()返回一个非None的值。当用户按下键时,这种情况就会发生。
defcheckForQuit():foreventinpygame.event.get(QUIT):#getalltheQUITeventsterminate()#terminateifanyQUITeventsarepresentforeventinpygame.event.get(KEYUP):#getalltheKEYUPeventsifevent.key==K_ESCAPE:terminate()#terminateiftheKEYUPeventwasfortheEsckeypygame.event.post(event)#puttheotherKEYUPeventobjectsbackcheckForQuit()函数可用于处理任何导致程序终止的事件。如果事件队列中有任何QUIT事件(由第348和349行处理),或者按下Esc键的KEYUP事件,则会发生这种情况。玩家应该能够随时按下Esc键退出程序。
因为第350行的pygame.event.get()调用会提取所有的KEYUP事件(包括不是Esc键的键的事件),如果事件不是针对Esc键的,我们希望通过调用pygame.event.post()函数将其放回事件队列中。
defcalculateLevelAndFallFreq(score):#Basedonthescore,returntheleveltheplayerisonand#howmanysecondspassuntilafallingpiecefallsonespace.level=int(score/10)+1fallFreq=0.27-(level*0.02)returnlevel,fallFreq每当玩家完成一行时,他们的分数将增加一分。每增加十分,游戏就会升一级,方块下落速度也会加快。游戏的级别和下落频率都可以根据传递给此函数的分数进行计算。
计算级别时,我们使用int()函数将分数除以10后向下取整。因此,如果分数在0和9之间,int()调用将将其舍入为0。代码中的+1部分是因为我们希望第一个级别是级别1,而不是级别0。当分数达到10时,int(10/10)将计算为1,+1将使级别为2。下面是一个图表,显示了分数为1到34时的级别值:
我们还可以制作一个图表,显示游戏每个级别下方块下落的速度:
如果您希望方块以较慢的速度开始(如果您明白我的意思)更快地下落,您可以更改calculateLevelAndFallFreq()使用的方程。例如,假设第360行是这样的:
fallFreq=0.27-(level*0.01)在上述情况下,方块每个级别下落的速度只会比原来快0.01秒,而不是0.02秒。图表会是这样的(原始线条也在图表中以浅灰色显示):
如您所见,使用这个新方程,第14级的难度只会和原始的第7级一样难。您可以通过更改calculateLevelAndFallFreq()中的方程来使游戏变得难或易。
defgetNewPiece():#returnarandomnewpieceinarandomrotationandcolorshape=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,#startitabovetheboard(i.e.lessthan0)'color':random.randint(0,len(COLORS)-1)}returnnewPiecegetNewPiece()函数生成一个位于板顶部的随机方块。首先,为了随机选择方块的形状,我们通过在第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。
defaddToBoard(board,piece):#fillintheboardbasedonpiece'slocation,shape,androtationforxinrange(TEMPLATEWIDTH):foryinrange(TEMPLATEHEIGHT):ifSHAPES[piece['shape']][piece['rotation']][y][x]!=BLANK:board[x+piece['x']][y+piece['y']]=piece['color']板数据结构是一个矩形空间的数据表示,用于跟踪先前着陆的方块。当前下落的方块不会在板数据结构上标记。addToBoard()函数的作用是获取一个方块数据结构,并将其盒子添加到板数据结构中。这是在方块着陆后发生的。
在第376和377行的嵌套for循环遍历方块数据结构中的每个空间,如果在空间中找到一个盒子(第378行),则将其添加到板上(第379行)。
defgetBlankBoard():#createandreturnanewblankboarddatastructureboard=[]foriinrange(BOARDWIDTH):board.append([BLANK]*BOARDHEIGHT)returnboard用于板的数据结构相当简单:它是一个值的列表的列表。如果值与BLANK中的值相同,那么它就是一个空格。如果值是整数,那么它表示的是颜色,该整数在COLORS常量列表中索引。也就是说,0是蓝色,1是绿色,2是红色,3是黄色。
为了创建一个空白板,使用列表复制来创建BLANK值的列表,这代表一列。这是在第386行完成的。为板中的每一列创建一个这样的列表(这是第385行上的for循环所做的)。
defisOnBoard(x,y):returnx>=0andx defisValidPosition(board,piece,adjX=0,adjY=0):#ReturnTrueifthepieceiswithintheboardandnotcollidingforxinrange(TEMPLATEWIDTH):foryinrange(TEMPLATEHEIGHT):isAboveBoard=y+piece['y']+adjY<0ifisAboveBoardorSHAPES[piece['shape']][piece['rotation']][y][x]==BLANK:continueisValidPosition()函数接收一个板数据结构和一个方块数据结构,并在方块的所有盒子都在板上且不重叠时返回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数据结构中的坐标是颠倒的。请参阅前一节“设置方块模板”)。 ifnotisOnBoard(x+piece['x']+adjX,y+piece['y']+adjY):returnFalseifboard[x+piece['x']+adjX][y+piece['y']+adjY]!=BLANK:returnFalsereturnTrue401行上的if语句检查方块是否位于板上。403行上的if语句检查方块所在的板空间是否为空白。如果这些条件中的任何一个为True,则isValidPosition()函数将返回False。请注意,这些if语句还会调整传递给函数的adjX和adjY参数的坐标。 如果代码通过嵌套的for循环并且没有找到返回False的原因,那么方块的位置必须是有效的,因此函数在405行返回True。 defisCompleteLine(board,y):#ReturnTrueifthelinefilledwithboxeswithnogaps.forxinrange(BOARDWIDTH):ifboard[x][y]==BLANK:returnFalsereturnTrueisCompleteLine在由y参数指定的行上进行了简单的检查。当板上的一行被认为是“完整”的时候,每个空间都被盒子填满。409行上的for循环遍历了行中的每个空间。如果空间为空白(这是由它具有与BLANK常量相同的值引起的),则函数返回False。 defremoveCompleteLines(board):#Removeanycompletedlinesontheboard,moveeverythingabovethemdown,andreturnthenumberofcompletelines.numLinesRemoved=0y=BOARDHEIGHT-1#startyatthebottomoftheboardwhiley>=0:removeCompleteLines()函数将在传递的板数据结构中查找任何完整的行,删除这些行,然后将板上的所有盒子向下移动一行。该函数将返回已删除的行数(由numLinesRemoved变量跟踪),以便将其添加到得分中。 这个函数的工作方式是通过在循环中运行,从第419行开始,y变量从最低行(即BOARDHEIGHT-1)开始。每当由y指定的行不完整时,y将递减到下一个更高的行。循环最终在y达到-1时停止。 ifisCompleteLine(board,y):#Removethelineandpullboxesdownbyoneline.forpullDownYinrange(y,0,-1):forxinrange(BOARDWIDTH):board[x][pullDownY]=board[x][pullDownY-1]#Setverytoplinetoblank.forxinrange(BOARDWIDTH):board[x][0]=BLANKnumLinesRemoved+=1#Noteonthenextiterationoftheloop,yisthesame.#Thisissothatifthelinethatwaspulleddownisalso#complete,itwillberemoved.else:y-=1#moveontochecknextrowupreturnnumLinesRemovedisCompleteLine()函数将返回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循环。 defconvertToPixelCoords(boxx,boxy):#Convertthegivenxycoordinatesoftheboardtoxy#coordinatesofthelocationonthescreen.return(XMARGIN+(boxx*BOXSIZE)),(TOPMARGIN+(boxy*BOXSIZE))这个辅助函数将棋盘的方框坐标转换为像素坐标。这个函数与前面游戏程序中使用的其他“转换坐标”函数的工作方式相同。 defdrawBox(boxx,boxy,color,pixelx=None,pixely=None):#drawasinglebox(eachtetrominopiecehasfourboxes)#atxycoordinatesontheboard.Or,ifpixelx&pixely#arespecified,drawtothepixelcoordinatesstoredin#pixelx&pixely(thisisusedforthe"Next"piece).ifcolor==BLANK:returnifpixelx==Noneandpixely==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行在较暗的方块上方绘制一个稍小的方块。 defdrawBoard(board):#drawtheborderaroundtheboardpygame.draw.rect(DISPLAYSURF,BORDERCOLOR,(XMARGIN-3,TOPMARGIN-7,(BOARDWIDTH*BOXSIZE)+8,(BOARDHEIGHT*BOXSIZE)+8),5)#fillthebackgroundoftheboardpygame.draw.rect(DISPLAYSURF,BGCOLOR,(XMARGIN,TOPMARGIN,BOXSIZE*BOARDWIDTH,BOXSIZE*BOARDHEIGHT))#drawtheindividualboxesontheboardforxinrange(BOARDWIDTH):foryinrange(BOARDHEIGHT):drawBox(x,y,board[x][y])drawBoard()函数负责调用棋盘边框和棋盘上所有方块的绘制函数。首先在DISPLAYSURF上绘制棋盘的边框,然后绘制棋盘的背景颜色。然后对棋盘上的每个空间调用drawBox()。drawBox()函数足够智能,如果board[x][y]设置为BLANK,它会略过这个方块。 defdrawStatus(score,level):#drawthescoretextscoreSurf=BASICFONT.render('Score:%s'%score,True,TEXTCOLOR)scoreRect=scoreSurf.get_rect()scoreRect.topleft=(WINDOWWIDTH-150,20)DISPLAYSURF.blit(scoreSurf,scoreRect)#drawtheleveltextlevelSurf=BASICFONT.render('Level:%s'%level,True,TEXTCOLOR)levelRect=levelSurf.get_rect()levelRect.topleft=(WINDOWWIDTH-150,50)DISPLAYSURF.blit(levelSurf,levelRect)drawStatus()函数负责在屏幕右上角渲染“得分:”和“等级:”信息的文本。 defdrawPiece(piece,pixelx=None,pixely=None):shapeToDraw=SHAPES[piece['shape']][piece['rotation']]ifpixelx==Noneandpixely==None:#ifpixelx&pixelyhasn'tbeenspecified,usethelocationstoredinthepiecedatastructurepixelx,pixely=convertToPixelCoords(piece['x'],piece['y'])#draweachoftheblocksthatmakeupthepieceforxinrange(TEMPLATEWIDTH):foryinrange(TEMPLATEHEIGHT):ifshapeToDraw[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()。 defdrawNextPiece(piece):#drawthe"next"textnextSurf=BASICFONT.render('Next:',True,TEXTCOLOR)nextRect=nextSurf.get_rect()nextRect.topleft=(WINDOWWIDTH-120,80)DISPLAYSURF.blit(nextSurf,nextRect)#drawthe"next"piecedrawPiece(piece,pixelx=WINDOWWIDTH-120,pixely=100)if__name__=='__main__':main()drawNextPiece()在屏幕右上角绘制“Next”方块。它通过调用drawPiece()函数并传入drawPiece()的pixelx和pixely参数来实现这一点。 这是最后一个函数。在所有函数定义执行完毕后,将运行第505和506行,然后调用main()函数开始程序的主要部分。 俄罗斯方块游戏(这是更受欢迎的“俄罗斯方块”的克隆)用英语向别人解释起来相当容易:“方块从棋盘顶部掉落,玩家移动和旋转它们,使它们形成完整的线。完整的线会消失(给玩家得分),上面的线会下移。游戏会一直进行,直到方块填满整个棋盘,玩家输掉游戏。” 用简单的英语解释是一回事,但当我们必须准确告诉计算机要做什么时,就有许多细节需要填写。最初的俄罗斯方块游戏是在1984年由苏联的一名人,亚历克斯·帕吉特诺夫设计和编程的。这个游戏简单、有趣、令人上瘾。它是有史以来最受欢迎的视频游戏之一,已经销售了1亿份,许多人都创造了自己的克隆和变种。 所有这些都是由一个懂得如何编程的人创造的。 有了正确的想法和一些编程知识,你可以创造出非常有趣的游戏。通过一些练习,你将能够将你的游戏想法变成真正的程序,可能会像俄罗斯方块一样受欢迎! 书籍网站上也有俄罗斯方块游戏的变体。“Pentomino”是由五个方块组成的版本。还有“TetrominoforIdiots”,其中所有的方块都只由一个小方块组成。 这些变体可以从以下网址下载: 松鼠吃松鼠loosley基于游戏“塊鼠大冒險”。玩家控制一个小松鼠,在屏幕上跳来跳去,吃掉比它小的松鼠,避开比它大的松鼠。每当玩家的松鼠吃掉比它小的松鼠时,它就会变得更大。如果玩家的松鼠被比它大的松鼠撞到,它就会失去一个生命点。当松鼠变成一个名为Omega松鼠的巨大松鼠时,玩家获胜。如果玩家的松鼠被撞三次,玩家就输了。 我真的不确定我是从哪里得到的一个松鼠互相吃掉的视频游戏的想法。有时候我有点奇怪。 这个游戏中有三种数据结构,它们被表示为字典值。这些类型分别是玩家松鼠、敌对松鼠和草对象。游戏中一次只有一个玩家松鼠对象。 注意:在面向对象编程中,“对象”在技术上有特定的含义。Python确实具有面向对象编程的特性,但本书中没有涉及。从技术上讲,Pygame对象,如“Rect对象”或“Surface对象”都是对象。但在本书中,我将使用术语“对象”来指代“游戏世界中存在的东西”。但实际上,玩家松鼠、敌对松鼠和草“对象”只是字典值。 所有对象的字典值中都有以下键:'x'、'y'和'rect'。'x'和'y'键的值给出了对象在游戏世界坐标中左上角的坐标。这些与像素坐标不同(这是'rect'键的值跟踪的内容)。游戏世界坐标和像素坐标之间的差异将在您学习摄像机概念时进行解释。 此外,玩家松鼠、敌对松鼠和草对象还有其他键,这些键在源代码的开头有一个大的注释进行了解释。 您还需要下载以下图像文件: CAMERASLACK=90#howfarfromthecenterthesquirrelmovesbeforemovingthecamera“相机松弛”稍后会进行解释。基本上,这意味着当玩家松鼠离窗口中心90像素时,相机将开始跟随玩家松鼠移动。 MOVERATE=9#howfasttheplayermovesBOUNCERATE=6#howfasttheplayerbounces(largeisslower)BOUNCEHEIGHT=30#howhightheplayerbouncesSTARTSIZE=25#howbigtheplayerstartsoffWINSIZE=300#howbigtheplayerneedstobetowinINVULNTIME=2#howlongtheplayerisinvulnerableafterbeinghitinsecondsGAMEOVERTIME=4#howlongthe"gameover"textstaysonthescreeninsecondsMAXHEALTH=3#howmuchhealththeplayerstartswithNUMGRASS=80#numberofgrassobjectsintheactiveareaNUMSQUIRRELS=30#numberofsquirrelsintheactiveareaSQUIRRELMINSPEED=3#slowestsquirrelspeedSQUIRRELMAXSPEED=7#fastestsquirrelspeedDIRCHANGEFREQ=2#%chanceofdirectionchangeperframeLEFT='left'RIGHT='right'这些常量旁边的注释解释了常量变量的用途。 defmain():globalFPSCLOCK,DISPLAYSURF,BASICFONT,L_SQUIR_IMG,R_SQUIR_IMG,GRASSIMAGESpygame.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('SquirrelEatSquirrel')BASICFONT=pygame.font.Font('freesansbold.ttf',32)main()函数的前几行是我们以前游戏程序中看到的相同的设置代码。pygame.display.set_icon()是一个Pygame函数,用于设置窗口标题栏中的图标(就像pygame.display.set_caption()设置标题栏中的标题文本一样)。pygame.display.set_icon()的单个参数是一个小图像的Surface对象。理想的图像尺寸是32x32像素,尽管您可以使用其他尺寸的图像。图像将被压缩成较小的尺寸,以用作窗口的图标。 #loadtheimagefilesL_SQUIR_IMG=pygame.image.load('squirrel.png')R_SQUIR_IMG=pygame.transform.flip(L_SQUIR_IMG,True,False)GRASSIMAGES=[]foriinrange(1,5):GRASSIMAGES.append(pygame.image.load('grass%s.png'%i))玩家和敌对松鼠的图像是从第74行的squirrel.png中加载的。确保这个PNG文件与squirrel.py在同一个文件夹中,否则你会得到错误pygame.error:Couldn'topensquirrel.png。 以下是图像水平和垂直翻转的示例: whileTrue:runGame()在main()中的设置完成后,游戏开始调用runGame()。 defrunGame():#setupvariablesforthestartofanewgameinvulnerableMode=False#iftheplayerisinvulnerableinvulnerableStartTime=0#timetheplayerbecameinvulnerablegameOverMode=False#iftheplayerhaslostgameOverStartTime=0#timetheplayerlostwinMode=False#iftheplayerhaswon松鼠吃松鼠游戏有很多跟踪游戏状态的变量。这些变量将在稍后在代码中使用时进行更详细的解释。 #createthesurfacestoholdgametextgameOverSurf=BASICFONT.render('GameOver',True,WHITE)gameOverRect=gameOverSurf.get_rect()gameOverRect.center=(HALF_WINWIDTH,HALF_WINHEIGHT)winSurf=BASICFONT.render('YouhaveachievedOMEGASQUIRREL!',True,WHITE)winRect=winSurf.get_rect()winRect.center=(HALF_WINWIDTH,HALF_WINHEIGHT)winSurf2=BASICFONT.render('(Press"r"torestart.)',True,WHITE)winRect2=winSurf2.get_rect()winRect2.center=(HALF_WINWIDTH,HALF_WINHEIGHT+30)这些变量包含屏幕上游戏结束后出现的“游戏结束”,“你已经获得OMEGA松鼠!”和“(按r重新开始)”文本的Surface对象。 #cameraxandcamerayarewherethemiddleofthecameraviewiscamerax=0cameray=0camerax和cameray变量跟踪“摄像机”的游戏坐标。想象游戏世界是一个无限的二维空间。当然,这永远无法适应任何屏幕。我们只能在屏幕上绘制无限2D空间的一部分。我们称这一部分的区域为摄像机,因为就像我们的屏幕只是摄像机所看到的游戏世界的区域。这是游戏世界(一个无限的绿色领域)和摄像机可以查看的区域的图片: 正如你所看到的,游戏世界的XY坐标将永远变大和变小。游戏世界的原点是游戏世界坐标为(0,0)的地方。你可以看到三只松鼠的位置(在游戏世界坐标中)分别为(-384,-84),(384,306)和(585,-234)。 但是我们只能在屏幕上显示640x480像素的区域(尽管如果我们向pygame.display.set_mode()函数传递不同的数字,这可能会改变),所以我们需要跟踪摄像机原点在游戏世界坐标中的位置。在上面的图片中,摄像机在游戏世界坐标中的位置是(-486,-330)。 下面的图片显示了相同的领域和松鼠,只是一切都是以摄像机坐标给出的: 相机可以看到的区域(称为相机视野)的中心(即其原点)位于游戏世界坐标(-486,-330)。由于相机看到的内容显示在玩家的屏幕上,因此“相机”坐标与“像素”坐标相同。要找出松鼠的像素坐标(即它们在屏幕上出现的位置),需要用松鼠的游戏坐标减去相机原点的游戏坐标。 左边的松鼠在游戏世界坐标为(-384,-84),但在屏幕上的像素坐标为(102,246)。(对于X坐标,-384--486=102,对于Y坐标,-84--330=246。) 当我们对其他两只松鼠进行相同的计算以找到它们的像素坐标时,我们发现它们存在于屏幕范围之外。这就是为什么它们不会出现在相机的视野中。 “活动区域”只是我想出来描述游戏世界的区域的一个名字,相机视野加上相机区域大小的周围区域: 计算某物是否在活动区域内的方法在本章后面的isOutsideActiveArea()函数的解释中有说明。当我们创建新的敌对松鼠或草对象时,我们不希望它们被创建在相机的视野内,因为这样看起来它们就像从无处冒出来一样。 但我们也不希望将它们创建得离相机太远,因为那样它们可能永远不会漫游到相机的视野中。在活动区域内但在相机之外是松鼠和草对象可以安全创建的地方。 此外,当松鼠和草对象超出活动区域的边界时,它们距离足够远,可以删除,以便它们不再占用内存。那么远的对象不再需要,因为它们很少可能再次出现在相机的视野中。 grassObjs=[]#storesallthegrassobjectsinthegamesquirrelObjs=[]#storesallthenon-playersquirrelobjects#storestheplayerobject: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=FalsemoveRight=FalsemoveUp=FalsemoveDown=FalsegrassObjs变量保存了游戏中所有草对象的列表。随着新的草对象的创建,它们被添加到这个列表中。当草对象被删除时,它们将从此列表中移除。squirrelObjs变量和敌对松鼠对象也是如此。 playerObj变量不是一个列表,而只是字典值本身。 第120至123行的移动变量跟踪着哪个箭头键(或WASD键)被按下,就像在之前的一些游戏程序中一样。 #startoffwithsomerandomgrassimagesonthescreenforiinrange(10):grassObjs.append(makeNewGrass(camerax,cameray))grassObjs[i]['x']=random.randint(0,WINWIDTH)grassObjs[i]['y']=random.randint(0,WINHEIGHT)活动区域应该从屏幕上可见的一些草对象开始。makeNewGrass()函数将创建并返回一个草对象,该对象随机位于活动区域但在相机视野之外的某个地方。这是我们调用makeNewGrass()时通常想要的,但由于我们希望确保前几个草对象在屏幕上,X和Y坐标被覆盖。 whileTrue:#maingameloop游戏循环,就像以前的游戏程序中的游戏循环一样,将处理事件,更新游戏状态,并将所有内容绘制到屏幕上。 #moveallthesquirrelsforsObjinsquirrelObjs:#movethesquirrel,andadjustfortheirbouncesObj['x']+=sObj['movex']sObj['y']+=sObj['movey']敌方松鼠都根据它们的'movex'和'movey'键中的值移动。如果这些值是正数,松鼠向右或向下移动。如果这些值是负数,它们向左或向上移动。值越大,它们在游戏循环中的每次迭代中移动得越远(这意味着它们移动得更快)。 第137行的for循环将应用此移动代码到squirrelObjs列表中的每个敌方松鼠对象。首先,第139和140行将调整它们的'x'和'y'键的值。 sObj['bounce']+=1ifsObj['bounce']>sObj['bouncerate']:sObj['bounce']=0#resetbounceamountsObj['bounce']中的值在每次松鼠的游戏循环迭代中递增。当这个值为0时,松鼠在其弹跳的最开始。当这个值等于sObj['bouncerate']中的值时,该值就结束了。(这就是为什么较小的sObj['bouncerate']值会导致更快的弹跳。如果sObj['bouncerate']是3,那么松鼠只需要三次游戏循环迭代就能完成一次完整的弹跳。如果sObj['bouncerate']是10,那么就需要十次迭代。) 当sObj['bounce']大于sObj['bouncerate']时,它需要被重置为0。这就是第142和143行的作用。 #randomchancetheychangedirectionifrandom.randint(0,99) 因为这意味着松鼠可能已经改变了方向,所以sObj['surface']中的Surface对象应该被一个新的替换,它应该正确地面向左或右,并且按照松鼠的大小进行缩放。这就是第149到152行的作用。请注意,第150行获取了一个从R_SQUIR_IMG缩放的Surface对象,第152行获取了一个从L_SQUIR_IMG缩放的Surface对象。 #gothroughalltheobjectsandseeifanyneedtobedeleted.foriinrange(len(grassObjs)-1,-1,-1):ifisOutsideActiveArea(camerax,cameray,grassObjs[i]):delgrassObjs[i]foriinrange(len(squirrelObjs)-1,-1,-1):ifisOutsideActiveArea(camerax,cameray,squirrelObjs[i]):delsquirrelObjs[i]在游戏循环的每次迭代中,代码将检查所有草和敌方松鼠对象,看它们是否在“活动区域”之外。isOutsideActiveArea()函数接受摄像机的当前坐标(存储在camerax和cameray中)和草/敌方松鼠对象,并在对象不在活动区域时返回True。 如果是这种情况,这个对象将在第158行(对于草对象)或第161行(对于松鼠对象)被删除。这就是当玩家离它们足够远时(或者当敌方松鼠离玩家足够远时),松鼠和草对象被删除的方式。这确保了玩家附近始终有一定数量的松鼠和草对象。 删除松鼠和草对象是使用del运算符完成的。但是,请注意,第156行和159行的for循环向range()函数传递参数,以便编号从最后一项的索引开始,然后递减-1(与通常的递增1相反),直到达到数字-1。我们是按照与通常情况下相反的方式迭代列表的索引。这是因为我们正在迭代我们也正在删除项目的列表。 要看为什么需要这种反向顺序,假设我们有以下列表值: animals=['cat','mouse','dog','horse']所以我们想要编写代码来从列表中删除字符串'dog'的任何实例。我们可能会想要编写如下代码: foriinrange(len(animals)):ifanimals[i]=='dog':delanimals[i]但是如果我们运行这段代码,我们将得到一个IndexError错误,看起来像这样: Traceback(mostrecentcalllast):File" 当i设置为2时,for循环迭代,if语句的条件将为True,delanimals[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']foriinrange(len(animals)-1,-1,-1):ifanimals[i]=='dog':delanimals[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循环中以相反的顺序迭代。 #addmoregrass&squirrelsifwedon'thaveenough.whilelen(grassObjs) #adjustcameraxandcamerayifbeyondthe"cameraslack"playerCenterx=playerObj['x']+int(playerObj['size']/2)playerCentery=playerObj['y']+int(playerObj['size']/2)if(camerax+HALF_WINWIDTH)-playerCenterx>CAMERASLACK:camerax=playerCenterx+CAMERASLACK-HALF_WINWIDTHelifplayerCenterx–(camerax+HALF_WINWIDTH)>CAMERASLACK:camerax=playerCenterx–CAMERASLACK-HALF_WINWIDTHif(cameray+HALF_WINHEIGHT)-playerCentery>CAMERASLACK:cameray=playerCentery+CAMERASLACK-HALF_WINHEIGHTelifplayerCentery–(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语句对左、上和下侧采用类似的逻辑。 #drawthegreenbackgroundDISPLAYSURF.fill(GRASSCOLOR)第182行开始绘制显示Surface对象内容的代码。首先,第182行绘制背景的绿色。这将覆盖Surface的所有先前内容,以便我们可以从头开始绘制帧。 #drawallthegrassobjectsonthescreenforgObjingrassObjs: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中,并简单地在草地对象本身中存储整数,才有意义。 #drawtheothersquirrelsforsObjinsquirrelObjs: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对象。这是因为松鼠图像可以按比例缩放到不同的大小。 玩家松鼠将在游戏循环迭代中绘制十分之一秒,然后在游戏循环迭代中的十分之一秒内不绘制。只要玩家是无敌的(在代码中意味着invulnerableMode变量设置为True),我们的代码将使闪烁持续两秒,因为2存储在第25行的INVULNTIME常量变量中。 然后将这个值乘以10,变成13239268936。一旦我们将其作为整数,我们可以首先使用在“记忆拼图”章节中讨论的“模二”技巧来查看它是偶数还是奇数。13239268936%2计算结果为0,这意味着flashIsOn将被设置为False,因为0==1是False。 实际上,time.time()将继续返回值,最终将False放入flashIsOn,直到1323926893.700,即下一个十分之一秒。这就是为什么flashIsOn变量在十分之一秒内将不断为False,然后在下一个十分之一秒内为True(无论在那十分之一秒内发生多少次迭代)。 ifnotgameOverModeandnot(invulnerableModeandflashIsOn):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)。 绘制玩家松鼠的代码几乎与绘制敌对松鼠的代码相同。 #drawthehealthmeterdrawHealthMeter(playerObj['health'])drawHealthMeter()函数在屏幕左上角绘制指示器,告诉玩家玩家松鼠在死亡之前可以被击中多少次。这个函数将在本章后面解释。 foreventinpygame.event.get():#eventhandlingloopifevent.type==QUIT:terminate()事件处理循环中首先检查的是是否生成了QUIT事件。如果是,则应终止程序。 elifevent.type==KEYDOWN:ifevent.keyin(K_UP,K_w):moveDown=FalsemoveUp=Trueelifevent.keyin(K_DOWN,K_s):moveUp=FalsemoveDown=True如果按下了上下箭头键(或它们的WASD等效键),则该方向的移动变量(moveRight,moveDown等)应设置为True,相反方向的移动变量应设置为False。 elifevent.keyin(K_LEFT,K_a):moveRight=FalsemoveLeft=TrueifplayerObj['facing']==RIGHT:#changeplayerimageplayerObj['surface']=pygame.transform.scale(L_SQUIR_IMG,(playerObj['size'],playerObj['size']))playerObj['facing']=LEFTelifevent.keyin(K_RIGHT,K_d):moveLeft=FalsemoveRight=TrueifplayerObj['facing']==LEFT:#changeplayerimageplayerObj['surface']=pygame.transform.scale(R_SQUIR_IMG,(playerObj['size'],playerObj['size']))playerObj['facing']=RIGHTmoveLeft和moveRight变量在按下左箭头或右箭头键时也应该被设置。此外,playerObj['facing']中的值应该更新为LEFT或RIGHT。如果玩家松鼠现在面对着一个新的方向,playerObj['surface']的值应该被替换为正确缩放的松鼠面对新方向的图像。 如果按下左箭头键,则运行第229行,并检查玩家松鼠是否面向右侧。如果是这样,那么玩家松鼠图像的新缩放表面对象将存储在playerObj['surface']中。第232行的elif语句处理相反的情况。 elifwinModeandevent.key==K_r:return如果玩家通过变得足够大而赢得了游戏(在这种情况下,winMode将被设置为True),并且按下了R键,则runGame()应该返回。这将结束当前游戏,并且下一次调用runGame()时将开始新游戏。 elifevent.type==KEYUP:#stopmovingtheplayer'ssquirrelifevent.keyin(K_LEFT,K_a):moveLeft=Falseelifevent.keyin(K_RIGHT,K_d):moveRight=Falseelifevent.keyin(K_UP,K_w):moveUp=Falseelifevent.keyin(K_DOWN,K_s):moveDown=False如果玩家松开任何箭头键或WASD键,则代码应该将该方向的移动变量设置为False。这将阻止松鼠继续朝着那个方向移动。 elifevent.key==K_ESCAPE:terminate()如果按下的键是Esc键,则终止程序。 ifnotgameOverMode:#actuallymovetheplayerifmoveLeft:playerObj['x']-=MOVERATEifmoveRight:playerObj['x']+=MOVERATEifmoveUp:playerObj['y']-=MOVERATEifmoveDown:playerObj['y']+=MOVERATE在第255行的if语句中的代码只有在游戏没有结束时才会移动玩家的松鼠。(这就是为什么在玩家的松鼠死亡后按箭头键没有效果。)根据哪个移动变量设置为True,playerObj字典的playerObj['x']和playerObj['y']的值应该改变MOVERATE。(这就是为什么MOVERATE中的较大值会使松鼠移动得更快。) if(moveLeftormoveRightormoveUpormoveDown)orplayerObj['bounce']!=0:playerObj['bounce']+=1ifplayerObj['bounce']>BOUNCERATE:playerObj['bounce']=0#resetbounceamountplayerObj['bounce']中的值跟踪玩家在反弹中的位置。这个变量存储一个从0到BOUNCERATE的整数值。就像敌对松鼠的反弹值一样,playerObj['bounce']值为0意味着玩家松鼠在反弹开始时,值为BOUNCERATE意味着玩家松鼠在反弹结束时。 玩家松鼠在玩家移动时或者如果玩家停止移动但松鼠还没有完成当前的反弹时会反弹。这个条件在第266行的if语句中捕获。如果任何移动变量设置为True或当前的playerObj['bounce']不是0(这意味着玩家当前正在反弹),则应在第267行递增该变量。 因为playerObj['bounce']变量应该只在0到BOUNCERATE的范围内,如果递增它使其大于BOUNCERATE,则应将其重置为0。 #checkiftheplayerhascollidedwithanysquirrelsforiinrange(len(squirrelObjs)-1,-1,-1):sqObj=squirrelObjs[i]第273行的for循环将在squirrelObjs中的每个敌对松鼠游戏对象上运行代码。请注意,第273行中range()的参数从squirrelObjs的最后一个索引开始递减。这是因为此for循环中的代码可能会删除其中一些敌对松鼠游戏对象(如果玩家的松鼠最终吃掉它们),因此重要的是从末尾向前迭代。之前解释过的原因在“在删除列表中的项目时,以相反顺序迭代列表”部分中已经解释过。 if'rect'insqObjandplayerObj['rect'].colliderect(sqObj['rect']):#aplayer/squirrelcollisionhasoccurred277.ifsqObj['width']*sqObj['height']<=playerObj['size']**2:#playerislargerandeatsthesquirrelplayerObj['size']+=int((sqObj['width']*sqObj['height'])**0.2)+1delsquirrelObjs[i]如果玩家的松鼠与其碰撞的敌对松鼠的大小相等或更大,则玩家的松鼠将吃掉那只松鼠并变大。在玩家对象的'size'键中添加的数字(即增长)是根据第280行的敌对松鼠的大小计算的。下面是显示不同大小松鼠的增长的图表。请注意,更大的松鼠会导致更多的增长: 因此,根据图表,吃掉一个宽度和高度为45(即1600像素)的松鼠会使玩家变宽5像素,变高5像素。 第281行从squirrelObjs列表中删除了被吃掉的松鼠对象,这样它就不会再出现在屏幕上或更新其位置。 ifplayerObj['facing']==LEFT:playerObj['surface']=pygame.transform.scale(L_SQUIR_IMG,(playerObj['size'],playerObj['size']))ifplayerObj['facing']==RIGHT:playerObj['surface']=pygame.transform.scale(R_SQUIR_IMG,(playerObj['size'],playerObj['size']))玩家的松鼠形象现在需要更新,因为松鼠变大了。这可以通过将原始松鼠图像传递给pygame.transform.scale()函数来完成,该函数将返回图像的放大版本。根据playerObj['facing']是否等于LEFT或RIGHT来确定我们将哪个原始松鼠图像传递给函数。 ifplayerObj['size']>WINSIZE:winMode=True#turnon"winmode"玩家赢得游戏的方式是使松鼠的大小大于WINSIZE常量变量中存储的整数。如果是这样,winMode变量将设置为True。此函数的其他部分将处理显示祝贺文本并检查玩家是否按下R键重新开始游戏。 elifnotinvulnerableMode:#playerissmallerandtakesdamageinvulnerableMode=TrueinvulnerableStartTime=time.time()playerObj['health']-=1ifplayerObj['health']==0:gameOverMode=True#turnon"gameovermode"gameOverStartTime=time.time()如果玩家的面积不等于或大于敌对松鼠的面积,并且invulnerableMode没有设置为True,那么玩家将受到与这只更大松鼠碰撞的伤害。 这样一来,玩家死亡后,在下一局游戏开始之前,敌对松鼠可以继续动画并在屏幕上移动几秒钟。《松鼠吃松鼠》的“游戏结束画面”不会等到玩家按键再开始新游戏。 #checkiftheplayerhaswon.ifwinMode: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行完成。 defdrawHealthMeter(currentHealth):foriinrange(currentHealth):#drawredhealthbarspygame.draw.rect(DISPLAYSURF,RED,(15,5+(10*MAXHEALTH)-i*10,20,10))foriinrange(MAXHEALTH):#drawthewhiteoutlinespygame.draw.rect(DISPLAYSURF,WHITE,(15,5+(10*MAXHEALTH)-i*10,20,10),1)要绘制健康仪表,首先在第317行的for循环中绘制填充的红色矩形以表示玩家的健康量。然后在第319行的for循环中,为玩家可能拥有的所有健康量(存储在MAXHEALTH常量中的整数值)绘制一个未填充的白色矩形。请注意,pygame.display.update()函数在drawHealthMeter()中未被调用。 defterminate():pygame.quit()sys.exit()terminate()函数与以前的游戏程序中的相同。 defgetBounceAmount(currentBounce,bounceRate,bounceHeight):#Returnsthenumberofpixelstooffsetbasedonthebounce.#LargerbounceRatemeansaslowerbounce.#LargerbounceHeightmeansahigherbounce.#currentBouncewillalwaysbelessthanbounceRatereturnint(math.sin((math.pi/float(bounceRate))*currentBounce)*bounceHeight)有一个数学函数(类似于编程中的函数,因为它们都基于其参数“返回”或“评估”为一个数字)称为正弦(发音类似于“标志”,通常缩写为“sin”)。你可能在数学课上学过它,但如果你没有,这里将会解释。Python将这个数学函数作为math模块中的Python函数。你可以将int或float值传递给math.sin(),它将返回一个称为“正弦值”的浮点值。 在交互式shell中,让我们看看math.sin()对一些值返回什么: >>>importmath>>>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等),然后用线连接这些点,你就可以更容易地看到这种波浪形的模式: 实际上,如果你不断添加更多的数据点到这个图表中,你会看到正弦波看起来像这样: 让我们看看getBounceAmount()的返回值,并确切地弄清楚它的作用。 returnint(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常量。 我们调用float()将bounceRate转换为浮点数的原因很简单,即使这样程序也可以在Python2版本中运行。在Python3版本中,即使操作数都是整数,除法运算符也将评估为浮点值,如下所示: >>>#Pythonversion3...>>>10/52.0>>>10/42.5>>>然而,在Python2版本中,如果操作数中有一个是整数,则/除法运算符只会评估为浮点值。如果两个操作数都是整数,则Python2的除法运算符将评估为整数值(如有需要四舍五入),如下所示: >>>#Pythonversion2...>>>10/52>>>10/42>>>10/4.02.5>>>10.0/42.5>>>10.0/4.02.5但是,如果我们总是使用float()函数将其中一个值转换为浮点值,那么无论哪个版本的Python运行此源代码,除法运算符都将评估为浮点值。进行这些更改以使我们的代码与旧版本的软件兼容称为向后兼容性。保持向后兼容性很重要,因为并非每个人都会始终运行最新版本的软件,您希望确保您编写的代码与尽可能多的计算机兼容。 您并非总是可以使您的Python3代码向后兼容Python2,但如果可能的话,您应该这样做。否则,当使用Python2的人尝试运行您的游戏时,将会收到错误消息,并认为您的程序有错误。 defgetRandomVelocity():speed=random.randint(SQUIRRELMINSPEED,SQUIRRELMAXSPEED)ifrandom.randint(0,1)==0:returnspeedelse:return-speedgetRandomVelocity()函数用于随机确定敌对松鼠的移动速度。此速度的范围设置在SQUIRRELMINSPEED和SQUIRRELMAXSPEED常量中,但除此之外,速度要么为负(表示松鼠向左或向上移动),要么为正(表示松鼠向右或向下移动)。随机速度为正或负的机会是五五开。 defgetRandomOffCameraPos(camerax,cameray,objWidth,objHeight):#createaRectofthecameraviewcameraRect=pygame.Rect(camerax,cameray,WINWIDTH,WINHEIGHT)whileTrue:x=random.randint(camerax-WINWIDTH,camerax+(2*WINWIDTH))y=random.randint(cameray-WINHEIGHT,cameray+(2*WINHEIGHT))349.#createaRectobjectwiththerandomcoordinatesandusecolliderect()#tomakesuretherightedgeisn'tinthecameraview.objRect=pygame.Rect(x,y,objWidth,objHeight)ifnotobjRect.colliderect(cameraRect):returnx,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循环将继续生成新的坐标,直到找到可接受的坐标为止。 defmakeNewSquirrel(camerax,cameray):sq={}generalSize=random.randint(5,25)multiplier=random.randint(1,3)sq['width']=(generalSize+random.randint(0,10))*multipliersq['height']=(generalSize+random.randint(0,10))*multipliersq['x'],sq['y']=getRandomOffCameraPos(camerax,cameray,sq['width'],sq['height'])sq['movex']=getRandomVelocity()sq['movey']=getRandomVelocity()创建敌对松鼠游戏对象类似于制作草地游戏对象。每个敌对松鼠的数据也存储在字典中。第360行和361行将宽度和高度设置为随机大小。使用generalSize变量是为了确保每只松鼠的宽度和高度不会相差太大。否则,对于宽度和高度完全随机的数字可能会给我们非常高而瘦的松鼠或非常短而宽的松鼠。松鼠的宽度和高度是这个一般大小,加上从0到10的随机数(用于轻微变化),然后乘以multiplier变量。 松鼠的原始XY坐标位置将是相机无法看到的随机位置,以防止松鼠只是在屏幕上“突然出现”。 速度和方向也是由getRandomVelocity()函数随机选择的。 ifsq['movex']<0:#squirrelisfacingleftsq['surface']=pygame.transform.scale(L_SQUIR_IMG,(sq['width'],sq['height']))else:#squirrelisfacingrightsq['surface']=pygame.transform.scale(R_SQUIR_IMG,(sq['width'],sq['height']))sq['bounce']=0sq['bouncerate']=random.randint(10,18)sq['bounceheight']=random.randint(10,50)returnsqL_SQUIR_IMG和R_SQUIR_IMG常量包含左侧和右侧松鼠图像的Surface对象。将使用pygame.transform.scale()函数创建新的Surface对象,以匹配松鼠的宽度和高度(分别存储在sq['width']和sq['height']中)。 defmakeNewGrass(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']))returngr草地游戏对象是带有通常的'x'、'y'、'width'、'height'和'rect'键的字典,但也有一个'grassImage'键,它是0到GRASSIMAGES列表长度减一的数字。这个数字将决定草地游戏对象使用什么图像。例如,如果草地对象的'grassImage'键的值为3,那么它将使用存储在GRASSIMAGES[3]的Surface对象作为其图像。 defisOutsideActiveArea(camerax,cameray,obj):#ReturnFalseifcameraxandcamerayaremorethan#ahalf-windowlengthbeyondtheedgeofthewindow.boundsLeftEdge=camerax-WINWIDTHboundsTopEdge=cameray-WINHEIGHTboundsRect=pygame.Rect(boundsLeftEdge,boundsTopEdge,WINWIDTH*3,WINHEIGHT*3)objRect=pygame.Rect(obj['x'],obj['y'],obj['width'],obj['height'])returnnotboundsRect.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()函数并启动游戏。 《松鼠吃松鼠》是我们的第一个游戏,其中有多个敌人同时在棋盘上移动。拥有多个敌人的关键是使用具有相同键的字典值,以便在游戏循环的迭代中对它们中的每一个运行相同的代码。 相机的概念也被引入了。在我们之前的游戏中并不需要相机,因为整个游戏世界都可以放在一个屏幕上。然而,当你制作自己的游戏涉及到玩家在一个大型游戏世界中移动时,你将需要编写代码来处理游戏世界坐标系和屏幕像素坐标系之间的转换。 最后,数学正弦函数被引入,以实现真实的松鼠跳跃(无论每次跳跃有多高或多远)。你并不需要了解很多数学知识来进行编程。在大多数情况下,只需要了解加法、乘法和负数就可以了。然而,如果你学习数学,你会经常发现数学有很多用途,可以让你的游戏更酷。