【文章基于《Python编程-从入门到实践》】
【项目规划】
“开发大型项目时先做好规划再动手编写项目很重要”
下面是对《外星人入侵》的规划:
①玩家控制一艘在屏幕底部中央的飞船,可通过箭头键左右移动飞船,还可以使用空格键射击
②一群外星人出现在天空中,他们不断向下移动
③待玩家将所有外星人消灭后,会出现一群移动速度更快地新外星人
④当有外星人撞到玩家的飞船或到达屏幕底部,玩家就损失一艘飞船
⑤玩家损失三艘飞船后,游戏结束
【安装Pygame】
pip是一个负责下载并安装Python包的程序(在Python 3中pip有时也被称为pip3),可以先查看系统是否已经安装了pip:
> pip --version# 在Linux和OS X系统中检查
> python -m pip --version# 在Windows系统中检查【为何我两个都行??】
Windows系统的用户通过http://www.lfd.uci.edu/~gohlke/pythonlibs/#pygame,查找到对应Python版本、电脑操作系统类型、电脑位数的文件进行下载
【开始游戏项目】
【创建游戏窗口】
import sys import pygame def run_game(): # 初始化游戏并创建一个屏幕对象 pygame.init() screen = pygame.display.set_mode((1200, 600)) pygame.display.set_caption('Alien Invasion') # 开始游戏主循环 while True: # 监视屏幕和鼠标事件 for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() # 让最近绘制的屏幕可见 pygame.display.flip() run_game()
以上为创建一个新的空游戏窗口的代码,我们来逐个分析:
① pygame.init( )
② screen = pygame.display.set_mode((1200 , 600))
③ pygame.display.set_caption('Alien Invasion')
要创建一个游戏窗口,首先要考虑窗口的大小以及窗口的名称(即游戏名称)
①处的pygame.init( )初始化背景设置,让Pygame能够正常运行;
②处中,display为pygame的控制窗口和屏幕显示的模块,执行display模块的set_mode方法:pygame.display.set_code( )。向该方法传入一个tuple(1200 , 600),表示创建一个宽1200像素、高600像素的游戏窗口,并指向screen这一变量
除了tuple外,还可以向set_mode( )传递一个list等
③处同样调用pygame的display模块,而set_caption( )方法是设置窗口名称
while True:
游戏需要实现“重新游戏”的功能,无论是通关还是失败,因此在Pygame中创建好一个游戏窗口后,直接进入游戏的主循环;在主循环内部通过其他方法退出循环
for event in pygame.event.get( ):
if event.type == pygame.QUIT:
sys.exit( )
[事件循环]:事件是用户玩游戏时执行的操作,如按键和鼠标移动。由于事件随时可能产生,而且量也会很大,Pygame通过pygame.event.get( )将一系列事件放在一个队列里,逐个处理
上述代码首先遍历pygame.event整个队列,并用if语句来检查,如果其中有一个事件的类型为pygame.QUIT,就调用sys模块中的exit( )方法,退出窗口,实现“关闭”游戏的效果
一般情况下,类型为pygame.QUIT的事件都是用户鼠标点击“退出游戏”按钮
pygame.display.flip( )
[管理屏幕更新]:上述代码命令Pygame让最近绘制的屏幕可见。
在这里,每次执行while循环时都会绘制一个空屏幕,并擦去旧屏幕使得只有新屏幕可见。当我们移动游戏元素时,pygame.display.flip( )将不断更新屏幕以显示元素的位置,从而营造平滑移动的效果。因此要修改当前屏幕,先得完成所有的修改,再通过flip( )显示更新
综上,创建窗口有以下步骤:【窗口设置】→【主循环】→【检测事件】→【更新屏幕】
【RGB】
RGB色彩模式是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。
通过形如(153,153,255)的形式确定红(R)、绿(G)、蓝(B)分别的强度,可以混合出几乎所有颜色(256*256*256 = 16777216≈1600万种)
默认创建的游戏窗口都是黑色的,太乏味了,可以将其设置为另一种颜色:
bg_color = (153 , 153 , 255)
由于窗口颜色的设置为构建窗口的一部分,所以应该将上面的代码置于while主循环之前,就如同pygame.display.set_mode( )和pygame.display.set_caption( )一样
然而bg_color = (153 , 153 , 255)只是定义了一个名为bg_color的tuple,还未发挥作用。
我们之前提及过,动画的平滑移动效果是通过pygame.display.flip( )不断更新屏幕造就的,所以我们在主循环中添加代码:
screen.fill(bg_color)
screen指向创建出来的窗口,通过fill( )方法对整个屏幕进行颜色填充。这样,每次循环时都会重绘屏幕,达到更换屏幕颜色的效果。
【创建设置类】
当游戏项目增大时,要修改游戏的外观等设置,如果一一去查找分布在文件不同位置的设置,浪费时间;因此可以定义一个Settings类,里面储存了《外星人入侵》的所有设置:
class Settings(object): '''储存《外星人入侵》的所有设置''' def __init__(self): self.screen_width = 1200 self.screen_height = 600 self.bg_color = (153, 153, 255)
随后更改原游戏代码:
import sys import pygame from ... import Settings def run_game(): pygame.init() ai_settings = Settings() # 不要遗漏了创建实例,以及下面set_mode()接收一个tuple screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height)) pygame.display.set_caption('Alien Invasion') While True: for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() screen.fill(ai_settings.bg_color) pygame.display.filp()
当要修改设置时,只需直接修改Settings中的值即可
【补充】fill( )和set_mode( )一样,都是接受一个tuple
【添加飞船图像】
首先要选定飞船图像,选定时务必注意许可,http://pixabay.com/网站提供的图形都无需许可,大可放心使用并对其进行修改
在游戏中几乎可以使用任何类型的图像文件,但最好使用位图(.bmp),Python默认加载位图。“虽然可配置Python以使用其他文件类型,但有些文件类型要求在计算机上安装相应的图像库”可通过Photoshop、GIMP和Paint等工具将.jpg、.png或.gif格式的图像转换为位图
将图像ship.bmp添加到文件夹alien_invasion的子文件夹images中,加载图像后,可以使用pygame的blit( )方法绘制它
【创建Ship类】
对于pygame而言,将一张飞船图像加载到创建的屏幕中是十分简单的,难点在于如何确定飞船的位置。一般的.bmp图像没有什么位置之分,因此我们将图像矩形化,也就是让Pygame像处理矩形一样处理游戏元素。由于矩形为简单的几何形状,Pygame处理其是高效的。
在这个游戏中,每个游戏元素都是一个surface,可通过get_rect( )方法来获取对应图像的rect对象
import pygame class Ship(object): def __init__(self, screen): self.screen = screen '''加载图像并获取其外接矩形''' self.image = pygame.image.load('images/ship.bmp') self.rect = self.image.get_rect() self.screen_rect = self.screen.get_rect() '''将每艘新飞船放在屏幕底部中央''' self.rect.centerx = self.screen_rect.centerx self.rect.bottom = self.screen_rect.bottom def blitme(self): self.screen.blit(self.image, self.rect)
下面还是来逐个分析:
self.screen = screen
事实上,创建Ship实例时要传入的实参screen就是之前通过set_mode( )创建的屏幕窗口,这里只是将screen与实例绑定,待会有用
self.image = pygame.image.load('images/ship.bmp')
self.rect = self.image.get_rect( )
self.screen_rect = self.screen.get_rect( )
首先使用Pygame内置image模块的load( )方法,通过相对搜索路径,load( )返回一个表示飞船的surface,并将这个surface储存到self.image中,待使用
之前有提及,为了将飞船安放在屏幕底部中央,我们要获取飞船图像的外接矩形,一旦获取了飞船图像的外接矩形(即rect对象),我们就可以设置rect对象的横中心线(centerx)、底部(bottom)等属性了
而上述的后面两行代码分别获取飞船图像、屏幕的外接矩形(别忘了屏幕也是surface)
对于【rect对象】,这里拓展一下:获取某个图像的外接矩形(rect对象)后,可以
①查看rect对象的size、width、height等参数
②可以设置横中心线(centerx)、纵中心线(centery)使游戏元素居中;设置属性top、bottom、left、right使游戏元素与屏幕边缘对齐
③直接为x、y赋值以达到确定位置的目的(x、y表示rect对象的中心坐标)
顺便一提,在Pygame中,原点(0 , 0)位于屏幕左上角,x值为横坐标向右、y值为纵坐标向下
self.rect.centerx = self.screen_rect.centerx
self.rect.bottom = self.screen_rect.bottom
这两行代码就是真正地确定了飞船的初始位置:令飞船的rect对象的横中心线(centerx)与屏幕的rect对象的横中心线重合(或相等)、令飞船的rect对象的底部(bottom)与屏幕的rect对象的底部重合(或相等),要知道centerx、bottom等都是rect对象的属性
def blitme(self):
self.screen.blit(self.image , self.rect)
【blit( )】
百度翻译为“位块传输”,“将一个平面的一部分或全部图象整块从这个平面复制到另一个平面”
在Pygame中,方法blit( )由屏幕调用,接收两个参数:一个是图像,另一个是放置该图像的位置;之前的self.rect.centerx = ... 、self.rect.bottom = ...都是为了设置该位置
提前加载飞船图像、设置图像位置后,在Ship类外面调用blitme( )就会绘制在特定位置的飞船
【在屏幕上绘制】
创建Ship类后,我们需要将其应用到run_game( )函数中,注意以下两点:
①在主循环之前创建Ship的实例,以免每次循环时都创建一艘飞船:ship = Ship(screen)
②确保飞船图像出现在背景前面,代码行ship.blitme( )需出现在screen.fill(bg_color)之后
【重构:模块game_functions】
在大型项目中,往往需要在添加新代码前重构既有代码,旨在简化既有代码结构,使其更拥有扩展
在《外星人入侵》的游戏代码中,除却在运行游戏的函数run_game( )之外的Settings类、Ship( )类可以储存在其他模块(.py文件)中,需要时再导入外,我们还可以创建一个game_functions( )的新模块,用于储存大量让游戏《外星人入侵》运行的代码
import sys import pygame def check_events(): for event = pygame.event.get(): if event.type == pygame.QUIT: sys.exit() def upgrade_screen(ai_settings, screen, ship): screen.fill(ai_settings.bg_color) ship.blitme() pygame.display.flip()
将上述代码储存到game_functions.py文件中,在原alien_invasion.py文件中导入该模块:import game_functions as gf,在需要的地方之间调用函数:
while True: gf.check_events() gf.upgrade_screen(ai_settings, screen, ship)
你会发觉,sys模块只是用在检查事件中,当check_events( )函数在game_functions模块中提前导入了sys模块,那么,在alien_invasion模块中就无需再导入sys模块了!
这样,程序员在检查代码时就能够从更高级的层面去查看代码
【响应按键(单击)】
用户按下键盘时,Pygame会在属性event中检测到,因此按键属于检测事件,对于按键的相关代码理应放在check_events( )函数中:
def check_events(ship): for event in pygame.event.get(): --snip-- elif event.type == pygame.KEYDOWN: if event.key == pygame.K_RIGHT: ship.rect.centerx += 1
Pygame不断监视事件,当判断事件的类型为「键盘按键」(即type == pygame.KEYDOWN)时,若再判断出按下的是右箭头键,则增大飞船的rect.centerx值。每次按右箭头键一次,飞船向右移动1像素
此外,由于要控制飞船,需修改函数check_events( )的传入参数,添加ship;在run_game( )调用时也应当添加相应的实参
【响应按键(持续)】
单击键盘造成的移动可以被Pygame检查到并进行简单的图像位移,但当持续移动时,必定涉及到while循环;若在check_events( )中更改ship.rect.centerx值,由于涉及到循环,而Pygame的运行又是不断在执行主循环,因此当两个循环碰撞时,会引发异常。
要想避免出现两个循环,可以将操控持续移动的代码剥离出循环性质(比如更改while为if),再将这段代码的触发置于主循环中。这样,当主循环不断执行时,持续的位移也可以实现。
为了在主循环中操作持续移动,我们定义一个update( )函数,用于触发;再在check_events( )函数中利用moving_right之类的标志,响应键盘。
class Ship(object): def __init__(self, screen): --snip-- self.moving_right = False def update(self): if self.moving_right == True: self.rect.centerx += 1
同时修改check_events( )函数:
def check_events(ship): --snip-- elif event.type == pygame.KEYDOWN: if event.key == pygame.K_RIGHT: ship.moving_right = True elif event.type == pygame.KEYUP: if event.key == pygame.K_RIGHT: ship.moving_right = False
通过更改后的check_events( )函数检查键盘来更改moving_right属性的值,再经update( )方法执行;最后一步就是将update( )方法置于主循环中:
while True: gf.check_events(ship) ship.update() gf.update_screen(ai_settings, screen, ship)
同理可实现向左移动
事实上,也可尝试将ship.update( )整合进gf.update_screen( )中
【调整速度】
每次执行while循环时我们设定飞船移动1像素,但当要往后增大游戏挑战难度时,我们需要调高飞船速度。通过将飞船的速度设置为浮点数(如1.5),我们可以更细致地控制飞船。
但是rect对象的centerx等属性只能储存整数,若直接赋值浮点数,rect.centerx只会取整数部分,因此我们设定一个中间值center:
'''设置放入Settings类中''' self.ship_speed_factor = 1.5 '''更改Ship类''' def __init__(self, screen, ai_settings): --snip-- self.center = float(self.rect.centerx)# 注意这行代码必须放到定义了centerx之后 def update(self): --snip-- if self.moving_left:self.center -= ai_settings.ship_speed_factor if self.moving_right:self.center += ai_settings.ship_speed_factor self.rect.centerx = self.center
定义的center属性类型为float,可以储存浮点数;尽管self.rect.centerx = self.center还是只会储存center的整数部分,但由于center的增加速率由原来的1.0变成了1.5,rect.centerx想要达到下一个整数值的速率也间接增大了,也就实现了加速的目的
【不妨添加飞船上下移动的代码,使游戏更加有趣】
【限制活动范围】
我们不想让飞船左右移动超出创建的屏幕范围,可以修改update( )方法:
def update(self): if self.moving_right and self.rect.right < self.screen_rect.right:# 不是 <1200,逻辑严谨 self.center += self.ai_settings.ship_speed_factor if self.moving_left and self.rect.left > 0: self.center -= self.ai_settings.ship_speed_factor
这样,只有飞船在限定范围才会发生“移动”
当还未学习这种方法时,我copy了网上的一种方法,虽然不好,但贴出来以作警示:
if self.rect.left < 0: self.rect.left = 0 elif self.rect.right > 1200: self.rect.right = 1200
不同于“在限定范围才会发生移动”不同,这是“若超出限定范围立刻回归”,不太好
【重构check_event( )】
由于check_event( )函数是检测事件的,随着游戏开发的进行,它将会越来越长。因此我们先将处理KEYDOWN事件和KEYUP事件剥离出来,分开储存在game_functions.py文件中,但仍与check_events( )函数有关联
【注意:重构时务必检查传入函数的参数】
--snip--
重构check_event( )函数后,响应按键时要稍微注意连续使用if和使用if ... elif ...的区别:
[连续使用if]表明各个按键之间是独立的,按下→键飞船向右移动,如果这时不松开→键直接按下←键,pygame默认响应最新的事件,飞船是会向左移动的,哪怕你→键还按着
[使用if ... elif ...]的话,当按下→键后再按下←键,pygame对按键的响应还停留在属于→键的if(或elif)中,只要这时不松开→键,pygame就无法响应其它键
【设置子弹】
关于子弹,我们首先设置单个子弹的相关属性,并存储到Settings类中:
self.bullet_speed_factor = 1 self.bullet_width = 3 self.bullet_height = 15 self.bullet_color = 15, 15, 15
随后,我们就像定义飞船那样,再定义一个Bullet类,需要注意的是这个Bullet类继承自pygame的sprite模块的Sprite类。
Sprite,/spraɪt/,精灵,可以看作是一种可以在屏幕上移动的图像对象,能与其它图像对象交互,可以营造出“碰撞”的效果。bullet在后面的代码会触碰“外星人”,因此首先将其设置为Sprite子类
from pygame.sprite import Sprite class Bullet(Sprite): def __init__(self, ai_settings, screen, ship): super().__init__() self.screen = screen self.rect = pygame.Rect(0, 0, ai_settings.bullet_width, ai_settings.bullet_height) self.rect.centerx = ship.rect.centerx self.rect.top = ship.rect.top self.y = float(self.rect.y) self.color = ai_settings.bullet_color self.speed = ai_settings.bullet_speed_factor def update(self): self.y -= self.speed self.rect.y = self.y def draw_bullet(self): pygame.draw.rect(self.screen, self.color, self.rect)
下面还是慢慢分析吧...
super( ).__init__( )
但凡是继承自Sprite的类都需将Sprite的属性继承并初始化,【详细情况未知?】
self.screen = screen
这一行代码反倒是我疑惑最多的地方,在Bullet类中只是将screen初始化了,并没有初始化ai_settings和ship。我在后面额外将ai_settings和ship也初始化绑定到self上了,并没有影响游戏的运行。
我的理解是,在最后的pygame.draw.rect( ... )中对screen进行了操作,就得绑定到self上;而ai_settings和ship并没有进行操作,只是单纯地“借用”了两者的值。
在编程能力还不熟练的时候,可以将所有的形参都初始化,往后再慢慢学习
self.rect = pygame.Rect(0 , 0 , ai_settings.bullet_width , ai_settings.bullet_height)
self.rect.centerx = ship.rect.centerx
self.rect.top = ship.rect.top
由于子弹并非基于图像,所以我们得使用pygame的Rect类(注意Rect是一个class)从空白创建一个矩形。
创建子弹矩形时传入的参数依次为:矩形左上角的x坐标,矩形左上角的y坐标,所创建矩形的宽度,所创建矩形的高度;将得到的矩形图像储存在self.rect中。
我们首先将该图像的大小设置,位置的话随便,之后根据子弹从飞船射出,设置子弹与飞船的centerx值和top值相同。这一步可以回忆一下设置Ship类时self.rect.centerx = self.screen_rect.centerx以及self.rect.bottom = self.screen_rect.bottom
self.y = float(self.rect.y)
这里的代码参考更改飞船速度时的self.center = float(self.rect.centerx),都是为了能够更精细地控制子弹的速度。体现这一点的就在update( )方法中:
self.color = ai_settings.bullet_color
def update(self):
self.y -= self.speed
self.rect.y = self.y
这里不妨比对一下Ship类的update( )方法,都是为了实现图像的平滑移动
self.speed = ai_settings.bullet_speed_factor
def draw_bullet(self):
pygame.draw.rect(self.screen , self.color , self.rect)
同样地,这里也可以对比Ship的blitme( )方法。由于飞船是基于图像的,所以我们通过让屏幕surface调用blit( )方法:self.screen.blit(self.image , self.rect),传入形参「图像」和「图像位置」来将飞船显示到屏幕上。
而子弹不基于图像,是通过pygame.Rect( )创建的矩形,因此将子弹显示到屏幕上是通过pygame.draw.rect( )方法,该方法的实际参数为:pygame.draw.rect(surface , color , rect , width = 0),在上述的代码中,由于要传入color,将其绑定到self后,基于与定义blitme( )同样的目的定义draw_bullet( )
随后我们引入【Group】这个概念
我们知道飞船能够形成平滑移动的效果是依靠update( )方法,不断更改self.rect.centerx的值实现的;而让子弹往上运动也一样,通过update( )不断更改self.y。而为了更好地管理屏幕中可能会出现的所有子弹,我们想定义一个组:
bullets = pygame.sprite.Group()
这个Group也是sprite模块中的一个类,上面代码就是创建一个Group实例。Group类似列表list,它作用是为里面的所有子弹执行相同的操作,比如update( )
接下来是响应空格键,玩家按下空格键,子弹将从飞船顶端射出,并持续向上运行;下面代码添加到game_functions模块的check_keydown_events( )函数中:
elif event.key == pygame.K_SPACE: new_bullet = Bullet(ai_settings, screen, ship) bullets.add(new_bullet)
当Pygame检测到玩家按下空格键后,首先创建一个Bullet类的实例,随后立即将这个实例储存到Group中。
再然后,由于玩家按下空格键产生了子弹,必须让子弹飞,在函数update_screen( )中将子弹添加到屏幕上:
def update_screen(): --snip-- for bullet in bullets.sprites(): bullet.draw_bullet()
调用bullets组的sprites( )方法,可以返回一个包含储存在内的所有精灵的列表,并为每个精灵执行draw_bullet( )方法,使其显示在屏幕上
屏幕上显示的子弹会出现在飞船顶端,模拟为飞船射出,随后得让子弹向上运动,于是在主循环中添加下面代码:
while 1: gf.check_events(...) ship.update() bullets.update() gf.update_screen(...)
这样就实现了飞船发射子弹的效果
你得注意,对bullets组直接调用了update( )方法,而调用draw_bullet( )方法时却是通过for循环为其中的每个子弹调用draw_bullets( )
值得一提的是,类似ship.blitme( )、bullet.draw_bullet( )这样,将图像显示在surface上的代码通常是包含在game_fucntions模块的update_screen( )方法中的,当然你可以将这两个方法都从update_screen( )中提取出来,但必须放在screen.fill( )和pygame.display.flip( )之间。放到上面了,screen.fill( )为窗口填充颜色会覆盖飞船和子弹、放到下面了,每次主循环都错过了pygame.display.filp( )的更新窗口,到下一循环又被screen.fill( )干掉了...
除了将在surface上显示图像的方法放到update_screen( )方法中外,诸如ship.update( )、bullets.update( )等,为了实现营造图像平滑移动效果的update方法,都应该显式地置于主循环中
将ship.blitme( )等方法打包在update_screen( )方法中是有好处的,类似ship.update( )、bullets.update( )都是在修改相关的数据,待全部修改完成后,再一次性通过update_screen( )显示
【删除已消失的子弹】
从之前不设置飞船的边界,飞船会无限地跑出游戏窗口之外来看,子弹射出到从窗口上边界消失,实际上依然存在,它们的y坐标变成负数,且越来越小。这些窗口外“看不见”的子弹将继续消耗内存和处理能力,因此我们需要删除它们。
while 1: --snip-- for bullet in bullets.copy(): if bullet.rect.bottom <= 0: bullets.remove(bullet) # print(len(bullets)) gf.update_screen(ai_settings, screen, ship, bullets)
注意那个copy( )方法,在for循环中,不应从要循环的列表或编组中删除元素,所以我们通过copy( )方法创建一个bullets组的副本。
遍历bullets的副本,检查其中每个bullet是否超过窗口上方(rect.bottom <= 0),若是,则将该bullet通过remove( )从bullets组中移除;还可敲上print(len(bullets))的代码,从终端窗口中可以看到游戏中子弹的数量,检验已消失的子弹确实被删除了
【番外(什么鬼?)】
关于for循环中不应从要循环的队列中删除元素,我们可以举个简单的例子:遍历一个2到10的list,剔除其中的合数,保留质数
L = list(range(2, 11)) for x in L: for i in range(2, x): if x % i == 0: L.remove(x) break
结果却显示L为[2, 3, 5, 7, 9],其中的元素{9}没有被剔除;但如果你遍历的不是L本身,而是L的副本,比如for x in L[:]:,结果会是让人满意的[2, 3, 5, 7]
为什么会这样?我认为,for循环是依据下标遍历的,从下标0开始一直到下标len(list/tuple)。上面的例子有一段特殊的序列:[8, 9, 10],难得的连着三个都为合数。当for依据下标遍历到{8}时,假设这时的下标为n,{8}符合remove的条件,{8}被剔除;随后由于{8}被剔除,后面的元素({9}和{10})往前补位,{9}就来到了之前{8}所对应的下标n的位置。for认为下标n已经遍历过了,这时应该遍历到n+1的位置,于是就跳过了{9}!直接检查{10}了
也就是说,{3}、{5}、{7}被保留并不是因为是质数,而是for循环中压根就没对它们进行筛选,直接忽略了!所以哪怕{3}、{5}、{7}都被替代为合数,上述代码依旧不会将它们筛选出。所以这个筛选质数的算法有大bug
若遍历的是L[:],在for循环中即使remove掉L中的元素,L[:]依旧不会受到任何影响,因为L[:]只是临时创建的L的一个副本,连指向都没有的副本。
【重构update_bullets( )】
为了尽量简化while循环中的代码,我们将下列代码分离:
bullets.update() for bullet in bullets.copy(): if bullet.rect.bottom <= 0: bullets.remove(bullet)
将上面的代码封装在game_functions模块的update_bullets(bullets)函数中
【self】
在编写《外星人入侵》的游戏代码中有留意到,传入的参数未必都需要绑定到self上,我目测大致有以下情况:
①赋值式(=)时,若参数在右侧,无需绑定self,因为只是单纯地引用参数的内容;如果是比较式(>)、(<=),则必须要绑定self
②被内部方法再次调用时(即再次作为参数),必须绑定self
但要想判断某个参数在代码中的情况,还得先编写代码,再返回前面补齐self.,暂时先将传入的全部参数都绑定到self上
【创建外星人】
创建外星人的类与Ship类并没有什么不同:
class Alien(pygame.sprite.Sprite): def __init__(self, ai_settings, screen): super().__init__() self.ai_settings = ai_settings self.screen = screen self.image = pygame.image.load('images/alien.bmp') self.rect = self.image.get_rect() self.screen_rect = self.screen.get_rect() self.rect.x = self.rect.width self.rect.y = self.rect.height def blitme(self): self.screen.blit(self.image, self.rect)
上述代码中,我们暂时定义Alien的位置在窗口左上角附近,但还没有编写Alien的update( )方法。
然后,与Ship类似,将Alien的blitme( )方法放在update_screen( )函数中,细节略
【创建一行外星人】
首先创建一个Group:
aliens = pygame.sprite.Group()
在这个Group中,我们储存的外星人每一个的位置都不同,因此要分别定义它们的横坐标;我们在game_functions模块中定义:
def creat_fleet(ai_settings, screen, aliens): sample_alien = Alien(ai_settings, screen) alien_width = sample_alien.rect.width available_space_x = ai_settings.screen_width - 2 * alien_width numbers_alien_x = int(available_space_x / (2 * alien_width)) for alien_number in range(numbers_alien_x): alien = Alien(ai_settings, screen) alien.rect.x = alien_width + 2 * alien_width * alien_number aliens.add(alien)
①我们首先创建一个sample_alien实例,目的是获取单个外星人的宽度,方便根据外星人的宽度和游戏窗口的宽度,酌情安排所有外星人的空间
②然后进行计算,规定每个外星人距离窗口左右边界的距离不得小于1个外星人的宽度,据此计算出所有外星人可用的x轴空间;又规定每个外星人之间的间距为1个外星人的宽度,也就假设每个外星人所拥有的x轴空间其实为2倍的外星人宽度;通过available_space_x除以2 * alien_width,求得窗口的一行应当放置的外星人个数
③由于每个外星人的横坐标不同,因此通过遍历range设置x坐标(即alien.rect.x代码行),最后将各个横坐标不同的外星人添加入aliens组中
【创建多行外星人】
首先考虑数据,我们规定可用的垂直空间为:游戏窗口的高度-第一行外星人的上边距(1个外星人高度)-飞船高度-2倍外星人高度;其中2倍的外星人高度是留给飞船的设计空间
这样,模仿创建一行外星人的代码来编写创建多行外星人,只需使用两个嵌套在一起的循环:
def creat_fleet(ai_settings, screen, ship, aliens): sample_alien = Alien(ai_settings, screen) alien_width = sample_alien.rect.width alien_height = sample_alien.rect.height available_space_x = ai_settings.screen_width - 2 * alien_width numbers_alien_x = int(available_space_x / (2 * alien_width)) available_space_y = ai_settings.screen_height - 3 * alien_height - ship.rect.height numbers_alien_y = int(available_space_y / (2 * alien_height)) for number_alien_y in range(numbers_alien_y): for number_alien_x in range(number_alien_x): alien = Alien(ai_settings, screen) alien.rect.y = alien_height + 2 * alien_height * number_alien_y alien.rect.x = alien_width + 2 * alien_width * number_alien_x aliens.add(alien)
然后,我们需在主游戏模块instance的while循环之前,提前调用create_fleet( )函数创建外星人群
最后,更新update_screen( )函数,添加一条aliens.draw(screen)【why......】
【外星人移动】
设置了初始的外星人布局后,我们令外星人移动。
我们的设想是,一开始外星人向右移动,当触碰到屏幕边界,往下移动一段距离后,改变方向往左移动;再次触碰到边界时又向下并反向,不断循环:
class Settings(object): def __init__(self): --snip-- self.fleet_drop_speed = 10 self.fleet_direction = 1
首先在设置类中定义外星人向下移动的速度,以及由于外星人整体只有左右移动,定义fleet_direction为1:当向右移时为1,当向左移时为-1
class Alien(pygame.sprite.Sprite): --snip-- def check_edges(self): screen_rect = self.screen.get_rect() if self.rect.right >= screen_rect.right: return True elif self.rect.left <= 0: return True def update(self): self.x += self.speed * self.ai_settings.fleet_direction self.rect.x = self.x
定义check_edges( )方法旨在检测某个瞬间,任意一个外星人是否触摸到边界,若是,则通过返回True值来表示;而update( )方法令外星人随着主循环而移动,只是为其添加了一个方向:fleet_direction
然后我们在game_functions模块中添加{当检测到外星人触碰边界时,外星人群的向下位移和方向改变}:
def check_fleet_edges(ai_settings, aliens): for alien in aliens.sprites(): if alien.check_edges(): change_fleet_direction(ai_settings, aliens) break def change_fleet_direction(ai_settings, aliens): for alien in aliens.sprites(): alien.rect.y += ai_settings.fleet_drop_speed ai_settings.fleet_direction *= -1
首先通过check_fleet_edges( )函数检测,整个外星人群中是否有某个外星人触碰到边界;当有外星人触碰边界,调用change_fleet_direction( )函数
change_fleet_direction( )函数先令整个外星人群向下移动,再更改fleet_direction的值。随着fleet_direction不断在1和-1间变动,关联着fleet_direction值的Alien的update( )方法将不断调整外星人的x值
最后,由于新添了Alien的update( )方法,需像ship.update( )和bullets.update( )那样添加到主循环的screen.fill( )和pygame.display.flip( )之间。并且,在调用Alien的update( )方法之前调用check_fleet_edges( ),保证外星人群的转向。
因此我们像封装update_bullet( )那样,封装这两个方法于game_functions模块中:
def update_alien(ai_settings, aliens): check_fleet_edges(ai_settings, aliens) aliens.update()
然后将update_alien( )函数置于主循环中即可
【出现了未知的Bug,只有一列的外星人在运动;但是注释掉update_alien( )函数,屏幕上还是显示许多列外星人...】
【射杀外星人】
当子弹射击外星人时,我们要检查“碰撞”,查看两者是否重叠在一起。若是,则营造射杀的效果
我们通过pygame.sprite.groupcollide( )来检测两个编组的成员之间的碰撞:
def update_bullet(aliens, bullets): --snip-- collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
方法groupcollide( )实为:pygame.sprite.groupcollide(group1 , group2 , dokill1 , dokill2),它相当于多次调用pygame.sprite.spritecollide( )方法
groupcollide( )作用于两个Group,其所需要的前两个参数为[相互碰撞的两个组],而后两个参数为[当发生碰撞后是否删除对应的组中元素]
groupcollide( )将返回一个字典,指向collisions变量,其中包含了发生碰撞的子弹和外星人,在这个字典中,键为子弹,而值则为对应的外星人。在后面实现计分系统时,会用到这个字典。
由于groupcollide( )的作用机理是快速遍历两个编组,检查是否有rect重叠,因此上述的代码也可以添加到update_alien( )中
【生成新的外星人群】
当一个外星人群被消灭后,根据游戏规则,我们要创建一群新的外星人且难度增大。鉴于外星人的消灭在update_bullet( )代码中,因此仍在里面添加代码。
我们的思路是,检查aliens编组是否为空,若为空,表明外星人群已消灭,再次调用create_fleet( )函数创建新的外星人群,并提高外星人群的移动速度:
def update_bullet(ai_settings, screen, ship, bullets, aliens): --snip-- if len(aliens) == 0: bullets.empty() create_fleet(ai_settings, screen, ship, aliens) ai_settings.alien_speed_factor += 1
你会发现随着屏幕元素的增多,游戏运行速度下降了。这是因为Pygame在每次循环中要做的工作更多了。可尝试调整Settings类中的属性,找到合适的值。
【检测外星人与飞船的碰撞】
外星人与飞船碰撞后,需要执行的任务很多,包括:删除余下的所有外星人和子弹、飞船重新居中、创建新的外星人群。为单纯地检测外星人与飞船碰撞的效果,我们在这里先不执行这些任务:
def update_alien(ai_settings, ship, aliens): --snip-- if pygame.sprite.spritecollideany(ship, aliens): print(''Ship hit!'')
更新外星人的位置后,立即检测外星人与飞船的碰撞,因此将上述代码添加到update_alien( )中
方法pygame.sprite.spritecollideany( )接收两个参数:一个精灵和一个编组;当精灵和编组中的成员未发生碰撞时,其返回None;如果检测到碰撞,则返回与飞船碰撞的外星人
【响应碰撞&统计游戏信息】
我们可以在飞船与外星人碰撞后,删除飞船并重新创建一个位于窗口中央;但我们不这样做,我们通过跟踪游戏的统计信息来记录飞船被撞了多少次:
首先我们规定,一次游戏中只有3艘飞船,即飞船被撞毁3次后游戏结束,为此在game_functions模块中添加游戏属性:
self.ship_limit = 3
然后我们创建一个新的game_stats模块,其中包含统计信息的新类——GameStats:
class GameStats(object): def __init__(self, ai_settings): self.ai_settings = ai_settings self.reset_stats() def reset_stats(self): self.ships_left = self.ai_settings.ship_limit
我们允许玩家在死亡3次后,再次重新开始游戏,这时就得将游戏中的相关属性调整:比如飞船的生命值重新更改为3。
于是我们不将统计信息置于__init__( )方法内,而是放在reset_stats( )方法中,这样,每当玩家需要重新开始游戏的时候,我们就调用reset_stats( )来重置统计信息,而不是逐个地将统计信息修改回来
需要注意的是,我们定义在reset_stats( )中的属性仍然可以通过普通的方法进行调用
之后,我们在主循环之前创建GameStats类的实例:
stats = GameStats(ai_settings)
然后我们在game_functions模块中定义ship_hit( )函数,执行当飞船与外星人碰撞后的操作:
import time def ship_hit(ai_settings, screen, ship, bullets, aliens, stats): stats.ships_left -= 1 aliens.empty() bullets.empty() create_fleet(ai_settings, screen, ship, aliens) ship.center_ship() time.sleep(1)
首先通过stats.ships_left -= 1更新统计数据,然后立刻将屏幕上的外星人和子弹清除,并创建一群新的外星人
你会发现,我们还得需要将飞船更新。我们使用统计信息的目的就在此,不删除已有的飞船,因此统计信息中已经“记住”飞船死亡一次,我们只需将飞船恢复到屏幕中央下方的起始位置
为此,我们在Ship类中定义一个新方法center_ship( ):
def center_ship(self): self.center = self.screen_rect.centerx
【注意我们有self.center = float(self.rect.centerx)这一行代码,记住,当有这种通过小数暂存数据的代码的时候,若要更新数据,一定要直接更新self.center,而不是self.rect.centerx】
因此,center_ship( )中的代码不能写成self.rect.centerx = self.screen_rect.centerx!
最后,为了让玩家在新外星人群出现前注意到发生了碰撞,我们很贴心地让游戏暂停1秒
最最后,我们不要忘了更新update_alien( )函数:
def update_alien(ai_settings, screen, ship, bullets, aliens, stats): if pygame.sprite.spritecollideany(ship, aliens): ship_hit(ai_settings, screen, ship, bullets, aliens, stats)
此外,重新开始游戏,外星人群的运动速度应该被恢复,因此将Settings类中的alien_speed_factor也储存到GameStats类的reset_stats( )方法中,然后耐心修改调用该属性的地方
【外星人到达屏幕底端】
已经设置好外星人与飞船碰撞的处理代码,当外星人到达屏幕底端时的处理就变得很简单
def check_aliens_bottom(ai_settings, screen, ship, bullets, aliens, stats): for alien in aliens.sprites(): if alien.rect.bottom >= ai_settings.screen_height: ship_hit(ai_settings, screen, ship, bullets, aliens, stats) break
由于[外星人到达屏幕底端]和[外星人与飞船碰撞]的处理一样,我们可以直接在检查到外星人到达底端的时候,调用ship_hit( )。感受下将各项功能分割,再通过函数调用的便捷性,create_fleet( )不也是么
将上述代码储存在game_functions模块中,我们还不忘在update_alien( )函数中调用:
def update_alien(ai_settings, screen, ship, bullets, aliens, stats): --snip-- check_aliens_bottom(ai_settings, screen, ship, bullets, aliens, stats)
【游戏结束】
目前我们通过ship_limit赋值给ships_left来控制飞船的生命值,但当飞船死亡3次时游戏仍在继续,下面来设置死亡3次后游戏结束:
def reset_stats(): self.game_active = True
先在统计信息中设置游戏状态,初始时都为True
def ship_hit(...): if stats.ships_left > 0: stats.ships_left -= 1 else: stats.game_active = False
在响应飞船被撞毁的函数ship_hit( )中添加if语句进行判断,当死亡3次后,更改代表游戏状态的数值game_active为False
while 1: gf.check_events(...) if stats.game_active: ship_update() gf.update_bullets(...) gf.update_aliens(...) gf.update_screen(...)
由上面代码可以看出,当game_active为False时,与游戏元素运动有关的3个函数是不会执行的;而check_events( )和update_screen( )是即使游戏处于非活动状态也应调用,例如:check_events( )知道玩家是否按下Q键退出游戏,update_screen( )使得当玩家开始新游戏时屏幕得以刷新
【设置按钮】
我们让玩家在一开始就通过点击''Play''按钮开始游戏,因此先得让游戏处于停止运行状态:
class GameStats(object): def __init__(self, ai_settings): --snip-- self.game_active = False
Python没有内置的创建按钮的方法,我们就创建一个Button类,用于创建带标签的实心矩形:
import pygame class Button(object): def __init__(self, screen, msg):【←为什么书上这里会有 ai_settings形参?!】 self.screen = screen self.screen_rect = self.screen.get_rect() self.width , self.height = 200, 50 self.button_color = 0, 255, 0 self.text_color = 255, 255, 255 self.font = pygame.font.SysFont(None, 48) self.rect = pygame.Rect(0, 0, self.width, self.height) self.rect.center = self.screen_rect.center self.prep_msg(msg) def prep_msg(self, msg): self.msg_image = self.font.render(msg, True, self.text_color, self.button_color) self.msg_image_rect = self.msg_image.get_rect() self.msg_image_rect.center = self.rect.center def draw_button(self): self.screen.fill(self.button_color, self.rect) self.screen.blit(self.msg_image, self.msg_image.rect)
上述代码储存在新开的button模块中。下面进行解析:
def __init__(self , screen , msg):
self.screen = screen
self.screen_rect = self.screen.get_rect( )
传入参数screen和msg,其中msg为message的缩写,若要创建''Play''按钮,则这时要传入的msg为字符串'Play'。
鉴于Python没有内置的创建按钮的方法,我们的打算是:先创建一个矩形,再将文本'Play'置于矩形中,当检测到鼠标点击矩形所在的区域的时候,修改game_active为True,实现按钮功能
self.width , self.height = 200 , 50
self.button_color = 0 , 255 , 0
self.text_color = 255 , 255 , 255
self.font = pygame.font.SysFont(None , 48)
这里是按钮矩形及其文本的一些设置:
self.width和self.height分别设置即将创建的矩形的宽和高
self.button_color和self.text_color分别设置按钮矩形的颜色为绿色、文本的颜色为白色
self.font = pygame.font.SysFont(None , 48)是对即将创建的文本的设置,实参None让Pygame使用默认字体,48表示字号。
pygame.font.SysFont( )返回的是一个Font对象,Pygame需要将Font对象渲染为图像来处理文本,否则无法绘制在屏幕上。对Font对象的渲染在下面。
self.rect = pygame.Rect(0 , 0 , self.width , self.height)
self.rect.center = self.screen_rect.center
这一步简单地创建一个矩形,并设置到屏幕中央
def prep_msg(self , msg):
①self.msg_image = self.font.render(msg , True , self.text_color , self.button_color)
②self.msg_image_rect = self.msg_image.get_rect( )
③self.msg_image_rect.center = self.rect.center
为了使代码更Pythonic,将渲染文本的代码置于prep_msg( )方法中。
①处通过Font.render( )方法实现了对文本的渲染,Font.render( )接收4个形参。msg为传入的文本字符串,而形参True指定开启/关闭反锯齿功能;第三个参数即为文本的颜色
第四个self.button_color参数有点特殊,它的意义是[文本的背景色],如果没有就默认为透明。我们将字符串渲染到上一步的矩形中,由于self.button_color本身就是为矩形准备的颜色参数,所以无论这里是[透明]还是[self.button_color],都不会有太大差别。因此该参数可传可不传,因为文本后面也会置于self.button_color的颜色背景中。
②③处先获取被渲染的文本图像的矩形,再让其在按钮矩形上居中
def draw_button(self):
self.screen.fill(self.button_color , self.rect)
self.screen.blit(self.msg_image , self.msg_image.rect)
最后再设置一个draw_button( )方法将按钮绘制到屏幕中,这个方法是独立的,并不是像prep_msg( )那样只是对代码的重构。
screen.fill( )在创建游戏屏幕背景色的时候出现过,只是那时仅有一个参数bg_color,而这个方法的第二个参数是用于指定范围的。当没有传入该参数的时候,默认将第一个颜色参数作用于整个屏幕。
这里传入第二个参数self.rect,即按钮的位置。通过将这个位置填充为self.button_color指定的颜色,模拟按钮的存在。
最后像blit飞船ship的图像那样,将文本图像blit到屏幕中
【屏幕上绘制】
定义好Button类后,将其与游戏代码联系起来:
首先,导入Button类并创建实例:
from button import Button play_button = Button(screen, 'Play')
然后,更新update_screen( )函数。
我们只是想当游戏处于非活动状态时才绘制Play按钮,而游戏运行期间不进行绘制,因此在update_screen( )函数中添加:
if not stats.game_active: play_button.draw_button()
【开始游戏】
将按钮绘制到屏幕上后,我们要对用户使用鼠标点击按钮产生反应,即检测并反应鼠标事件
--snip-- elif event.type == pygame.MOUSEBUTTONDOWN: mouse_x , mouse_y = pygame.mouse.get_pos() check_play_button(stats, play_button) def check_play_button(stats, play_button) if play_button.rect.collidepoint(mouse_x, mouse_y): stats.game_active = True
首先在check_events( )函数中添加一行代码,检测事件的类型是否为[鼠标点击],如果是,就通过pygame.mouse.get_pos( )方法,立即获取本次鼠标点击的位置mouse_x和mouse_y
获取鼠标点击位置后,调用check_play_button( )函数,判断本次点击是否在''Play''按钮的范围内。
在check_play_button( )函数的定义中,使用Rect.rect.collidepoint( )方法。该方法检测传入的点(mouse_x , mouse_y)是否位于该矩形的内部;如果是,修改game_active为True,开始游戏
【重置游戏】
尝试开始游戏并撞机3次后,游戏会停止,这时的''Play''按钮再次出现,按下即可重新开始游戏。
但是由于stats中的ship_limit属性已经减小为0,这种情况下再次撞机,就没有“三条生命”了,直接出现''Play''按钮。
为此,我们在check_play_button( )函数中增加一些功能,主要是重设数据stats
def check_play_button(ai_settings, ship, bullets, aliens, stats, play_button, mouse_x, mouse_y): if play_button.rect.collidepoint(mouse_x, mouse_y): stats.game_active = True stats.reset_stats() bullets.empty() aliens.empty() create_fleet(ai_settings, ship, aliens, stats) ship.center_ship()
新添加的代码与ship_hit( )函数相似,只是少了ship_limit -= 1等代码
【将按钮切换到非活动状态】
游戏过程中,若玩家不小心用鼠标点击到''Play''按钮所在的区域,游戏依然会做出响应,重新开始。
简单的就是check_play_button( )函数中,添加重置游戏的条件:
if play_button.rect.collidepoint(mouse_x, mouse_y) and not stats.game_actvie: --snip--
添加not stats.game_actvie条件,即,当game_actvie为False时,点击''Play''按钮才能启动重启游戏的功能;若game_active为True,怎么点击都无效
个人认为之所以之前''Play''按钮不可见,点击后仍可生效,是因为游戏一开始就为了显示''Play''按钮而调用了stats.reset_stats( ),也就是说,''Play''按钮从一开始就已经存在。之后由于不满足play_button.draw_button( )的条件,以及flip( )导致按钮不可见,但当鼠标点击到按钮的范围时,仍满足play_button.rect.collidepoint(mouse_x , mouse_y)条件
【隐藏光标】
避免游戏过程中光标的添乱,我们可以设置当game_active为False时隐藏光标,当game_active为True时又显示光标
if play_button.rect.collidepoint(mouse_x, mouse_y) and not stats.game_active: pygame.mouse.set_visible(False) --snip--
又:
else: stats.game_active = False pygame.mouse.set_visible(True)
【开始游戏快捷键P】
当屏幕出现''Play''按钮时,我们除了用鼠标点击按钮外,还可以设置使用快捷键P
首先要重构check_play_button( )函数,原函数为:
def check_play_button(...): if play_button.rect.collidepoint(mouse_x, mouse_y) and not stats.game_active: pygame.mouse.set_visible(False) stats.reset_stats() stats.game_active = True bullets.empty() aliens.empty() create_fleet(...) ship.center_ship()
现将if代码块中的代码包装到start_game( )函数中,也就是:
def check_play_button(...): if play_button.rect.collidepoint(mouse_x, mouse_y) and not stats.game_active: start_game(...)
然后在check_keydown_events( )函数中添加:
elif event.key == pygame.K_p: if not stats.game_active: statr_game(...)
不要忘了规定只在stats.game_active为False时才能重启游戏;还要小心传参
【等级提升】
为了使游戏更具趣味性和挑战性,我们在每次完全击杀外星人群后,加快游戏的节奏
所谓“加快游戏的节奏”,是指将ship、bullet、alien的speed值增大
首先我们对Settings类进行修改,将游戏设置划分为静态设置和动态设置两组:
Class settings(object): def __init__(self): --snip-- self.speedup_scale = 1.1 self.initialize_dynatic_settings() def initialize_dynatic_settings(self): self.ship_speed_factor = 1.5 self.bullet_speed_factor = 1 self.alien_speed_factor = 1
新定义的speedup_scale为加速范围,用于乘以ship、bullet、alien的speed值,实现加速;而initialize_dynatic_settings( )方法与GameStats类的reset_stats( )方法类似,都是为了在重置游戏的时候将游戏的相关属性复原
def increase_speed(self): self.ship_speed_factor *= self.speedup_scale self.bullet_speed_factor *= self.speedup_scale self.alien_speed_factor *= self.speedup_scale
increase_speed()方法将三个speed值都增大
修改Settings类后,我们需要调用新添加的两个方法。
一个在update_bullet( )函数中:
if len(aliens) == 0: ai_settings.increase_speed() create_fleet(...)
因为负责检测外星人群是否被完全消灭的代码位于update_bullet( )函数中;
另一个在start_game( )函数中直接添加:
ai_settings.initialize_dynatic_settings()
我们知道,函数start_game( )用于[当飞船死亡次数超过3,游戏重置]时的情况,因此这时也会执行stats.reset_stats( ),同理
【显示得分】
每次射杀外星人后都将得分,为了将得分显示到屏幕上,我们首先想办法将一个数字blit到屏幕上
而其做法与将''Play''并无二致
首先创建一个scoreboard.py文件,并在game_stats的initialize_dynatic_settings( )方法中添加:
self.score = 0
然后,编写scoreboard模块中的代码:
import pygame class Scoreboard(object): def __init__(self, ai_settings, screen, stats): self.ai_settings = ai_settings self.screen = screen self.stats = stats self.screen_rect = self.screen.get_rect() self.text_color = 30, 30, 30 self.font = pygame.font.SysFont(None, 48) self.prep_score() def prep_score(self): score_str = str(self.stats.score) self.score_image = self.font.render(score_str, True, self.text_color) self.score_rect = self.score_image.get_rect() sefl.score_rect.right = self.screen_rect.right - 20 self.score_rect.rop = 20 def show_score(self): self.screen.blit(self.score_image, self.score.rect)
综上,你会发现Scoreboard类的内容与Button类的内容有一点相似
随后,在instance模块中import Scoreboard并创建实例sb;由于要绘制到屏幕上,再在update_screen( )函数中添加:
sb.show_score()
别忘了向update_screen( )函数添加实参sb
【更新得分】
目前我们的分数只是game_stats模块中的self.score = 0,下面让我们将self.score更新
首先确定单个外星人的“价值”,我们在Settings类的initialize_dynatic_settings( )方法中添加:
self.alien_points = 50
暂时指定每击杀一个外星人,分数增加50。之所以在initialize_dynatic_settings( )方法中添加,是因为之后随着游戏节奏的加快,会上调这个值;而当游戏重新开始时,应该恢复这个值为50
更新分数,前提是击杀了外星人,即子弹与外星人发生了碰撞,因此我们将目光投向update_bullets( )函数,因为该函数负责检测子弹与外星人的碰撞:pygame.sprite.groupcollide(bullets , aliens , True , True),由于update_bullets拥有上面的代码行,因此更改代码:
collision = pygame.sprite.groupcollide(bullets, aliens, True, True) if collision: stats.score += ai_settings.alien_points sb.prep_score()
每当发生一次碰撞,方法groupcollide( )返回一个字典,当检测到collision为True时,就说明这一刻成功击杀了外星人,更新stats.score值
更新stats.score值后,不要忘了,绘制到屏幕上的分数是通过prep_scroe( )方法中的score_str = str(self.stats.score)获取的,所以也得调用一次sb.prep_score( ),更新屏幕上的数值
【同时消灭】
当创建大子弹同时消灭多个外星人的时候,你会发现,pygame还是只添加一个外星人的分值
【groupcollide( )】
方法groupcollide( )返回的是碰撞的双方组成的一个字典dict,由于传入的形参为bullets、aliens,因此该字典为{bullet:alien};倘若一颗大子弹同时击中多个外星人,则返回的字典为{bullet:[alien1 , alien2 ... , alienx}。
之前的代码只是检测一次遍历中字典collision是否为True,若为True,则执行一次[加分]。当同时击中多个外星人,返回的仍是一个字典,检测为True后执行一次加分。
因此我们可以这样修改:
if collision: for aliens in collision.values(): stats.score += ai_settings.alien_points * len(aliens) sb.prep_score()
△我认为,在一次遍历中,存在不止发射了一颗子弹的情况;也就是说,collision并非总是含有一个键值对元素,有可能含有多个。因此,首先通过for aliens in collision.values( )获取遍历所有可能的键值对元素,获取每个值的长度(长度即一次性击杀的外星人数),通过乘以长度得到更准确的分数
【提高点数】
之前有提及,击杀单个外星人的分数为50,但随着游戏节奏的上升,这个值会变大
class Settings(object): def __init__(self): --snip-- self.scoreup_scale = 1.5 def increase_speed(self): --snip-- self.alien_points = int(self.alien_points * self.scoreup_scale)
我们注意到加快游戏节奏的代码主要存储在increase_speed( )中,因此先像定义self.speedup_scale那样,定义外星人分值的增加幅度self.scoreup_scale为1.5,再在increase_speed( )中改变self.alien_points值,并通过int( )取整
【将得分圆整】
“大多数街机风格的射击游戏都将得分显示为10的整数倍(即个位数恒为0)”
我们还将设置得分的格式,在大数字中添加用逗号表示的千位分隔符:
def prep_score(self): rounded_score = int(round(self.stats.score, -1)) score_str = '{: ,}'.format(rounded_score) --snip--
【round( )】
函数round( )通常通过指定第二个实参,让小数精确到小数点后多少位。而当第二个实参为负数时,round( )将圆整到最近的10、100、1000等整数倍
Python 2.7中,round( )总是返回一个小数值,因此使用int( );而Python 3可以省略对int( )的调用
再通过'{: ,}'.format( )进行千分位分隔,而format( )本身就返回str对象
【补充】
需要注意的是,我们更新屏幕上的数字是通过调用sb.prep_score( )的,而这个方法被我们置于update_bullets( )的if collision代码块中,也就是一旦有子弹与外星人碰撞,就更新stats.score的值并更新屏幕上的显示得分。
然而当三次死亡后我们再次点击'Play'开始游戏,这时的得分还没有更新!依旧保持为上一局的得分!只有当你击杀第一个外星人时,分数才会更新为50
原因就在于我们将sb.prep_score( )放在检测子弹碰撞的函数中,只要没有发生子弹碰撞,就不会调用sb.prep_score( ),屏幕上的分数也就无法更新
为了让重新开始游戏时的得分显示为'0',我们需要在其他地方再次调用sb.prep_score( )。而我们知道当重启游戏(点击'Play'按钮)时,调用的是start_game( )函数,因此在该函数添加:
sb.prep_score()
然后你会发现,重启游戏之后,得分就立刻变成'0'了
【最高分】
整场游戏中,我们打算将最高得分绘制到屏幕中央正上方,而绘制的步骤,与之前绘制得分无异:
首先在GameStats中添加high_socre = 0,注意不要将其添加到initialize_dynatic_settings( )中,因为每次重启游戏时都不会重置high_score值
然后返回Scoreboard类中:
def __init__(self): --snip-- self.prep_high_score() def prep_high_score(self): high_score = int(round(self.stats.high_score, -1)) high_score_str = '{: ,}'.format(high_score) self.high_score_image = self.font.render(high_score_str, True, self.text_color) self.high_score_rect = self.high_score_image.get_rect() self.high_score_rect.centerx = self.screen_rect.centerx self.high_score_rect.top = self.screen_rect.top def show_score(self): self.screen.blit(self.score_image, self.score_rect) self.screen.blit(self.high_score_image, self.high_score_rect)
这时运行程序,就可以看到屏幕正上方的'0'了
然后更新stats.high_score值,我们在game_functions模块中添加函数:
def update_high_score(stats, sb): if stats.score > stats.high_score: stats.high_score = stats.score sb.prep_high_score()
新添加的update_high_score( )函数比较了stats.score和stats.high_score的大小。如果符合条件,就更改stats.high_score的值,并且通过sb.prep_high_score( )更新屏幕上的数字图片
我们之前将sb.prep_score( )置于update_bullets( )函数中,因为在该函数中有检测子弹与外星人碰撞的代码,所以这次我们也将update_high_score( )放入其中:
if collision: for aliens in collision.values(): stats.score += ai_settings.alien_points * len(aliens) sb.prep_score() update_high_score(stats, sb) --snip--
【将high_score写入文件】
我们发现,high_score记录的是历史最高的分数值,然而当关闭游戏后再次运行程序,这个值会清零;因此我们需要将high_score这个值写入到文件中,让文件一直储存着这个值,当下次打开游戏的时候,就从文件中读取这个值。
首先我们在存有游戏代码的文件夹中创建一个txt文本,命名为high_score,打开high_score.txt,在文本中输入一个'0'
然后我们要读取这个文件夹中的内容,因此在prep_high_score( )方法中添加:
def prep_high_score(self): with open('high_score.txt') as file: content = int(file.read())# 记住read( )返回str对象 high_score = int(round(content, -1)) high_score_str = '{: ,}'.format(high_score) --snip--
这样,我们就将high_score.txt中的'0'读取出来了,并无缝对接到之前的代码中
然后使通过写入更新high_score值,就在update_high_score( )函数中进行修改:
def update_high_score(stats, sb): if stats.score > stats.high_score: stats.high_score = stats.score with open('high_score.txt', 'w') as file: file.write(str(stats.high_score))# write()只接收str对象 sb.prep_high_score()
通过写入模式('w')而不是附加模式('a')打开high_score.txt,将新的stats.high_score值存储进去
[注意] 到这一步,stats.high_score还是有用的,它的作用就是比较,不能删去
【注意】
尽管写入模式('w')在不存在目标文件时,会自动创建一个,但在这里必须提前手动创建一个high_score.txt文件。因为在整个pygame的执行顺序中,创建sb实例要早于调用update_high_score( )函数。而创建sb实例就意味着要执行[with open('high_score.txt') as file]的读取操作,读取操作不会自动创建文件,因此会报错FileNotFoundError
还有就是,如果你要“走后门”更改high_score.txt的内容,必须更改为int类型对象,如果是其它类型的对象(哪怕是None对象),都要重新修改代码以适应这种情况。
【提升等级】
我们标明每一群外星人的等级,以便玩家检查
首先在GameStats的reset_stats( )方法中添加:
self.level = 1
在Scoreboard类的__init__( )方法中调用self.prep_level( ),开始设置prep_level( ):
def prep_level(self): self.level_image = self.font.render(str(self.stats.level), True, self.text_color) self.level_rect = self.image.get_rect() self.level_rect.right = self.screen_rect.right - 20 self.level_rect.top = self.score_rect.bottom + 10
注意render( )的第一个参数必须为str对象
再将下列代码添加进show_score( )方法,解决绘制到屏幕上的问题:
self.screen.blit(self.level_image, self.level_rect)
剩下的就是随着游戏的进行,更新level值,我们添加进update_bullets( )函数中:
if len(aliens) == 0: ai_settings.increase_speed() create_fleet(ai_settings , ship, aliens, stats) stats.level += 1 sb.prep_level()# 很容易遗漏这一步,更新数值后应同步更新屏幕
参照sb.prep_score( ),再在start_game( )添加sb.prep_level( )
【显示飞船】
我们的设定是,玩家除了一开始的飞船外,还有三架备用的飞船,我们打算将这三架显示到屏幕上,提醒玩家还剩下多少飞船
我们之前绘制飞船是通过blit( )的,而现在飞船的数量不止1架,因此最好通过pygame.sprite.Group( )将它们囊括。通过创建Group( )可以像绘制外星人一样在屏幕上显示,你也可以把即将要显示的三架飞船看成是不会移动的微型外星人群
既然要模仿Alien,那么首先得让Ship类成为精灵:
class Ship(pygame.sprite.Sprite): def __init__(self, ai_settings, screen): super().__init__() --snip--
由于前面的代码基本已经完成,我们想编写显示飞船数量的代码,考虑到即将要编写的代码的主要作用是绘制,那么不妨直接写入scoreboard.py中,仅仅是影响执行顺序问题而已
from ship import Ship def prep_ships(self): self.ships = pygame.sprite.Group() for ship_number in range(self.stats.ship_limit): ship = Ship(ai_settings, screen) ship.rect.left = 10 + ship_number * ship.rect.width ship.rect.top = 10 self.ships.add(ship)
在新定义的prep_ships( )方法中,我们像绘制外星人群那样绘制了“飞船群”,还记得Group如果绘制到屏幕上吗?参考aliens的aliens.draw(screen),因此我们在show_score( )方法中添加:
def show_score(self): --snip-- self.ships.draw(self.screen)
这样,所谓的'show_score'已经名存实亡了(N_N)
之后便是更新屏幕的问题,也就是找到恰当的地方调用prep_ships( )
首先是start_game( )函数,在游戏开始之初就应该将三架飞船绘制到屏幕上;然后是ship_hit( )函数,因为ships组中的元素数量是由stats.ship_limit决定的,而更新stats.ship_limit值就在ship_hit( )函数中:
def ship_hit(...): if stats.ship_limit > 0: stats.ship_limit -= 1 else: stats.game_active = False pygame.mouse.set_visible(True) sb.prep_ships() --snip--
最后便是修改参数的问题了
【完结撒花!】
''''''''
【错误】
1、class的__init__( )方法中,定义属性时忘记添加self,根本关联不到实例上!
2、我认为,编写控制飞船持续移动的代码时,不能使用while语句而是改用if语句的原因,是要避免while语句和主循环冲突。主循环的作用在于持续不断地更新屏幕,移动的代码被封装在Ship类的upgrade( )方法中,在主循环中调用ship.upgrade( )时,若符合移动的条件,在主循环的无限重复下能不断地执行if语句
这有些难理解,但要记得,实现飞船的某些功能,要到Ship类中编写代码;check_events( )函数仅适用于检查事件,不直接进行游戏元素的功能实现
3、traceback提示ai_settings未定义,我反复看了self.ai_settings = ai_settings以及其他处的ai_settings都没发现...应该前面加self.
4、rect对象的top、bottom属性对于我们来说都是直观的,打开的屏幕最上面就是top、最下面就是bottom;但pygame所创建的游戏窗口原点(0 , 0)位于左上角,也就是说,在上下方向它与我们平常所创建的平面直角坐标系是相反的!从bottom到top,纵坐标的值不断减小。
因此在设置飞船可以上下移动的时候要小心,up时y值在减小、且判断不超出屏幕条件为rect.top > screen_rect.top,注意是大于号(>);down时则是相反
5、传入__init__的参数中,若该参数仅仅是引用其储存的值或内容,无需绑定到self上;若要在class中对其进行修改利用,必须绑定到self上,否则会报错未定义
比如Ship类的参数ai_settings、screen,ai_settings只是为了设置飞船速度而调用的一个值,本身与直接传入1.5没有什么不同;但screen参数在blitme( )方法中要进行利用:self.screen.blit(self.image , self.rect),需绑定到self上
6、关于draw和blit,我是这样想的。凭空创建的矩形图像,通过pygame.draw.rect( )来显示,包含在draw_...( )函数中;单个本地图像,通过self.Surface.blit( )显示,包含在blitme( )函数中;多个矩形图像,囊括在Group中,须遍历多个本地图像,囊括在Group中,使用Group特有的draw(Surface)函数,而不是以上三种
【问题】
①如何确定参数是否要绑定到self?
②混淆使用if ... elif ...和if ... if ...会有什么不良后果?
③定义飞船能同时相应多个方向键,为何往左上方移动时无法射出子弹,其它方向都可以?
④super( ).__init__( )继承的是Sprite的什么?
⑤为何pygame的原点(0,0)在左上角?
⑥Group.sprites与Group.copy有什么不同?我把两个替换着用都没有问题啊!?你说呢,就像list和list[:]一样,一个本身,一个副本,取决于你用在什么地方
⑦问题类似③,同时按住左键和右键,为什么飞船只能往下?
⑧绘制外星人通过aliens.draw(screen),那Alien类中的blitme( )方法还有没有用?
⑨random类是如何保证完全随机的?
⑩为什么update外星人后,全屏的外星人群只剩下一列在移动?
十分感谢StackOverflow的朋友,原因是在create_fleet( )函数时:
alien = Alien(...) alien.rect.x = alien_width + 2 * alien_width * number_alien_x alien.rect.y = alien_height + 2 * alien_height * number_alien_y
我们直接定义的是alien.rect.x,而返回到Alien类中,我们是先定义self.x = float(self.rect.x),再在update(self)方法中更改self.x并self.rect.x = self.x
由于在create_fleet( )中没有定义self.x,所以self.x值就是刚创建Alien实例时的默认值0;但是self.rect.x被定义了,所以暂时看到了满屏的外星人群
这时调用Alien的update( )方法:
def update(self): self.x += (self.speed * self.ai_setting.fleet_direction) self.rect.x = self.x
一次调用后self.x值为self.speed,你会发现,所有的外星人的self.x值都相同!再通过self..rect.x = self.x,原本一行外星人各自的self.rect.x值都不同,这下子就都一样了,全为self.speed!然后所有外星人都重叠了!
此外,之所以在后面射杀外星人中没有察觉到外星人的重叠,是因为“射杀”的设置造成的。我们检测到外星人的rect与子弹的rect有重合,就判定外星人“被射杀”。重叠的外星人是同一时间检测到与子弹重叠的,因此同时全部被“射杀”
【建议】因此在设定值时,留意是否为float( )承载的值,若是,要先定义该float( )值,不要嫌麻烦
11、复原ship位置时为什么不能直接通过ship.rect.bottom = screen_rect.bottom进行操作,而是必须通过ship.x、ship.y值?
12、突然发现没有将Ship类继承pygame.sprite.Sprite,更没有super( ).__init__( ),但是运行竟然没差别?
13、为什么书上的演示代码是import pygame.font,它在下面也是self.font = pygame.font.SysFont( ... ),完全可以import pygame就行了?