一个游戏实践-python
一个游戏实践-Python学习
0. 游戏概述
玩家控制着一艘最初出现在屏幕底部中央的飞船。玩家可以使用箭头键左右移动飞船,还可使用空格键进行射击。游戏开始时,一群外星人出现在天空中,他们在屏幕中向下移动。玩家的任务是射杀这些外星人。玩家将所有外星人都消灭干净后,将出现一群新的外星人,他们移动的速度更快。只要有外星人撞到了玩家的飞船或到达了屏幕底部,玩家就损失一艘飞船。玩家损失三艘飞船后,游戏结束。
1.准备工作
Pygame,这是一组功能强大而有趣的模块,可用于管理图形、动画乃至声音,让你能够更轻松地开发复杂的游戏。通过使用Pygame来处理在屏幕上绘制图像等任务,你不用考虑众多烦琐而艰难的编码工作,而是将重点放在程序的高级逻辑上.
安装Pygame包
1 |
|
2.开始游戏项目
2.1创建一个空白窗口并响应用户输入
创建一个空的Pygame窗口,供后面用来绘制游戏元素,如飞船和外星人。我们还将让这个游戏响应用户输入、设置背景色以及加载飞船图像。
1 |
|
- 行pygame.init()初始化背景设置,让Pygame能够正确地工作
- 调用pygame.display.set_mode()来创建一个名为screen的显示窗口,这个游戏的所有图形元素都将在其中绘制。实参(1200, 800)是一个元组,指定了游戏窗口的尺寸。通过将这些尺寸值传递给pygame.display.set_mode(),我们创建了一个宽1200像素、高800像素的游戏窗口
- 对象screen是一个surface。在Pygame中,surface是屏幕的一部分,用于显示游戏元素。在这个游戏中,每个元素(如外星人或飞船)都是一个surface。display.set_mode()返回的surface表示整个游戏窗口。我们激活游戏的动画循环后,每经过一次循环都将自动重绘这个surface。
- 游戏由一个while循环控制,其中包含一个事件循环以及管理屏幕更新的代码。事件是用户玩游戏时执行的操作,如按键或移动鼠标。为让程序响应事件,我们编写一个事件循环,以侦听事件,并根据发生的事件执行相应的任务。for循环就是一个事件循环
- 使用方法pygame.event.get()访问pygame检测到的事件,所有键盘和鼠标事件都将促使for循环运行。在这个循环中,我们将编写一系列的if语句来检测并响应特定的事件
- 单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,而我们调用sys.exit()来退出游戏
- 调用pygame.display.flip(),命令Pygame让最近绘制的屏幕可见。
- 在每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见。在我们移动游戏元素时,pygame.display.flip()将不断更新屏幕,以显示元素的新位置,并在原来的位置隐藏元素,从而营造平滑移动的效果
2.2设置背景色
1 |
|
- 定义一个bg_color,其值为RGB颜色值
- 调用screen.fill(),用背景色填充屏幕
2.3创建设置类
为了方便对设置的内容进行统一管理,可以将所有涉及到设置放到一个单独的模块中,以免在代码中到处添加设置。可以新增如下Settings类
1 |
|
主程序文件中,我们导入Settings类,调用pygame.init(),再创建一个Settings实例,并将其存储在变量ai_settings中。创建屏幕时,使用了ai_settings的属性screen_width和screen_height;接下来填充屏幕时,也使用了ai_settings来访问背景色
2.4添加飞船图像
下面将飞船加入到游戏中。为了在屏幕上绘制玩家的飞船,我们将加载一幅图像,再使用Pygame方法blit()绘制它。
可以使用http://pixabay.com/等网站提供的图形,这些图形无需许可,你可以对其进行修改。
在游戏中几乎可以使用任何类型的图像文件,但使用位图(.bmp)文件最为简单,因为Pygame默认加载位图。
选择图像时,要特别注意其背景色。请尽可能选择背景透明的图像,这样可使用图像编辑器将其背景设置为任何颜色。图像的背景色与游戏的背景色相同时,游戏看起来最漂亮;也可以将游戏的背景色设置成与图像的背景色相同。
2.5创建ship类
选择用于表示飞船的图像后,需要将其显示到屏幕上。我们将创建一个名为ship的模块,其中包含Ship类,它负责管理飞船的大部分行为。
1 |
|
- Ship的方法__init__()接受两个参数:引用self和screen,其中后者指定了要将飞船绘制到什么地方。
- 调用了pygame.image.load()加载图像,这个函数返回一个表示飞船的surface,而我们将这个surface存储到了self.image中。
- 使用get_rect()获取相应surface的属性rect
- 处理rect对象时,可使用矩形四角和中心的x和y坐标。可通过设置这些值来指定矩形的位置
- 要将游戏元素居中,可设置相应rect对象的属性center、centerx或centery。要让游戏元素与屏幕边缘对齐,可使用属性top、bottom、left或right;要调整游戏元素的水平或垂直位置,可使用属性x和y,它们分别是相应矩形左上角的x和y坐标。
- 在Pygame中,原点(0, 0)位于屏幕左上角,向右下方移动时,坐标值将增大。在1200×800的屏幕上,原点位于左上角,而右下角的坐标为(1200, 800)
- 将表示屏幕的矩形存储在self.screen_rect中,再将self.rect.centerx(飞船中心的x坐标)设置为表示屏幕的矩形的属性centerx,并将self.rect.bottom(飞船下边缘的y坐标)设置为表示屏幕的矩形的属性bottom。Pygame将使用这些rect属性来放置飞船图像,使其与屏幕下边缘对齐并水平居中
- 定义方法blitme(),它根据self.rect指定的位置将图像绘制到屏幕上
在屏幕上绘制飞船
在主程序中引入ship类,并调用其方法blitme()
2.6重构模块game_fuctions
为简化既有代码的结构,使其更容易扩展,通过创建模块game_fuctions,避免主模块alien_invasion.py太长。
check_events()
先把管理事件的代码移到一个名为check_events()的函数中,以简化run_game()并隔离事件管理循环。通过隔离事件循环,可将事件管理与游戏的其他方面(如更新屏幕)分离.
1 |
|
这个模块中导入了事件检查循环要使用的sys和pygame。当前,函数check_events()不需要任何形参,其函数体复制了alien_invasion.py的事件循环。
update_screen()
1 |
|
2.7驾驶飞船
下面来让玩家能够左右移动飞船。为此,我们将编写代码,在用户按左或右箭头键时作出响应
响应按键
每当用户按键时,都将在Pygame中注册一个事件。事件都是通过方法pygame.event.get()获取的,因此在函数check_events()中,我们需要指定要检查哪些类型的事件。每次按键都被注册为一个KEYDOWN事件。
检测到KEYDOWN事件时,我们需要检查按下的是否是特定的键。例如,如果按下的是右箭头键,我们就增大飞船的rect.centerx值,将飞船向右移动:
1 |
|
主程序更新调用的check_events()代码,将ship作为实参传递:
1 |
|
现在运行alien_invasion.py,则每按右箭头键一次,飞船都将向右移动1像素
允许不断移动
玩家按住右箭头键不放时,我们希望飞船不断地向右移动,直到玩家松开为止。我们将让游戏检测pygame.KEYUP事件,以便玩家松开右箭头键时我们能够知道这一点;然后,我们将结合使用KEYDOWN和KEYUP事件,以及一个名为moving_right的标志来实现持续移动。
飞船不动时,标志moving_right将为False。玩家按下右箭头键时,我们将这个标志设置为True;而玩家松开时,我们将这个标志重新设置为False。
飞船的属性都由Ship类控制,因此我们将给这个类添加一个名为moving_right的属性和一个名为update()的方法。方法update()检查标志moving_right的状态,如果这个标志为True,就调整飞船的位置。每当需要调整飞船的位置时,我们都调用这个方法。
修改ship类:
1 |
|
修改game_fuctions.py模块代码
1 |
|
修改主程序:
1 |
|
左右移动
再次修改Ship类和函数check_events():
修改game_fuctions.py
调整飞船的速度
可以在Settings类中添加属性ship_speed_factor,用于控制飞船的速度
self.ship_speed_factor = 1.5
通过将速度设置指定为小数值,可在后面加快游戏的节奏时更细致地控制飞船的速度,然而,rect的centerx等属性只能存储整数值,因此我们需要对Ship类做些修改:
限制飞船的活动范围
修改Ship类的方法update(),使的飞船不能超出屏幕范围
1 |
|
重构check_events()
函数check_events()将越来越长,我们将其部分代码放在两个函数中:一个处理KEYDOWN事件,另一个处理KEYUP事件
2.8 射击
在settings中添加子弹的设置:
1 |
|
创建Bullet类
1 |
|
Bullet类继承了我们从模块pygame.sprite中导入的Sprite类。通过使用精灵,可将游戏中相关的元素编组,进而同时操作编组中的所有元素。
定义update()跟draw_bullet()两个方法,用于更新子弹的位置,并在屏幕上绘制出新的子弹。
属性speed_factor让我们能够随着游戏的进行或根据需要提高子弹的速度,以调整游戏的行为。子弹发射后,其x坐标始终不变,因此子弹将沿直线垂直地往上穿行。
将子弹存储到编组中
定义Bullet类和必要的设置后,就可以编写代码了,在玩家每次按空格键时都射出一发子弹。首先,我们将在alien_invasion.py中创建一个编组(group),用于存储所有有效的子弹,以便能够管理发射出去的所有子弹。
这个编组将是pygame.sprite.Group类的一个实例;pygame.sprite. Group类类似于列表,但提供了有助于开发游戏的额外功能。
将bullets传递给了check_events()和update_screen()。在check_events()中,需要在玩家按空格键时处理bullets;而在update_screen()中,需要更新要绘制到屏幕上的bullets。
当你对编组调用update()时,编组将自动对其中的每个精灵调用update(),因此代码行bullets.update()将为编组bullets中的每颗子弹调用bullet.update()。
开火
在game_functions.py中,我们需要修改check_keydown_events(),以便在玩家按空格键时发射一颗子弹。我们无需修改check_keyup_events(),因为玩家松开空格键时什么都不会发生。我们还需修改update_screen(),确保在调用flip()前在屏幕上重绘每颗子弹
1 |
|
编组bulltes传递给了check_keydown_events()。玩家按空格键时,创建一颗新子弹(一个名为new_bullet的Bullet实例),并使用方法add()将其加入到编组bullets中;代码bullets.add(new_bullet)将新子弹存储到编组bullets中。
在check_events()的定义中,我们需要添加形参bullets;调用check_keydown_events()时,我们也需要将bullets作为实参传递给它。
给在屏幕上绘制子弹的 update_screen() 添加了形参 bullets 。方法bullets.sprites()返回一个列表,其中包含编组bullets中的所有精灵。为在屏幕上绘制发射的所有子弹,我们遍历编组bullets中的精灵,并对每个精灵都调用draw_bullet()。
如果此时运行alien_invasion.py,将能够左右移动飞船,并发射任意数量的子弹。子弹在屏幕上向上穿行,抵达屏幕顶部后消失。可在settings.py中修改子弹的尺寸、颜色和速度。
删除消失的子弹
子弹抵达屏幕顶端后消失,这仅仅是因为Pygame无法在屏幕外面绘制它们。这些子弹实际上依然存在,它们的y坐标为负数,且越来越小。这是个问题,因为它们将继续消耗内存和处理能力。
为此,我们需要检测这样的条件,即表示子弹的rect的bottom属性为零,它表明子弹已穿过屏幕顶端:
1 |
|
在for循环中,不应从列表或编组中删除条目,因此必须遍历编组的副本。我们使用了方法copy()来设置for循环,这让我们能够在循环中修改bullets。我们检查每颗子弹,看看它是否已从屏幕顶端消失。如果是这样,就将其从bullets中删除。我们使用了一条print语句,以显示当前还有多少颗子弹,从而核实已消失的子弹确实删除了。
限制子弹的数量
很多射击游戏都对可同时出现在屏幕上的子弹数量进行限制,以鼓励玩家有目标地射击,因此可以修改settings类,并在game_fuctions中新增子弹前检查当前子弹数量是否小于设定值
创建update_bullets()函数
编写并检查子弹管理代码后,可将其移到模块game_functions中,以让主程序文件alien_invasion.py尽可能简单,在game_fuctions.py后面追加一个函数:
1 |
|
创建函数 fire_bullet()
将发射子弹的代码移到一个独立的函数中,这样,在check_keydown_events()中只需使用一行代码来发射子弹,让elif代码块变得非常简单
1 |
|
3.外星人
首先,我们在屏幕上边缘附近添加一个外星人,然后生成一群外星人。我们让这群外星人向两边和下面移动,并删除被子弹击中的外星人。最后,我们将显示玩家拥有的飞船数量,并在玩家的飞船用完后结束游戏。
3.1创建Alien类
1 |
|
除位置不同外,这个类的大部分代码都与Ship类相似。每个外星人最初都位于屏幕左上角附近,我们将每个外星人的左边距都设置为外星人的宽度,并将上边距设置为外星人的高度
创建Alien实例
1 |
|
导入了新创建的Alien类,并在进入主while循环前创建了一个Alien实例。我们没有修改外星人的位置,因此该while循环没有任何新东西,但我们修改了对update_screen()的调用,传递了一个外星人实例
让外星人出现在屏幕上
1 |
|
先绘制飞船和子弹,再绘制外星人,让外星人在屏幕上位于最前面
3.2创建一群外星人
要绘制一群外星人,需要确定一行能容纳多少个外星人以及要绘制多少行外星人。我们将首先计算外星人之间的水平间距,并创建一行外星人,再确定可用的垂直空间,并创建整群外星人
available_space_x = ai_settings.screen_width – (2 * alien_width)
:为确定一行可容纳多少个外星人,我们来看看可用的水平空间有多大。屏幕宽度存储在ai_settings.screen_width中,但需要在屏幕两边都留下一定的边距,把它设置为外星人的宽度。由于有两个边距,因此可用于放置外星人的水平空间为屏幕宽度减去外星人宽度的两倍
number_aliens_x = available_space_x / (2 * alien_width)
:我们还需要在外星人之间留出一定的空间,即外星人宽度。因此,显示一个外星人所需的水平空间为外星人宽度的两倍:一个宽度用于放置外星人,另一个宽度为外星人右边的空白区域。为确定一行可容纳多少个外星人,我们将可用空间除以外星人宽度的两倍
创建多行外星人
为创建一行外星人,首先在alien_invasion.py中创建一个名为aliens的空编组,用于存储全部外星人,再调用game_functions.py中创建外星人群的函数
1 |
|
- 创建了一个空编组,用于存储所有的外星人。
- 调用稍后将编写的函数create_fleet(),并将ai_settings、对象screen和空编组aliens传递给它。
- 修改对update_screen()的调用,让它能够访问外星人编组
创建外星人群
新函数create_fleet(),我们将它放在game_functions. py的末尾。我们还需要导入Alien类,因此务必在文件game_functions.py开头添加相应的import语句
1 |
|
重构create_fleet()
将create_fleet()函数拆分为get_number_aliens_x()和create_alien():
1 |
|
- 函数get_number_aliens_x()的代码都来自create_fleet(),且未做任何修改。
- 函数create_alien()的代码也都来自create_fleet(),且未做任何修改,只是使用刚创建的外星人来获取外星人宽度。
- 我们将计算可用水平空间的代码替换为对get_number_aliens_x()的调用,并删除了引用alien_width的代码行,因为现在这是在create_alien()中处理的。然后调用create_alien()。
添加行
要创建外星人群,需要计算屏幕可容纳多少行,并对创建一行外星人的循环重复相应的次数.
为计算可容纳的行数,我们这样计算可用垂直空间:将屏幕高度减去第一行外星人的上边距(外星人高度)、飞船的高度以及最初外星人群与飞船的距离(外星人高度的两倍):
available_space_y = ai_settings.screen_height – 3 * alien_height – ship_height
每行下方都要留出一定的空白区域,并将其设置为外星人的高度。为计算可容纳的行数,我们将可用垂直空间除以外星人高度的两倍(同样,如果这样的计算不对,我们马上就能发现,继而将间距调整为合理的值)。
number_rows = available_height_y / (2 * alien_height)
1 |
|
3.3让外星人移动
下面来让外星人群在屏幕上向右移动,撞到屏幕边缘后下移一定的距离,再沿相反的方向移动。我们将不断地移动所有的外星人,直到所有外星人都被消灭,有外星人撞上飞船,或有外星人抵达屏幕底端
向右移动外星人
1 |
|
在settings.py类中增加外星人的移动速度ai_settings.alien_speed_factor
在game_fuctions.py中增加函数:
1 |
|
增加移动方向设置
1 |
|
设置fleet_drop_speed指定了有外星人撞到屏幕边缘时,外星人群向下移动的速度。将这个速度与水平速度分开是有好处的,这样你就可以分别调整这两种速度了。
要实现fleet_direction设置,可以将其设置为文本值,如’left’或’right’,但这样就必须编写if-elif语句来检查外星人群的移动方向。鉴于只有两个可能的方向,我们使用值1和-1来表示它们,并在外星人群改变方向时在这两个值之间切换。
另外,鉴于向右移动时需要增大每个外星人的x坐标,而向左移动时需要减小每个外星人的x坐标,使用数字来表示方向更合理.
检查是否撞到屏幕
1 |
|
我们可对任何外星人调用新方法check_edges(),看看它是否位于屏幕左边缘或右边缘。如果外星人的rect的right属性大于或等于屏幕的rect的right属性,就说明外星人位于屏幕右边缘。如果外星人的rect的left属性小于或等于0,就说明外星人位于屏幕左边缘。
修改了方法update(),将移动量设置为外星人速度和fleet_direction的乘积,让外星人向左或向右移。如果fleet_direction为1,就将外星人当前的x坐标增大alien_speed_factor,从而将外星人向右移;如果fleet_direction为,就将外星人当前的x坐标减去alien_speed_factor,从而将外星人向左移。
向下移动
有外星人到达屏幕边缘时,需要将整群外星人下移,并改变它们的移动方向。需要对game_functions.py做重大修改,因为我们要在这里检查是否有外星人到达了左边缘或右边缘。为此,我们编写函数check_fleet_edges()和change_fleet_direction(),并对update_aliens()进行修改:
1 |
|
在check_fleet_edges()中,我们遍历外星人群,并对其中的每个外星人调用check_edges()。如果check_edges()返回True,我们就知道相应的外星人位于屏幕边缘,需要改变外星人群的方向,因此我们调用change_fleet_direction()并退出循环。在change_fleet_direction()中,我们遍历所有外星人,将每个外星人下移fleet_drop_speed设置的值;然后,将fleet_direction的值修改为其当前值与-1的乘积。
我们修改了函数update_aliens(),在其中通过调用check_fleet_edges()来确定是否有外星人位于屏幕边缘。现在,函数update_aliens()包含形参ai_settings,因此我们调用它时指定了与ai_settings对应的实参。
3.4射杀外星人
游戏编程中,碰撞指的是游戏元素重叠在一起。要让子弹能够击落外星人,我们将使用sprite.groupcollide()检测两个编组的成员之间的碰撞。
检测子弹与外星人的碰撞
方法sprite.groupcollide()将每颗子弹的rect同每个外星人的rect进行比较,并返回一个字典,其中包含发生了碰撞的子弹和外星人。在这个字典中,每个键都是一颗子弹,而相应的值都是被击中的外星人
1 |
|
新增的这行代码遍历编组bullets中的每颗子弹,再遍历编组aliens中的每个外星人。每当有子弹和外星人的rect重叠时,groupcollide()就在它返回的字典中添加一个键-值对。两个实参True告诉Pygame删除发生碰撞的子弹和外星人(要模拟能够穿行到屏幕顶端的高能子弹——消灭它击中的每个外星人,可将第一个布尔实参设置为False,并让第二个布尔实参为True。这样被击中的外星人将消失,但所有的子弹都始终有效,直到抵达屏幕顶端后消失。)
为测试创建大子弹
测试有些功能时,可以修改游戏的某些设置,以便专注于游戏的特定方面。例如,可以缩小屏幕以减少需要击落的外星人数量,也可以提高子弹的速度,以便能够在单位时间内发射大量子弹。
生成新的外星人群
要在外星人群被消灭后又显示一群外星人,首先需要检查编组aliens是否为空。如果为空,就调用create_fleet()。我们将在update_bullets()中执行这种检查,因为外星人都是在这里被消灭的。
1 |
|
提高子弹速度
如果你现在尝试在这个游戏中射杀外星人,可能发现子弹的速度比以前慢,这是因为在每次循环中, Pygame 需要做的工作更多了。为提高子弹的速度,可调整 settings.py 中bullet_speed_factor的值。
重构update_bullets()
下面来重构update_bullets(),使其不再完成那么多任务。我们将把处理子弹和外星人碰撞的代码移到一个独立的函数中
3.5结束游戏
检测外星人与飞船的碰撞
我们首先检查外星人和飞船之间的碰撞,以便外星人撞上飞船时我们能够作出合适的响应。我们在更新每个外星人的位置后立即检测外星人和飞船之间的碰撞。
1 |
|
方法spritecollideany()接受两个实参:一个精灵和一个编组。它检查编组是否有成员与精灵发生了碰撞,并在找到与精灵发生了碰撞的成员后就停止遍历编组。在这里,它遍历编组aliens,并返回它找到的第一个与飞船发生了碰撞的外星人。
如果没有发生碰撞,spritecollideany()将返回None,因此Ø处的if代码块不会执行。如果找到了与飞船发生碰撞的外星人,它就返回这个外星人,因此if代码块将执行:打印“Ship hit!!!”。(有外星人撞到飞船时,需要执行的任务很多:需要删除余下的所有外星人和子弹,让飞船重新居中,以及创建一群新的外星人。
响应外星人与飞船的碰撞
现在需要确定外星人与飞船发生碰撞时,该做些什么。我们不销毁ship实例并创建一个新的ship实例,而是通过跟踪游戏的统计信息来记录飞船被撞了多少次(跟踪统计信息还有助于记分)。
下面来编写一个用于跟踪游戏统计信息的新类——GameStats,并将其保存为文件game_stats.py:
1 |
|
在这个游戏运行期间,我们只创建一个GameStats实例,但每当玩家开始新游戏时,需要重置一些统计信息。为此,我们在方法reset_stats()中初始化大部分统计信息,而不是在__init__()中直接初始化它们。我们在__init__()中调用这个方法,这样创建GameStats实例时将妥善地设置这些统计信息,同时在玩家开始新游戏时也能调用reset_stats()。
当前只有一项统计信息——ships_left,其值在游戏运行期间将不断变化。一开始玩家拥有的飞船数存储在settings.py的ship_limit中.
我们还需对alien_invasion.py做些修改,以创建一个GameStats实例:
我们导入了新类GameStats,创建了一个名为stats的实例,再调用update_aliens()并添加了实参stats、screen和ship。在有外星人撞到飞船时,我们将使用这些实参来跟踪玩家还有多少艘飞船,以及创建一群新的外星人.
1 |
|
我们首先从模块time中导入了函数sleep(),以便使用它来让游戏暂停。新函数ship_hit()在飞船被外星人撞到时作出响应。在这个函数内部,将余下的飞船数减1,然后清空编组aliens和bullets。
接下来,我们创建一群新的外星人,并将飞船居中,稍后将在Ship类中添加方法center_ship()。最后,我们更新所有元素后(但在将修改显示到屏幕前)暂停,让玩家知道其飞船被撞到了。屏幕将暂时停止变化,让玩家能够看到外星人撞到了飞船。函数sleep()执行完毕后,将接着执行函数update_screen().
更新了update_aliens()的定义,使其包含形参stats、screen和bullets,让它能够在调用ship_hit()时传递这些值.
有外星人到达屏幕底端
如果有外星人到达屏幕底端,我们将像有外星人撞到飞船那样作出响应。
1 |
|
函数check_aliens_bottom()检查是否有外星人到达了屏幕底端。到达屏幕底端后,外星人的属性rect.bottom的值大于或等于屏幕的属性rect.bottom的值。如果有外星人到达屏幕底端,我们就调用ship_hit();只要检测到一个外星人到达屏幕底端,就无需检查其他外星人,因此我们在调用ship_hit()后退出循环。
我们在更新所有外星人的位置并检测是否有外星人和飞船发生碰撞后调用check_aliens_bottom()
结束游戏
现在这个游戏看起来更完整了,但它永远都不会结束,只是ships_left不断变成更小的负数。
下面在GameStats中添加一个作为标志的属性game_active,以便在玩家的飞船用完后结束游戏.
1 |
|
现在在ship_hit()中添加代码,在玩家的飞船都用完后将game_active设置为False
1 |
|
确定应运行游戏的哪个部分
在alien_invasion.py中,我们需要确定游戏的哪些部分在任何情况下都应运行,哪些部分仅在游戏处于活动状态时才运行
1 |
|
4.记分系统
4.1添加PLAY按钮
将添加一个Play按钮,它在游戏开始前出现,并在游戏结束后再次出现,让玩家能够开始新游戏
下面让游戏一开始处于非活动状态,并提示玩家单击Play按钮来开始游戏。为此,在game_stats.py中将self.game_active = False
创建button类
由于Pygame没有内置创建按钮的方法,我们创建一个Button类,用于创建带标签的实心矩形
1 |
|
首先,我们导入了模块pygame.font,它让Pygame能够将文本渲染到屏幕上。方法__init__()接受参数self,对象ai_settings和screen,以及msg,其中msg是要在按钮中显示的文本。
我们设置按钮的尺寸,然后通过设置button_color让按钮的rect对象为亮绿色,并通过设置text_color让文本为白色。
指定使用什么字体来渲染文本。实参None让Pygame使用默认字体,而48指定了文本的字号。
为让按钮在屏幕上居中,我们创建一个表示按钮的rect对象,并将其center属性设置为屏幕的center属性。
Pygame通过将你要显示的字符串渲染为图像来处理文本。调用prep_msg()来处理这样的渲染。
1 |
|
调用screen.fill()来绘制表示按钮的矩形,再调用screen.blit(),并向它传递一幅图像以及与该图像相关联的rect对象,从而在屏幕上绘制文本图像
在屏幕上绘制按钮
使用Button类来创建一个Play按钮。鉴于只需要一个Play按钮,我们直接在alien_invasion.py中创建它
1 |
|
开始游戏
为在玩家单击Play按钮时开始新游戏,需在game_functions.py中添加如下代码,以监视与这个按钮相关的鼠标事件
1 |
|
重置游戏
为在玩家每次单击Play按钮时都重置游戏,需要重置统计信息、删除现有的外星人和子弹、创建一群新的外星人,并让飞船居中,如下所示:
1 |
|
将 Play 按钮切换到非活动状态
当前,Play按钮存在一个问题,那就是即便Play按钮不可见,玩家单击其原来所在的区域时,游戏依然会作出响应。游戏开始后,如果玩家不小心单击了Play按钮原来所处的区域,游戏将重新开始!
为修复这个问题,可让游戏仅在game_active为False时才开始
1 |
|
标志button_clicked的值为True或False,仅当玩家单击了Play按钮且游戏当前处于非活动状态时,游戏才重新开始。
隐藏光标
为让玩家能够开始游戏,我们要让光标可见,但游戏开始后,光标只会添乱。为修复这种问题,我们在游戏处于活动状态时让光标不可见:
1 |
|
通过向set_visible()传递False,让Pygame在光标位于游戏窗口内时将其隐藏起来
4.2提高等级
当前,将整群外星人都消灭干净后,玩家将提高一个等级,但游戏的难度并没有变。下面来增加一点趣味性:每当玩家将屏幕上的外星人都消灭干净后,加快游戏的节奏,让游戏玩起来更难
修改速度设置
先重新组织Settings类,将游戏设置划分成静态的和动态的两组。对于随着游戏进行而变化的设置,我们还确保它们在开始新游戏时被重置
1 |
|
我们添加了设置speedup_scale,用于控制游戏节奏的加快速度:2表示玩家每提高一个等级,游戏的节奏就翻倍;1表示游戏节奏始终不变。将其设置为1.1能够将游戏节奏提高到够快,让游戏既有难度,又并非不可完成。最后,我们调用initialize_dynamic_settings(),以初始化随游戏进行而变化的属性.
1 |
|
这个方法设置了飞船、子弹和外星人的初始速度。随游戏的进行,我们将提高这些速度,而每当玩家开始新游戏时,都将重置这些速度。在这个方法中,我们还设置了fleet_direction,使得游戏刚开始时,外星人总是向右移动。每当玩家提高一个等级时,我们都使用increase_speed()来提高飞船、子弹和外星人的速度
1 |
|
为提高这些游戏元素的速度,我们将每个速度设置都乘以speedup_scale的值。
在check_bullet_alien_collisions()中,我们在整群外星人都被消灭后调用increase_speed()来加快游戏的节奏,再创建一群新的外星人
重置速度
每当玩家开始新游戏时,我们都需要将发生了变化的设置重置为初始值,否则新游戏开始时,速度设置将是前一次游戏增加了的值
4.3记分
下面来实现一个记分系统,以实时地跟踪玩家的得分,并显示最高得分、当前等级和余下的飞船数。
得分是游戏的一项统计信息,因此我们在GameStats中添加一个score属性
1 |
|
为在每次开始游戏时都重置得分,我们在reset_stats()而不是__init__()中初始化score
显示得分
为在屏幕上显示得分,我们首先创建一个新类Scoreboard。就当前而言,这个类只显示当前得分,但后面我们也将使用它来显示最高得分、等级和余下的飞船数
1 |
|
由于Scoreboard在屏幕上显示文本,因此我们首先导入模块pygame.font。接下来,我们在__init__()中包含形参ai_settings、screen和stats,让它能够报告我们跟踪的值。然后,我们设置文本颜色并实例化一个字体对象。为将要显示的文本转换为图像,我们调用了prep_score()
1 |
|
在prep_score()中,我们首先将数字值stats.score转换为字符串(见),再将这个字符串传递给创建图像的render()。为在屏幕上清晰地显示得分,我们向render()传递了屏幕背景色,以及文本颜色。
我们将得分放在屏幕右上角,并在得分增大导致这个数字更宽时让它向左延伸。为确保得分始终锚定在屏幕右边,我们创建了一个名为score_rect的rect,让其右边缘与屏幕右边缘相距20像素,并让其上边缘与屏幕上边缘也相距20像素。
最后,我们创建方法show_score(),用于显示渲染好的得分图像。
1 |
|
创建记分牌
1 |
|
为显示得分,将update_screen()修改成下面这样:
在外星人被消灭时更新得分
为在屏幕上实时地显示得分,每当有外星人被击中时,我们都更新stats.score的值,再调用prep_score()更新得分图像。但在此之前,我们需要指定玩家每击落一个外星人都将得到多少个点self.alien_points = 50
在check_bullet_alien_collisions()中,每当有外星人被击落时,都更新得分:
1 |
|
更新check_bullet_alien_collisions()的定义,在其中包含了形参stats和sb,让它能够更新得分和记分牌。有子弹撞到外星人时,Pygame返回一个字典(collisions)。我们检查这个字典是否存在,如果存在,就将得分加上一个外星人值的点数。接下来,我们调用prep_score()来创建一幅显示最新得分的新图像
将消灭的每个外星人的点数都计入得分
当前,我们的代码可能遗漏了一些被消灭的外星人。例如,如果在一次循环中有两颗子弹射中了外星人,或者因子弹更宽而同时击中了多个外星人,玩家将只能得到一个被消灭的外星人的点数。为修复这种问题,我们来调整检测子弹和外星人碰撞的方式。
在check_bullet_alien_collisions()中,与外星人碰撞的子弹都是字典collisions中的一个键;而与每颗子弹相关的值都是一个列表,其中包含该子弹撞到的外星人。我们遍历字典collisions,确保将消灭的每个外星人的点数都记入得分
1 |
|
提高点数
玩家每提高一个等级,游戏都变得更难,因此处于较高的等级时,外星人的点数应更高。
我们定义了点数提高的速度,并称之为score_scale。很小的节奏加快速度(1.1)让游戏很快就变得极具挑战性,但为让记分发生显著的变化,需要将点数的提高速度设置为更大的值(1.5)。现在,我们在加快游戏节奏的同时,提高了每个外星人的点数。为让点数为整数,我们使用了函数int()。
将得分整圆
大多数街机风格的射击游戏都将得分显示为10的整数倍,下面让我们的记分系统遵循这个原则。我们还将设置得分的格式,在大数字中添加用逗号表示的千位分隔符。我们在Scoreboard中修改成这样:
1 |
|
函数round()通常让小数精确到小数点后多少位,其中小数位数是由第二个实参指定的。然而,如果将第二个实参指定为负数,round()将圆整到最近的10、100、1000等整数倍。让Python将stats.score的值圆整到最近的10的整数倍,并将结果存储到rounded_score中.
使用了一个字符串格式设置指令,它让Python将数值转换为字符串时在其中插入逗号,例如,输出1,000,000而不是1000000。如果你现在运行这个游戏,看到的将是10的整数倍的整洁得分
最高得分
每个玩家都想超过游戏的最高得分记录。下面来跟踪并显示最高得分,给玩家提供要超越的目标。我们将最高得分存储在GameStats中
1 |
|
下面来修改Scoreboard以显示最高得分。先来修改方法_init_():
self.prep_high_score()
1 |
|
我们将最高得分圆整到最近的10的整数倍,并添加了用逗号表示的千分位分隔符。然后,我们根据最高得分生成一幅图像,使其水平居中,并将其top属性设置为当前得分图像的top属性。
1 |
|
为检查是否诞生了新的最高得分,我们在game_functions.py中添加一个新函数check_high_score():
1 |
|
函数check_high_score()包含两个形参:stats和sb。它使用stats来比较当前得分和最高得分,并在必要时使用sb来修改最高得分图像.
在check_bullet_alien_collisions()中,每当有外星人被消灭,都需要在更新得分后调用check_high_score()
显示等级
为在游戏中显示玩家的等级,首先需要在GameStats中添加一个表示当前等级的属性。为确保每次开始新游戏时都重置等级,在reset_stats()中初始化它
1 |
|
为让Scoreboard能够在当前得分下方显示当前等级,我们在__init__()中调用了一个新方法prep_level()
1 |
|
方法prep_level()根据存储在stats.level中的值创建一幅图像,并将其right属性设置为得分的right属性。然后,将top属性设置为比得分图像的bottom属性大10像素,以便在得分和等级之间留出一定的空间。
修改show_score()函数:
1 |
|
在check_bullet_alien_collisions()中提高等级,并更新等级图像
1 |
|
如果整群外星人都被消灭,我们就将stats.level的值加1,并调用prep_level(),以确保正确地显示新等级.
为确保开始新游戏时更新记分和等级图像,在按钮Play被单击时触发重置
1 |
|
check_play_button()的定义需要包含对象sb。为重置记分牌图像,我们在重置相关游戏设置后调用prep_score()、prep_high_score()和prep_level(),在check_events()中,现在需要向check_play_button()传递sb,让它能够访问记分牌对象.
显示余下的飞船
最后,我们来显示玩家还有多少艘飞船,但使用图形而不是数字。为此,我们在屏幕左上角绘制飞船图像来指出还余下多少艘飞船,就像众多经典的街机游戏那样
需要让Ship继承Sprite,以便能够创建飞船编组
1 |
|
导入了Sprite,让Ship继承Sprite (见),并在__init__()的开头就调用了super()
修改Scoreboard,在其中创建一个可供显示的飞船编组。下面是其中的import语句和方法__init__():
鉴于要创建一个飞船编组,我们导入Group和Ship类。调用prep_level()后,我们调用了prep_ships()。
1 |
|
方法prep_ships()创建一个空编组self.ships,用于存储飞船实例。为填充这个编组,根据玩家还有多少艘飞船运行一个循环相应的次数。在这个循环中,我们创建一艘新飞船,并设置其x坐标,让整个飞船编组都位于屏幕左边,且每艘飞船的左边距都为10像素。
我们还将y坐标设置为离屏幕上边缘10像素,让所有飞船都与得分图像对齐。最后,我们将每艘新飞船都添加到编组ships中
我们还在飞船被外星人撞到时调用prep_ships(),从而在玩家损失一艘飞船时更新飞船图像
首先,我们在update_aliens()的定义中添加了形参sb。然后,我们向ship_hit()和check_aliens_bottom()都传递了sb,让它们都能够访问记分牌对象。
接下来,我们更新了ship_hit()的定义,使其包含形参sb。我们在将ships_left的值减1后调用了prep_ships(),这样每次损失了飞船时,显示的飞船数都是正确的。
在check_aliens_bottom()中需要调用ship_hit(),因此对这个函数进行更新
现在,check_aliens_bottom()包含形参sb,并在调用ship_hit()时传递了实参sb。
最后,在alien_invasion.py中修改调用update_aliens()的代码,向它传递实参sb