—snip—

def create_fleet(ai_settings, screen, aliens):

"""创建外星人群"""

创建一个外星人,并计算一行可容纳多少个外星人

外星人间距为外星人宽度

❶ alien = Alien(ai_settings, screen)

❷ alien_width = alien.rect.width

❸ available_space_x = ai_settings.screen_width - 2 * alien_width

❹ number_aliens_x = int(available_space_x / (2 * alien_width))

创建第一行外星人

❺ for alien_number in range(number_aliens_x):

创建一个外星人并将其加入当前行

❻ alien = Alien(ai_settings, screen)

06 - 图1

alien.x = alien_width + 2 alien_width alien_number alien.rect.x = alien.x

aliens.add(alien)

这些代码大都在前面详细介绍过。为放置外星人,我们需要知道外星人的宽度和高度,因此在执行计算前,我们先创建一个外星人(见❶)。这个外星人不是外星人群的成员,

因此没有将它加入到编组aliens 中。在❷处,我们从外星人的rect 属性中获取外星人宽度,并将这个值存储到alien_width 中,以免反复访问属性rect 。在❸处,我们计

算可用于放置外星人的水平空间,以及其中可容纳多少个外星人。

相比于前面介绍的工作,这里唯一的不同是使用了int() 来确保计算得到的外星人数量为整数(见❹),因为我们不希望某个外星人只显示一部分,而且函数range() 也需要

一个整数。函数int() 将小数部分丢弃,相当于向下圆整(这大有裨益,因为我们宁愿每行都多出一点点空间,也不希望每行的外星人之间过于拥挤)。

接下来,我们编写了一个循环,它从零数到要创建的外星人数(见❺)。在这个循环的主体中,我们创建一个新的外星人,并通过设置 x 坐标将其加入当前行(见❻)。将每个

外星人都往右推一个外星人的宽度。接下来,我们将外星人宽度乘以2,得到每个外星人占据的空间(其中包括其右边的空白区域),再据此计算当前外星人在当前行的位置。最

后,我们将每个新创建的外星人都添加到编组aliens 中。

如果你现在运行这个游戏,将看到第一行外星人,如图13-3所示。

图13-3  第一行外星人

这行外星人在屏幕上稍微偏向了左边,这实际上是有好处的,因为我们将让外星人群往右移,触及屏幕边缘后稍微往下移,然后往左移,以此类推。就像经典游戏《太空入侵

者》,相比于只往下移,这种移动方式更有趣。我们将让外形人群不断这样移动,直到所有外星人都被击落或有外星人撞上飞船或抵达屏幕底端。

注意  根据你选择的屏幕宽度,在你的系统中,第一行外星人的位置可能稍有不同。

13.3.4  重构create_fleet()

倘若我们创建了外星人群,也许应该让create_fleet() 保持原样,但鉴于创建外星人的工作还未完成,我们稍微清理一下这个函数。下面是create_fleet() 和两个新函

数,get_number_aliens_x() 和create_alien() :

game_functions.py

❶ def get_number_aliens_x(ai_settings, alien_width):

"""计算每行可容纳多少个外星人"""

available_space_x = ai_settings.screen_width - 2 * alien_width

number_aliens_x = int(available_space_x / (2 * alien_width))

return number_aliens_x

def create_alien(ai_settings, screen, aliens, alien_number):

"""创建一个外星人并将其放在当前行"""

alien = Alien(ai_settings, screen)

❷ alien_width = alien.rect.width

alien.x = alien_width + 2 alien_width alien_number alien.rect.x = alien.x

aliens.add(alien)

def create_fleet(ai_settings, screen, aliens):

"""创建外星人群"""

创建一个外星人,并计算每行可容纳多少个外星人

alien = Alien(ai_settings, screen)

❸ number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width) # 创建第一行外星人

for alien_number in range(number_aliens_x):

❹ create_alien(ai_settings, screen, aliens, alien_number)

函数get_number_aliens_x() 的代码都来自create_fleet() ,且未做任何修改(见❶)。函数create_alien() 的代码也都来自create_fleet() ,且未做任何修

改,只是使用刚创建的外星人来获取外星人宽度(见❷)。在❸处,我们将计算可用水平空间的代码替换为对get_number_aliens_x() 的调用,并删除了引

用alien_width 的代码行,因为现在这是在create_alien() 中处理的。在❹处,我们调用create_alien() 。通过这样的重构,添加新行进而创建整群外星人将更容

易。

13.3.5  添加行

要创建外星人群,需要计算屏幕可容纳多少行,并对创建一行外星人的循环重复相应的次数。为计算可容纳的行数,我们这样计算可用垂直空间:将屏幕高度减去第一行外星人

的上边距(外星人高度)、飞船的高度以及最初外星人群与飞船的距离(外星人高度的两倍):

available_space_y = ai_settings.screen_height – 3 * alien_height – ship_height 06 - 图2

这将在飞船上方留出一定的空白区域,给玩家留出射杀外星人的时间。

每行下方都要留出一定的空白区域,并将其设置为外星人的高度。为计算可容纳的行数,我们将可用垂直空间除以外星人高度的两倍(同样,如果这样的计算不对,我们马上就

能发现,继而将间距调整为合理的值)。

number_rows = available_height_y / (2 * alien_height)

知道可容纳多少行后,便可重复执行创建一行外星人的代码:

game_functions.py

❶ def get_number_rows(ai_settings, ship_height, alien_height):

"""计算屏幕可容纳多少行外星人"""

❷ available_space_y = (ai_settings.screen_height -

(3 * alien_height) - ship_height)

number_rows = int(available_space_y / (2 * alien_height))

return number_rows

def create_alien(ai_settings, screen, aliens, alien_number, row_number): —snip—

alien.x = alien_width + 2 alien_width alien_number alien.rect.x = alien.x

❸ alien.rect.y = alien.rect.height + 2 alien.rect.height row_number aliens.add(alien) def create_fleet(ai_settings, screen, ship, aliens):

—snip—

number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)

number_rows = get_number_rows(ai_settings, ship.rect.height,

alien.rect.height)

创建外星人群

❹ for row_number in range(number_rows):

for alien_number in range(number_aliens_x):

create_alien(ai_settings, screen, aliens, alien_number,

row_number)

为计算屏幕可容纳多少行外星人,我们在函数get_number_rows() 中实现了前面计算available_space_y 和number_rows 的公式(见❶),这个函数

与get_number_aliens_x() 类似。计算公式用括号括起来了,这样可将代码分成两行,以遵循每行不超过79字符的建议(见❷)。这里使用了int() ,因为我们不想创建

不完整的外星人行。

为创建多行,我们使用两个嵌套在一起的循环:一个外部循环和一个内部循环(见❸)。其中的内部循环创建一行外星人,而外部循环从零数到要创建的外星人行数。Python将重

复执行创建单行外星人的代码,重复次数为number_rows 。

为嵌套循环,我们编写了一个新的for 循环,并缩进了要重复执行的代码。(在大多数文本编辑器中,缩进代码块和取消缩进都很容易,详情请参阅附录B。)我们调

用create_alien() 时,传递了一个表示行号的实参,将每行都沿屏幕依次向下放置。

create_alien() 的定义需要一个用于存储行号的形参。在create_alien() 中,我们修改外星人的 y 坐标(见❹),并在第一行外星人上方留出与外星人等高的空白区

域。相邻外星人行的 y 坐标相差外星人高度的两倍,因此我们将外星人高度乘以2,再乘以行号。第一行的行号为0,因此第一行的垂直位置不变,而其他行都沿屏幕依次向下放

置。

在create_fleet() 的定义中,还新增了一个用于存储ship 对象的形参,因此在alien_invasion.py中调用create_fleet() 时,需要传递实参ship :

alien_invasion.py

创建外星人群

gf.create_fleet(ai_settings, screen, ship, aliens)

如果你现在运行这个游戏,将看到一群外星人,如图13-4所示。

图13-4  整群外星人都现身了

在下一节,我们将让外星人群动起来!

动手试一试

13-1 星星 :找一幅星星图像,并在屏幕上显示一系列整齐排列的星星。

13-2 更逼真的星星 :为让星星的分布更逼真,可随机地放置星星。本书前面说过,可像下面这样来生成随机数:

from random import randint

random_number = randint(-10,10)

上述代码返回一个-10和10之间的随机整数。在为完成练习13-1而编写的程序中,随机地调整每颗星星的位置。

13.4  让外星人群移动

下面来让外星人群在屏幕上向右移动,撞到屏幕边缘后下移一定的距离,再沿相反的方向移动。我们将不断地移动所有的外星人,直到所有外星人都被消灭,有外星人撞上飞

船,或有外星人抵达屏幕底端。下面先来让外星人向右移动。

13.4.1  向右移动外星人

为移动外星人,我们将使用alien.py中的方法update() ,且对外星人群中的每个外星人都调用它。首先,添加一个控制外星人速度的设置:

settings.py

def init(self):

—snip—

外星人设置

self.alien_speed_factor = 1

然后,使用这个设置来实现update() :

alien.py

def update(self):

"""向右移动外星人"""

❶ self.x += self.ai_settings.alien_speed_factor

❷ self.rect.x = self.x

每次更新外星人位置时,都将它向右移动,移动量为alien_speed_factor 的值。我们使用属性self.x 跟踪每个外星人的准确位置,这个属性可存储小数值(见❶)。然

后,我们使用self.x 的值来更新外星人的rect 的位置(见❷)。

在主while 循环中已调用了更新飞船和子弹的方法,但现在还需更新每个外星人的位置:

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, ship, bullets)

ship.update()

gf.update_bullets(bullets)

gf.update_aliens(aliens)

gf.update_screen(ai_settings, screen, ship, aliens, bullets)

我们在更新子弹后再更新外星人的位置,因为稍后要检查是否有子弹撞到了外星人。

最后,在文件game_functions.py末尾添加新函数update_aliens() :

game_functions.py

def update_aliens(aliens):

"""更新外星人群中所有外星人的位置"""

aliens.update()

我们对编组aliens 调用方法update() ,这将自动对每个外星人调用方法update() 。如果你现在运行这个游戏,会看到外星人群向右移,并逐渐在屏幕右边缘消失。

13.4.2  创建表示外星人移动方向的设置

下面来创建让外星人撞到屏幕右边缘后向下移动、再向左移动的设置。实现这种行为的代码如下:

settings.py

外星人设置

self.alien_speed_factor = 1

self.fleet_drop_speed = 10

fleet_direction为1表示向右移,为-1表示向左移

self.fleet_direction = 1

设置fleet_drop_speed 指定了有外星人撞到屏幕边缘时,外星人群向下移动的速度。将这个速度与水平速度分开是有好处的,这样你就可以分别调整这两种速度了。

要实现fleet_direction 设置,可以将其设置为文本值,如'left' 或'right' ,但这样就必须编写if-elif 语句来检查外星人群的移动方向。鉴于只有两个可能的方

向,我们使用值1和-1来表示它们,并在外星人群改变方向时在这两个值之间切换。另外,鉴于向右移动时需要增大每个外星人的 x 坐标,而向左移动时需要减小每个外星人的 x 坐标,使用数字来表示方向更合理。

13.4.3  检查外星人是否撞到了屏幕边缘

现在需要编写一个方法来检查是否有外星人撞到了屏幕边缘,还需修改update() ,以让每个外星人都沿正确的方向移动:

alien.py

def check_edges(self):

"""如果外星人位于屏幕边缘,就返回True"""

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.ai_settings.alien_speed_factor *

self.ai_settings.fleet_direction)

self.rect.x = self.x

我们可对任何外星人调用新方法check_edges() ,看看它是否位于屏幕左边缘或右边缘。如果外星人的rect 的right 属性大于或等于屏幕的rect 的right 属性,就说明外

星人位于屏幕右边缘(见❶)。如果外星人的rect 的left 属性小于或等于0,就说明外星人位于屏幕左边缘(见❷)。

我们修改了方法update() ,将移动量设置为外星人速度和fleet_direction 的乘积,让外星人向左或向右移。如果fleet_direction 为1,就将外星人当前的 x 坐标增

大alien_speed_factor ,从而将外星人向右移;如果fleet_direction 为-1,就将外星人当前的 x 坐标减去alien_speed_factor ,从而将外星人向左移。

13.4.4  向下移动外星人群并改变移动方向

有外星人到达屏幕边缘时,需要将整群外星人下移,并改变它们的移动方向。我们需要对game_functions.py做重大修改,因为我们要在这里检查是否有外星人到达了左边缘或右边

缘。为此,我们编写函数check_fleet_edges() 和change_fleet_direction() ,并对update_aliens() 进行修改:

game_functions.py

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

def update_aliens(ai_settings, aliens):

"""

检查是否有外星人位于屏幕边缘,并更新整群外星人的位置

"""

❸ check_fleet_edges(ai_settings, aliens)

aliens.update()

在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 对应的实参:

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, ship, bullets)

ship.update()

gf.update_bullets(bullets)

gf.update_aliens(ai_settings, aliens)

gf.update_screen(ai_settings, screen, ship, aliens, bullets)

如果你现在运行这个游戏,外星人群将在屏幕上来回移动,并在抵达屏幕边缘后向下移动。现在可以开始射杀外星人,检查是否有外星人撞到飞船,或抵达了屏幕底端。

动手试一试

13-3 雨滴 :寻找一幅雨滴图像,并创建一系列整齐排列的雨滴。让这些雨滴往下落,直到到达屏幕底端后消失。

13-4 连绵细雨 :修改为完成练习13-3而编写的代码,使得一行雨滴消失在屏幕底端后,屏幕顶端又出现一行新雨滴,并开始往下落。

13.5  射杀外星人

我们创建了飞船和外星人群,但子弹击中外星人时,将穿过外星人,因为我们还没有检查碰撞。在游戏编程中,碰撞指的是游戏元素重叠在一起。要让子弹能够击落外星人,我

们将使用sprite.groupcollide() 检测两个编组的成员之间的碰撞。

13.5.1  检测子弹与外星人的碰撞

子弹击中外星人时,我们要马上知道,以便碰撞发生后让外星人立即消失。为此,我们将在更新子弹的位置后立即检测碰撞。

方法sprite.groupcollide() 将每颗子弹的rect 同每个外星人的rect 进行比较,并返回一个字典,其中包含发生了碰撞的子弹和外星人。在这个字典中,每个键都是一

颗子弹,而相应的值都是被击中的外星人(第14章实现记分系统时,也会用到这个字典)。

在函数update_bullets() 中,使用下面的代码来检查碰撞:

06 - 图3

game_functions.py

def update_bullets(aliens, bullets):

"""更新子弹的位置,并删除已消失的子弹"""

—snip—

检查是否有子弹击中了外星人

如果是这样,就删除相应的子弹和外星人

collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)

新增的这行代码遍历编组bullets 中的每颗子弹,再遍历编组aliens 中的每个外星人。每当有子弹和外星人的rect 重叠时,groupcollide() 就在它返回的字典中添加一

个键-值对。两个实参True 告诉Pygame删除发生碰撞的子弹和外星人。(要模拟能够穿行到屏幕顶端的高能子弹——消灭它击中的每个外星人,可将第一个布尔实参设置

为False ,并让第二个布尔实参为True 。这样被击中的外星人将消失,但所有的子弹都始终有效,直到抵达屏幕顶端后消失。)

我们调用update_bullets() 时,传递了实参aliens :

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, ship, bullets)

ship.update()

gf.update_bullets(aliens, bullets)

gf.update_aliens(ai_settings, aliens)

gf.update_screen(ai_settings, screen, ship, aliens, bullets)

如果你此时运行这个游戏,被击中的外星人将消失。如图13-5所示,其中有一部分外星人被击落。

图13-5  可以射杀外星人了

13.5.2  为测试创建大子弹

只需通过运行这个游戏就可以测试其很多功能,但有些功能在正常情况下测试起来比较烦琐。例如,要测试代码能否正确地处理外星人编组为空的情形,需要花很长时间将屏幕

上的外星人都击落。

测试有些功能时,可以修改游戏的某些设置,以便专注于游戏的特定方面。例如,可以缩小屏幕以减少需要击落的外星人数量,也可以提高子弹的速度,以便能够在单位时间内

发射大量子弹。

测试这个游戏时,我喜欢做的一项修改是增大子弹的尺寸,使其在击中外星人后依然有效,如图13-6所示。请尝试将bullet_width 设置为300,看看将所有外星人都射杀有多

快!

类似这样的修改可提高测试效率,还可能激发出如何赋予玩家更大威力的思想火花。(完成测试后,别忘了将设置恢复正常。)

06 - 图4

图13-6  威力更大的子弹让游戏的有些方法测试起来更容易

13.5.3  生成新的外星人群

这个游戏的一个重要特点是外星人无穷无尽,一个外星人群被消灭后,又会出现一群外星人。

要在外星人群被消灭后又显示一群外星人,首先需要检查编组aliens 是否为空。如果为空,就调用create_fleet() 。我们将在update_bullets() 中执行这种检查,因

为外星人都是在这里被消灭的:

game_functions.py

def update_bullets(ai_settings, screen, ship, aliens, bullets):

—snip—

检查是否有子弹击中了外星人

如果是,就删除相应的子弹和外星人

collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)

❶ if len(aliens) == 0:

删除现有的子弹并新建一群外星人

❷ bullets.empty()

create_fleet(ai_settings, screen, ship, aliens)

在❶处,我们检查编组aliens 是否为空。如果是,就使用方法empty() 删除编组中余下的所有精灵,从而删除现有的所有子弹。我们还调用了create_fleet() ,再次在屏

幕上显示一群外星人。

现在,update_bullets() 的定义包含额外的形参ai_settings 、screen 和ship ,因此我们需要更新alien_invasion.py中对update_bullets() 的调用:

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, ship, bullets)

ship.update()

gf.update_bullets(ai_settings, screen, ship, aliens, bullets)

gf.update_aliens(ai_settings, aliens)

gf.update_screen(ai_settings, screen, ship, aliens, bullets)

现在,当前外星人群消灭干净后,将立刻出现一个新的外星人群。

13.5.4  提高子弹的速度

如果你现在尝试在这个游戏中射杀外星人,可能发现子弹的速度比以前慢,这是因为在每次循环中,Pygame需要做的工作更多了。为提高子弹的速度,可调整settings.py 中bullet_speed_factor 的值。例如,如果将这个值增大到3,子弹在屏幕上向上穿行的速度将变得相当快:

settings.py

子弹设置

self.bullet_speed_factor = 3

self.bullet_width = 3

—snip—

这项设置的最佳值取决于你的系统速度,请找出适合你的值吧。

13.5.5  重构update_bullets()

下面来重构update_bullets() ,使其不再完成那么多任务。我们将把处理子弹和外星人碰撞的代码移到一个独立的函数中:

game_functions.py

def update_bullets(ai_settings, screen, ship, aliens, bullets):

—snip—

删除已消失的子弹

for bullet in bullets.copy():

if bullet.rect.bottom <= 0:

bullets.remove(bullet)

check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets) def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets): """响应子弹和外星人的碰撞"""

删除发生碰撞的子弹和外星人

collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)

if len(aliens) == 0:

删除现有的所有子弹,并创建一个新的外星人群

bullets.empty()

create_fleet(ai_settings, screen, ship, aliens)

我们创建了一个新函数——check_bullet_alien_collisions() ,以检测子弹和外星人之间的碰撞,以及在整群外星人都被消灭干净时采取相应的措施。这避免

了update_bullets() 太长,简化了后续的开发工作。

动手试一试

13-5 抓球 :创建一个游戏,在屏幕底端放置一个玩家可左右移动的角色。让一个球出现在屏幕顶端,且水平位置是随机的,并让这个球以固定的速度往下落。如果角

色与球发生碰撞(表示将球抓住了),就让球消失。每当角色抓住球或球因抵达屏幕底端而消失后,都创建一个新球。

13.6  结束游戏

如果玩家根本不会输,游戏还有什么趣味和挑战性可言?如果玩家没能在足够短的时间内将整群外星人都消灭干净,且有外星人撞到了飞船,飞船将被摧毁。与此同时,我们还

限制了可供玩家使用的飞船数,而有外星人抵达屏幕底端时,飞船也将被摧毁。玩家用光了飞船后,游戏便结束。

13.6.1  检测外星人和飞船碰撞

我们首先检查外星人和飞船之间的碰撞,以便外星人撞上飞船时我们能够作出合适的响应。我们在更新每个外星人的位置后立即检测外星人和飞船之间的碰撞。

game_functions.py

def update_aliens(ai_settings, ship, aliens):

"""

检查是否有外星人到达屏幕边缘

然后更新所有外星人的位置

"""

check_fleet_edges(ai_settings, aliens)

aliens.update()

检测外星人和飞船之间的碰撞

❶ if pygame.sprite.spritecollideany(ship, aliens):

❷ print("Ship hit!!!")

方法spritecollideany() 接受两个实参:一个精灵和一个编组。它检查编组是否有成员与精灵发生了碰撞,并在找到与精灵发生了碰撞的成员后就停止遍历编组。在这里,

它遍历编组aliens ,并返回它找到的第一个与飞船发生了碰撞的外星人。

如果没有发生碰撞,spritecollideany() 将返回None ,因此❶处的if 代码块不会执行。如果找到了与飞船发生碰撞的外星人,它就返回这个外星人,因此if 代码块将执

行:打印“Ship hit!!!”(见❷)。(有外星人撞到飞船时,需要执行的任务很多:需要删除余下的所有外星人和子弹,让飞船重新居中,以及创建一群新的外星人。编写完成这些任

务的代码前,需要确定检测外星人和飞船碰撞的方法是否可行。而为确定这一点,最简单的方式是编写一条print 语句。)

现在,我们需要将ship 传递给update_aliens() :

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, ship, bullets)

ship.update()

gf.update_bullets(ai_settings, screen, ship, aliens, bullets)

gf.update_aliens(ai_settings, ship, aliens)

gf.update_screen(ai_settings, screen, ship, aliens, bullets)

现在如果你运行这个游戏,则每当有外星人撞到飞船时,终端窗口都将显示“Ship hit!!!”。测试这项功能时,请将alien_drop_speed 设置为较大的值,如50或100,这样外星人

将更快地撞到飞船。

13.6.2  响应外星人和飞船碰撞

现在需要确定外星人与飞船发生碰撞时,该做些什么。我们不销毁ship 实例并创建一个新的ship 实例,而是通过跟踪游戏的统计信息来记录飞船被撞了多少次(跟踪统计信息

还有助于记分)。

下面来编写一个用于跟踪游戏统计信息的新类——GameStats ,并将其保存为文件game_stats.py:

game_stats.py

class GameStats():

"""跟踪游戏的统计信息"""

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

在这个游戏运行期间,我们只创建一个GameStats 实例,但每当玩家开始新游戏时,需要重置一些统计信息。为此,我们在方法reset_stats() 中初始化大部分统计信息,

而不是在init() 中直接初始化它们。我们在init() 中调用这个方法,这样创建GameStats 实例时将妥善地设置这些统计信息(见❶),同时在玩家开始新游戏

时也能调用reset_stats() 。

当前只有一项统计信息——ships_left ,其值在游戏运行期间将不断变化。一开始玩家拥有的飞船数存储在settings.py的ship_limit 中:

settings.py

飞船设置

self.ship_speed_factor = 1.5

self.ship_limit = 3

我们还需对alien_invasion.py做些修改,以创建一个GameStats 实例:

alien_invasion.py

—snip—

from settings import Settings

❶ from game_stats import GameStats

—snip—

def run_game():

—snip—

pygame.display.set_caption("Alien Invasion")

创建一个用于存储游戏统计信息的实例

❷ stats = GameStats(ai_settings)

—snip—

开始游戏主循环

while True:

—snip—

gf.update_bullets(ai_settings, screen, ship, aliens, bullets)

❸ gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)

—snip—

我们导入了新类GameStats (见❶),创建了一个名为stats 的实例(见❷),再调用update_aliens() 并添加了实参stats 、screen 和ship (见❸)。在有外星人

撞到飞船时,我们将使用这些实参来跟踪玩家还有多少艘飞船,以及创建一群新的外星人。

有外星人撞到飞船时,我们将余下的飞船数减1,创建一群新的外星人,并将飞船重新放置到屏幕底端中央(我们还将让游戏暂停一段时间,让玩家在新外星人群出现前注意到发

生了碰撞,并将重新创建外星人群)。

下面将实现这些功能的大部分代码放到函数ship_hit() 中:

game_functions.py

import sys

❶ from time import sleep

import pygame

—snip—

def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):

"""响应被外星人撞到的飞船"""

将ships_left减1

❷ stats.ships_left -= 1

清空外星人列表和子弹列表

❸ aliens.empty()

bullets.empty()

创建一群新的外星人,并将飞船放到屏幕底端中央

❹ create_fleet(ai_settings, screen, ship, aliens)

ship.center_ship()

暂停

❺ sleep(0.5)

❻ def update_aliens(ai_settings, stats, screen, ship, aliens, bullets): —snip—

检测外星人和飞船碰撞

if pygame.sprite.spritecollideany(ship, aliens):

ship_hit(ai_settings, stats, screen, ship, aliens, bullets)

我们首先从模块time 中导入了函数sleep() ,以便使用它来让游戏暂停(见❶)。新函数ship_hit() 在飞船被外星人撞到时作出响应。在这个函数内部,将余下的飞船数

减1(见❷),然后清空编组aliens 和bullets (见❸)。

接下来,我们创建一群新的外星人,并将飞船居中(见❹),稍后将在Ship 类中添加方法center_ship() 。最后,我们更新所有元素后(但在将修改显示到屏幕前)暂停,

让玩家知道其飞船被撞到了(见❺)。屏幕将暂时停止变化,让玩家能够看到外星人撞到了飞船。函数sleep() 执行完毕后,将接着执行函数update_screen() ,将新的外

星人群绘制到屏幕上。

我们还更新了update_aliens() 的定义,使其包含形参stats 、screen 和bullets (见❻),让它能够在调用ship_hit() 时传递这些值。

下面是新方法center_ship() ,请将其添加到ship.py的末尾:

ship.py

def center_ship(self):

"""让飞船在屏幕上居中"""

self.center = self.screen_rect.centerx

为让飞船居中,我们将飞船的属性center 设置为屏幕中心的 x 坐标,而该坐标是通过属性screen_rect 获得的。

注意  我们根本没有创建多艘飞船,在整个游戏运行期间,我们都只创建了一个飞船实例,并在该飞船被撞到时将其居中。统计信息ships_left 让我们知道飞船

是否用完。

请运行这个游戏,射杀几个外星人,并让一个外星人撞到飞船。游戏暂停后,将出现一群新的外星人,而飞船将在屏幕底端居中。

13.6.3  有外星人到达屏幕底端

如果有外星人到达屏幕底端,我们将像有外星人撞到飞船那样作出响应。请添加一个执行这项任务的新函数,并将其命名为update_aliens() :

game_functions.py

def check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets): """检查是否有外星人到达了屏幕底端"""

screen_rect = screen.get_rect()

for alien in aliens.sprites():

❶ if alien.rect.bottom >= screen_rect.bottom:

像飞船被撞到一样进行处理

ship_hit(ai_settings, stats, screen, ship, aliens, bullets)

break

def update_aliens(ai_settings, stats, screen, ship, aliens, bullets):

—snip—

检查是否有外星人到达屏幕底端

❷ check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets) 函数check_aliens_bottom() 检查是否有外星人到达了屏幕底端。到达屏幕底端后,外星人的属性rect.bottom 的值大于或等于屏幕的属性rect.bottom 的值(见

❶)。如果有外星人到达屏幕底端,我们就调用ship_hit() ;只要检测到一个外星人到达屏幕底端,就无需检查其他外星人,因此我们在调用ship_hit() 后退出循环。

我们在更新所有外星人的位置并检测是否有外星人和飞船发生碰撞后调用check_aliens_bottom() (见❷)。现在,每当有外星人撞到飞船或抵达屏幕底端时,都将出现一

群新的外星人。

13.6.4  游戏结束

现在这个游戏看起来更完整了,但它永远都不会结束,只是ships_left 不断变成更小的负数。下面在GameStats 中添加一个作为标志的属性game_active ,以便在玩家的

飞船用完后结束游戏:

game_stats.py

def init(self, settings):

—snip—

游戏刚启动时处于活动状态

self.game_active = True

现在在ship_hit() 中添加代码,在玩家的飞船都用完后将game_active 设置为False :

game_functions.py

def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):

"""响应飞船被外星人撞到"""

if stats.ships_left > 0:

将ships_left减1

stats.ships_left -= 1

—snip—

暂停一会儿

sleep(0.5)

else:

stats.game_active = False

ship_hit() 的大部分代码都没变。我们将原来的所有代码都移到了一个if 语句块中,这条if 语句检查玩家是否至少还有一艘飞船。如果是这样,就创建一群新的外星人,暂

停一会儿,再接着往下执行。如果玩家没有飞船了,就将game_active 设置为False 。

13.7  确定应运行游戏的哪些部分

在alien_invasion.py中,我们需要确定游戏的哪些部分在任何情况下都应运行,哪些部分仅在游戏处于活动状态时才运行:

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, ship, bullets)

if stats.game_active:

ship.update()

gf.update_bullets(ai_settings, screen, ship, aliens, bullets)

gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)

gf.update_screen(ai_settings, screen, ship, aliens, bullets)

在主循环中,在任何情况下都需要调用check_events() ,即便游戏处于非活动状态时亦如此。例如,我们需要知道玩家是否按了Q键以退出游戏,或单击关闭窗口的按钮。

我们还需要不断更新屏幕,以便在等待玩家是否选择开始新游戏时能够修改屏幕。其他的函数仅在游戏处于活动状态时才需要调用,因为游戏处于非活动状态时,我们不用更新

游戏元素的位置。

现在,你运行这个游戏时,它将在飞船用完后停止不动。

动手试一试

13-6 游戏结束 :在为完成练习13-5而编写的代码中,跟踪玩家有多少次未将球接着。在未接着球的次数到达三次后,结束游戏。

13.8  小结

在本章中,你学习了:如何在游戏中添加大量相同的元素,如创建一群外星人;如何使用嵌套循环来创建元素网格,还通过调用每个元素的方法update() 移动了大量的元素;

如何控制对象在屏幕上移动的方向,以及如何响应事件,如有外星人到达屏幕边缘;如何检测和响应子弹和外星人碰撞以及外星人和飞船碰撞;如何在游戏中跟踪统计信息,以

及如何使用标志game_active 来判断游戏是否结束了。

06 - 图5

在与这个项目相关的最后一章中,我们将添加一个Play按钮,让玩家能够开始游戏,以及游戏结束后再玩。每当玩家消灭一群外星人后,我们都将加快游戏的节奏,并添加一个记

分系统,得到一个极具可玩性的游戏!

记分

在本章中,我们将结束游戏《外星人入侵》的开发。我们将添加一个Play按钮,用于根据需要启动游戏以及在游戏结束后重启游戏。我们还将修改这个游戏,使其在玩

家的等级提高时加快节奏,并实现一个记分系统。阅读本章后,你将掌握足够多的知识,能够开始编写随玩家等级提高而加大难度以及显示得分的游戏。

14.1  添加Play 按钮

在本节中,我们将添加一个Play按钮,它在游戏开始前出现,并在游戏结束后再次出现,让玩家能够开始新游戏。

当前,这个游戏在玩家运行alien_invasion.py时就开始了。下面让游戏一开始处于非活动状态,并提示玩家单击Play按钮来开始游戏。为此,在game_stats.py中输入如下代码:

game_stats.py

def init(self, ai_settings):

"""初始化统计信息"""

self.ai_settings = ai_settings

self.reset_stats()

让游戏一开始处于非活动状态

self.game_active = False

def reset_stats(self):

—snip—

现在游戏一开始将处于非活动状态,等我们创建Play按钮后,玩家才能开始游戏。

14.1.1  创建Button 类

由于Pygame没有内置创建按钮的方法,我们创建一个Button 类,用于创建带标签的实心矩形。你可以在游戏中使用这些代码来创建任何按钮。下面是Button 类的第一部分,

请将这个类保存为文件button.py:

button.py

import pygame.font

class Button():

❶ def init(self, ai_settings, screen, msg):

"""初始化按钮的属性"""

self.screen = screen

self.screen_rect = 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)

创建按钮的rect对象,并使其居中

❹ self.rect = pygame.Rect(0, 0, self.width, self.height)

self.rect.center = self.screen_rect.center

按钮的标签只需创建一次

❺ self.prep_msg(msg)

首先,我们导入了模块pygame.font ,它让Pygame能够将文本渲染到屏幕上。方法init() 接受参数self ,对象ai_settings 和screen ,以及msg ,其中msg 是

要在按钮中显示的文本(见❶)。我们设置按钮的尺寸(见❷),然后通过设置button_color 让按钮的rect 对象为亮绿色,并通过设置text_color 让文本为白色。

在(见❸)处,我们指定使用什么字体来渲染文本。实参None 让Pygame 使用默认字体,而48 指定了文本的字号。为让按钮在屏幕上居中,我们创建一个表示按钮的rect 对

象(见❹),并将其center 属性设置为屏幕的center 属性。

Pygame通过将你要显示的字符串渲染为图像来处理文本。在❺处,我们调用prep_msg() 来处理这样的渲染。

prep_msg() 的代码如下:

button.py

def prep_msg(self, msg):

"""将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

方法prep_msg() 接受实参self 以及要渲染为图像的文本(msg )。调用font.render() 将存储在msg 中的文本转换为图像,然后将该图像存储在msg_image 中(见

❶)。方法font.render() 还接受一个布尔实参,该实参指定开启还是关闭反锯齿功能(反锯齿让文本的边缘更平滑)。余下的两个实参分别是文本颜色和背景色。我们启用

了反锯齿功能,并将文本的背景色设置为按钮的颜色(如果没有指定背景色,Pygame将以透明背景的方式渲染文本)。

在❷处,我们让文本图像在按钮上居中:根据文本图像创建一个rect ,并将其center 属性设置为按钮的center 属性。

最后,我们创建方法draw_button() ,通过调用它可将这个按钮显示到屏幕上:

button.py

def draw_button(self):

绘制一个用颜色填充的按钮,再绘制文本

self.screen.fill(self.button_color, self.rect)

self.screen.blit(self.msg_image, self.msg_image_rect)

我们调用screen.fill() 来绘制表示按钮的矩形,再调用screen.blit() ,并向它传递一幅图像以及与该图像相关联的rect 对象,从而在屏幕上绘制文本图像。至

此,Button 类便创建好了。

14.1.2  在屏幕上绘制按钮

我们将使用Button 类来创建一个Play按钮。鉴于只需要一个Play按钮,我们直接在alien_invasion.py中创建它,如下所示:

alien_invasion.py

—snip—

from game_stats import GameStats

from button import Button

—snip—

def run_game():

—snip—

pygame.display.set_caption("Alien Invasion")

创建Play按钮

❶ play_button = Button(ai_settings, screen, "Play")

—snip—

开始游戏主循环

while True:

—snip—

❷ gf.update_screen(ai_settings, screen, stats, ship, aliens, bullets,

play_button)

run_game()

我们导入Button 类,并创建一个名为play_button 的实例(见❶),然后我们将play_button 传递给update_screen() ,以便能够在屏幕更新时显示按钮(见❷)。

接下来,修改update_screen() ,以便在游戏处于非活动状态时显示Play按钮:

game_functions.py

def update_screen(ai_settings, screen, stats, ship, aliens, bullets,

play_button):

"""更新屏幕上的图像,并切换到新屏幕"""

—snip—

如果游戏处于非活动状态,就绘制Play按钮

if not stats.game_active:

play_button.draw_button()

让最近绘制的屏幕可见

pygame.display.flip()

为让Play按钮位于其他所有屏幕元素上面,我们在绘制其他所有游戏元素后再绘制这个按钮,然后切换到新屏幕。如果你现在运行这个游戏,将在屏幕中央看到一个Play按钮,如

图14-1所示。

06 - 图6

图14-1  游戏处于非活动状态时出现的Play 按钮

14.1.3  开始游戏

为在玩家单击Play按钮时开始新游戏,需在game_functions.py中添加如下代码,以监视与这个按钮相关的鼠标事件:

game_functions.py

def check_events(ai_settings, screen, stats, play_button, ship, bullets): """响应按键和鼠标事件"""

for event in pygame.event.get():

if event.type == pygame.QUIT:

—snip—

❶ elif event.type == pygame.MOUSEBUTTONDOWN:

❷ mouse_x, mouse_y = pygame.mouse.get_pos()

❸ check_play_button(stats, play_button, mouse_x, mouse_y)

def check_play_button(stats, play_button, mouse_x, mouse_y):

"""在玩家单击Play按钮时开始新游戏"""

❹ if play_button.rect.collidepoint(mouse_x, mouse_y):

stats.game_active = True

我们修改了check_events() 的定义,在其中添加了形参stats 和play_button 。我们将使用stats 来访问标志game_active ,并使用play_button 来检查玩家是否

单击了Play按钮。

无论玩家单击屏幕的什么地方,Pygame都将检测到一个MOUSEBUTTONDOWN 事件(见❶),但我们只想让这个游戏在玩家用鼠标单击Play按钮时作出响应。为此,我们使用

了pygame.mouse.get_pos() ,它返回一个元组,其中包含玩家单击时鼠标的 x 和 y 坐标(见❷)。我们将这些值传递给函数check_play_button() (见❸),而这个函

数使用collidepoint() 检查鼠标单击位置是否在Play按钮的rect 内(见❹)。如果是这样的,我们就将game_active 设置为True ,让游戏就此开始!

在alien_invasion.py 中调用check_events() ,需要传递另外两个实参——stats 和play_button :

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, stats, play_button, ship,

bullets)

—snip—

至此,你应该能够开始这个游戏了。游戏结束时,game_active 应为False ,并重新显示Play按钮。

14.1.4  重置游戏

前面编写的代码只处理了玩家第一次单击Play按钮的情况,而没有处理游戏结束的情况,因为没有重置导致游戏结束的条件。

为在玩家每次单击Play按钮时都重置游戏,需要重置统计信息、删除现有的外星人和子弹、创建一群新的外星人,并让飞船居中,如下所示:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens, bullets, mouse_x, mouse_y): """在玩家单击Play按钮时开始新游戏"""

if play_button.rect.collidepoint(mouse_x, mouse_y):

重置游戏统计信息

❶ stats.reset_stats()

stats.game_active = True

清空外星人列表和子弹列表

❷ aliens.empty()

bullets.empty()

创建一群新的外星人,并让飞船居中

❸ create_fleet(ai_settings, screen, ship, aliens)

ship.center_ship()

我们更新了check_play_button() 的定义,使其能够访问ai_settings 、stats 、ship 、aliens 和bullets 。为重置在游戏期间发生了变化的设置以及刷新游戏的

视觉元素,它需要这些对象。

在❶处,我们重置了游戏统计信息,给玩家提供了三艘新飞船。接下来,我们将game_active 设置为True (这样,这个函数的代码执行完毕后,游戏就会开始),清空编

组aliens 和bullets (见❷),创建一群新的外星人,并将飞船居中(见❸)。

check_events() 的定义需要修改,调用check_play_button() 的代码亦如此:

game_functions.py

def check_events(ai_settings, screen, stats, play_button, ship, aliens, bullets): """响应按键和鼠标事件"""

for event in pygame.event.get():

if event.type == pygame.QUIT:

—snip—

elif event.type == pygame.MOUSEBUTTONDOWN:

mouse_x, mouse_y = pygame.mouse.get_pos()

❶ check_play_button(ai_settings, screen, stats, play_button, ship,

aliens, bullets, mouse_x, mouse_y)

check_events() 的定义需要形参aliens ,以便将它传递给check_play_button() 。接下来,我们修改了调用check_play_button() 的代码,以将合适的实参传递

给它(见❶)。

下面来修改alien_invasion.py中调用check_events() 的代码,以将实参aliens 传递给它:

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, stats, play_button, ship,

aliens, bullets)

—snip—

现在,每当玩家单击Play按钮时,这个游戏都将正确地重置,让玩家想玩多少次就玩多少次!

14.1.5  将Play 按钮切换到非活动状态

当前,Play按钮存在一个问题,那就是即便Play按钮不可见,玩家单击其原来所在的区域时,游戏依然会作出响应。游戏开始后,如果玩家不小心单击了Play按钮原来所处的区

域,游戏将重新开始!

为修复这个问题,可让游戏仅在game_active 为False 时才开始:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens, bullets, mouse_x, mouse_y): """玩家单击Play按钮时开始新游戏"""

❶ button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

❷ if button_clicked and not stats.game_active:

重置游戏统计信息

—snip—

标志button_clicked 的值为True 或False (见❶),仅当玩家单击了Play按钮且 游戏当前处于非活动状态时,游戏才重新开始(见❷)。为测试这种行为,可开始新游

戏,并不断地单击Play按钮原来所在的区域。如果一切都像预期的那样工作,单击Play按钮原来所处的区域应该没有任何影响。

14.1.6  隐藏光标

为让玩家能够开始游戏,我们要让光标可见,但游戏开始后,光标只会添乱。为修复这种问题,我们在游戏处于活动状态时让光标不可见:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens, bullets, mouse_x, mouse_y): """在玩家单击Play按钮时开始新游戏"""

button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

if button_clicked and not stats.game_active:

隐藏光标

pygame.mouse.set_visible(False)

—snip—

通过向set_visible() 传递False ,让Pygame在光标位于游戏窗口内时将其隐藏起来。

游戏结束后,我们将重新显示光标,让玩家能够单击Play按钮来开始新游戏。相关的代码如下:

game_functions.py

def ship_hit(ai_settings, screen, stats, ship, aliens, bullets):

"""响应飞船被外星人撞到"""

if stats.ships_left > 0:

—snip—

else:

stats.game_active = False

pygame.mouse.set_visible(True)

在ship_hit() 中,我们在游戏进入非活动状态后,立即让光标可见。关注这样的细节让游戏显得更专业,也让玩家能够专注于玩游戏而不是费力搞明白用户界面。

动手试一试

14-1 按P 开始新游戏 :鉴于游戏《外星人入侵》使用键盘来控制飞船,最好让玩家也能够通过按键来开始游戏。请添加让玩家在按P时开始游戏的代码。也许这样做

会有所帮助:将check_play_button() 的一些代码提取出来,放到一个名为start_game() 的函数中,并在check_play_button() 和check_keydown_events() 中调用这个函数。

14-2 射击练习 :创建一个矩形,它在屏幕右边缘以固定的速度上下移动。然后,在屏幕左边缘创建一艘飞船,玩家可上下移动该飞船,并射击前述矩形目标。添加一

个用于开始游戏的Play按钮,在玩家三次未击中目标时结束游戏,并重新显示Play按钮,让玩家能够通过单击该按钮来重新开始游戏。

14.2  提高等级

当前,将整群外星人都消灭干净后,玩家将提高一个等级,但游戏的难度并没有变。下面来增加一点趣味性:每当玩家将屏幕上的外星人都消灭干净后,加快游戏的节奏,让游

戏玩起来更难。

14.2.1  修改速度设置

我们首先重新组织Settings 类,将游戏设置划分成静态的和动态的两组。对于随着游戏进行而变化的设置,我们还确保它们在开始新游戏时被重置。settings.py的方

init() 如下:

settings.py

def init(self):

"""初始化游戏的静态设置"""

屏幕设置

self.screen_width = 1200

self.screen_height = 800

self.bg_color = (230, 230, 230)

飞船设置

self.ship_limit = 3

子弹设置

self.bullet_width = 3

self.bullet_height = 15

self.bullet_color = 60, 60, 60

self.bullets_allowed = 3

外星人设置

self.fleet_drop_speed = 10

以什么样的速度加快游戏节奏

❶ self.speedup_scale = 1.1

❷ self.initialize_dynamic_settings()

我们依然在init() 中初始化静态设置。在❶处,我们添加了设置speedup_scale ,用于控制游戏节奏的加快速度:2表示玩家每提高一个等级,游戏的节奏就翻倍;1

表示游戏节奏始终不变。将其设置为1.1能够将游戏节奏提高到够快,让游戏既有难度,又并非不可完成。最后,我们调用initialize_dynamic_settings() ,以初始化随

游戏进行而变化的属性(见❷)。

initialize_dynamic_settings() 的代码如下:

settings.py

def initialize_dynamic_settings(self):

"""初始化随游戏进行而变化的设置"""

self.ship_speed_factor = 1.5

self.bullet_speed_factor = 3

self.alien_speed_factor = 1

fleet_direction为1表示向右;为-1表示向左

self.fleet_direction = 1

这个方法设置了飞船、子弹和外星人的初始速度。随游戏的进行,我们将提高这些速度,而每当玩家开始新游戏时,都将重置这些速度。在这个方法中,我们还设置

了fleet_direction ,使得游戏刚开始时,外星人总是向右移动。每当玩家提高一个等级时,我们都使用increase_speed() 来提高飞船、子弹和外星人的速度:

settings.py

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

为提高这些游戏元素的速度,我们将每个速度设置都乘以speedup_scale 的值。

在check_bullet_alien_collisions() 中,我们在整群外星人都被消灭后调用increase_speed() 来加快游戏的节奏,再创建一群新的外星人:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets): —snip—

if len(aliens) == 0:

删除现有的子弹,加快游戏节奏,并创建一群新的外星人

bullets.empty()

ai_settings.increase_speed()

create_fleet(ai_settings, screen, ship, aliens)

通过修改速度设置ship_speed_factor 、alien_speed_factor 和bullet_speed_factor 的值,足以加快整个游戏的节奏!

14.2.2  重置速度

每当玩家开始新游戏时,我们都需要将发生了变化的设置重置为初始值,否则新游戏开始时,速度设置将是前一次游戏增加了的值:

game_functions.py def check_play_button(ai_settings, screen, stats, play_button, ship, aliens, bullets, mouse_x, mouse_y): """在玩家单击Play按钮时开始新游戏"""

button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

if button_clicked and not stats.game_active:

重置游戏设置

ai_settings.initialize_dynamic_settings()

隐藏光标

pygame.mouse.set_visible(False)

—snip—

现在,游戏《外星人入侵》玩起来更有趣,也更有挑战性。每当玩家将屏幕上的外星人消灭干净后,游戏都将加快节奏,因此难度会更大些。如果游戏的难度提高得太快,可降

低settings.speedup_scale 的值;如果游戏的挑战性不足,可稍微提高这个设置的值。找出这个设置的最佳值,让难度的提高速度相对合理:一开始的几群外星人很容易

消灭干净;接下来的几群消灭起来有一定难度,但也不是不可能;而要将更靠后的外星人群消灭干净几乎不可能。

动手试一试

14-3 有一定难度的射击练习 :以你为完成练习14-2而做的工作为基础,让标靶的移动速度随游戏进行而加快,并在玩家单击Play按钮时将其重置为初始值。

14.3  记分

下面来实现一个记分系统,以实时地跟踪玩家的得分,并显示最高得分、当前等级和余下的飞船数。

得分是游戏的一项统计信息,因此我们在GameStats 中添加一个score 属性:

game_stats.py

class GameStats():

—snip—

def reset_stats(self):

"""初始化随游戏进行可能变化的统计信息"""

self.ships_left = self.ai_settings.ship_limit

self.score = 0

为在每次开始游戏时都重置得分,我们在resetstats() 而不是_init() 中初始化score 。

14.3.1  显示得分

为在屏幕上显示得分,我们首先创建一个新类Scoreboard 。就当前而言,这个类只显示当前得分,但后面我们也将使用它来显示最高得分、等级和余下的飞船数。下面是这个

类的前半部分,它被保存为文件scoreboard.py:

scoreboard.py

import pygame.font

class Scoreboard():

"""显示得分信息的类"""

❶ def init(self, ai_settings, screen, stats):

"""初始化显示得分涉及的属性"""

self.screen = screen

self.screen_rect = screen.get_rect()

self.ai_settings = ai_settings

self.stats = stats

显示得分信息时使用的字体设置

❷ self.text_color = (30, 30, 30)

❸ self.font = pygame.font.SysFont(None, 48)

准备初始得分图像

❹ self.prep_score()

由于Scoreboard 在屏幕上显示文本,因此我们首先导入模块pygame.font 。接下来,我们在init() 中包含形参ai_settings 、screen 和stats ,让它能够报告

我们跟踪的值(见❶)。然后,我们设置文本颜色(见❷)并实例化一个字体对象(见❸)。

为将要显示的文本转换为图像,我们调用了prep_score() (见❹),其定义如下:

scoreboard.py

def prep_score(self):

"""将得分转换为一幅渲染的图像"""

❶ score_str = str(self.stats.score)

❷ self.score_image = self.font.render(score_str, True, self.text_color, self.ai_settings.bg_color) # 将得分放在屏幕右上角

❸ self.score_rect = self.score_image.get_rect()

❹ self.score_rect.right = self.screen_rect.right - 20

❺ self.score_rect.top = 20

在prep_score() 中,我们首先将数字值stats.score 转换为字符串(见❶),再将这个字符串传递给创建图像的render() (见❷)。为在屏幕上清晰地显示得分,我们

向render() 传递了屏幕背景色,以及文本颜色。

我们将得分放在屏幕右上角,并在得分增大导致这个数字更宽时让它向左延伸。为确保得分始终锚定在屏幕右边,我们创建了一个名为score_rect 的rect (见❸),让其右

边缘与屏幕右边缘相距20像素(见❹),并让其上边缘与屏幕上边缘也相距20像素(见❺)。

最后,我们创建方法show_score() ,用于显示渲染好的得分图像:

scoreboard.py

06 - 图7

def show_score(self):

"""在屏幕上显示得分"""

self.screen.blit(self.score_image, self.score_rect)

这个方法将得分图像显示到屏幕上,并将其放在score_rect 指定的位置。

14.3.2  创建记分牌

为显示得分,我们在alien_invasion.py中创建一个Scoreboard 实例:

alien_invasion.py

—snip—

from game_stats import GameStats

from scoreboard import Scoreboard

—snip—

def run_game():

—snip—

创建存储游戏统计信息的实例,并创建记分牌

stats = GameStats(ai_settings)

❶ sb = Scoreboard(ai_settings, screen, stats)

—snip—

开始游戏主循环

while True:

—snip—

❷ gf.update_screen(ai_settings, screen, stats, sb, ship, aliens,

bullets, play_button)

run_game()

我们导入新创建的类Scoreboard ,并在创建实例stats 后创建了一个名为sb 的Scoreboard 实例(见❶)。接下来,我们将sb 传递给update_screen() ,让它能够在

屏幕上显示得分(见❷)。

为显示得分,将update_screen() 修改成下面这样:

game_functions.py

def update_screen(ai_settings, screen, stats, sb, ship, aliens, bullets, play_button): —snip—

显示得分

sb.show_score()

如果游戏处于非活动状态,就显示Play按钮

if not stats.game_active:

play_button.draw_button()

让最近绘制的屏幕可见

pygame.display.flip()

我们在update_screen() 的形参列表中添加了sb ,并在绘制Play按钮前调用show_score 。

如果现在运行这个游戏,你将在屏幕右上角看到0(当前,我们只想在进一步开发记分系统前确认得分出现在正确的地方)。图14-2显示了游戏开始前的得分。

图14-2  得分出现在屏幕右上角

下面来指定每个外星人值多少点!

14.3.3  在外星人被消灭时更新得分

为在屏幕上实时地显示得分,每当有外星人被击中时,我们都更新stats.score 的值,再调用prep_score() 更新得分图像。但在此之前,我们需要指定玩家每击落一个外

星人都将得到多少个点:

settings.py

def initialize_dynamic_settings(self):

—snip—

记分

self.alien_points = 50

随着游戏的进行,我们将提高每个外星人值的点数。为确保每次开始新游戏时这个值都会被重置,我们在initialize_dynamic_settings() 中设置它。

在check_bullet_alien_collisions() 中,每当有外星人被击落时,都更新得分:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship, aliens, bullets): """响应子弹和外星人发生碰撞"""

删除发生碰撞的子弹和外星人

collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)

if collisions:

❶ stats.score += ai_settings.alien_points

sb.prep_score()

—snip—

我们更新check_bullet_alien_collisions() 的定义,在其中包含了形参stats 和sb ,让它能够更新得分和记分牌。有子弹撞到外星人时,Pygame返回一个字典

(collisions )。我们检查这个字典是否存在,如果存在,就将得分加上一个外星人值的点数(见❶)。接下来,我们调用prep_score() 来创建一幅显示最新得分的新图

像。

我们需要修改update_bullets() ,确保在函数之间传递合适的实参:

game_functions.py

def update_bullets(ai_settings, screen, stats, sb, ship, aliens, bullets): """更新子弹的位置,并删除已消失的子弹"""

—snip—

check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

aliens, bullets)

在update_bullets() 的定义中,需要新增形参stats 和sb ,而调用check_bullet_alien_collisions() 时,也需要传递实参stats 和sb 。

我们还需要修改主while 循环中调用update_bullets() 的代码:

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, stats, play_button, ship,

aliens, bullets)

if stats.game_active:

ship.update()

gf.update_bullets(ai_settings, screen, stats, sb, ship, aliens,

bullets)

—snip—

调用update_bullets() 时,需要传递实参stats 和sb 。

如果你现在运行这个游戏,得分将不断增加!

14.3.4  将消灭的每个外星人的点数都计入得分

当前,我们的代码可能遗漏了一些被消灭的外星人。例如,如果在一次循环中有两颗子弹射中了外星人,或者因子弹更宽而同时击中了多个外星人,玩家将只能得到一个被消灭

的外星人的点数。为修复这种问题,我们来调整检测子弹和外星人碰撞的方式。

在check_bullet_alien_collisions() 中,与外星人碰撞的子弹都是字典collisions 中的一个键;而与每颗子弹相关的值都是一个列表,其中包含该子弹撞到的外星

人。我们遍历字典collisions ,确保将消灭的每个外星人的点数都记入得分:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship, aliens, bullets): —snip—

if collisions:

❶ for aliens in collisions.values():

stats.score += ai_settings.alien_points * len(aliens)

sb.prep_score()

—snip—

如果字典collisions 存在,我们就遍历其中的所有值。别忘了,每个值都是一个列表,包含被同一颗子弹击中的所有外星人。对于每个列表,都将一个外星人的点数乘以其中

包含的外星人数量,并将结果加入到当前得分中。为测试这一点,请将子弹宽度改为300像素,并核实你得到了更宽的子弹击中的每个外星人的点数,再将子弹宽度恢复到正常

值。

14.3.5  提高点数

玩家每提高一个等级,游戏都变得更难,因此处于较高的等级时,外星人的点数应更高。为实现这种功能,我们添加一些代码,以在游戏节奏加快时提高点数:

settings.py

06 - 图8

class Settings():

"""存储游戏《外星人入侵》的所有设置的类"""

def init(self):

—snip—

加快游戏节奏的速度

self.speedup_scale = 1.1

外星人点数的提高速度

❶ self.score_scale = 1.5

self.initialize_dynamic_settings()

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

❷ self.alien_points = int(self.alien_points * self.score_scale)

我们定义了点数提高的速度,并称之为score_scale (见❶)。很小的节奏加快速度(1.1)让游戏很快就变得极具挑战性,但为让记分发生显著的变化,需要将点数的提高速

度设置为更大的值(1.5)。现在,我们在加快游戏节奏的同时,提高了每个外星人的点数。为让点数为整数,我们使用了函数int() 。

为显示外星人的点数,我们在Settings 的方法increase_speed() 中添加了一条print 语句:

settings.py

def increase_speed(self):

—snip—

self.alien_points = int(self.alien_points * self.score_scale)

print(self.alien_points)

现在每当提高一个等级时,你都会在终端窗口看到新的点数值。

注意  确认点数在不断增加后,一定要删除这条print 语句,否则它可能会影响游戏的性能以及分散玩家的注意力。

14.3.6  将得分圆整

大多数街机风格的射击游戏都将得分显示为10的整数倍,下面让我们的记分系统遵循这个原则。我们还将设置得分的格式,在大数字中添加用逗号表示的千位分隔符。我们

在Scoreboard 中执行这种修改:

scoreboard.py

def prep_score(self):

"""将得分转换为渲染的图像"""

❶ rounded_score = int(round(self.stats.score, -1))

❷ score_str = "{:,}".format(rounded_score)

self.score_image = self.font.render(score_str, True, self.text_color,

self.ai_settings.bg_color)

—snip—

函数round() 通常让小数精确到小数点后多少位,其中小数位数是由第二个实参指定的。然而,如果将第二个实参指定为负数,round() 将圆整到最近的10、100、1000等整

数倍。❶处的代码让Python将stats.score 的值圆整到最近的10的整数倍,并将结果存储到rounded_score 中。

注意  在Python 2.7中,round() 总是返回一个小数值,因此我们使用int() 来确保报告的得分为整数。如果你使用的是Python 3,可省略对int() 的调用。

❷处使用了一个字符串格式设置指令,它让Python将数值转换为字符串时在其中插入逗号,例如,输出1,000,000 而不是1000000 。如果你现在运行这个游戏,看到的将是10

的整数倍的整洁得分,即便得分很高亦如此,如图14-3所示。

图14-3  得分为10 的整数倍,并将逗号用作千分位分隔符

14.3.7  最高得分

每个玩家都想超过游戏的最高得分记录。下面来跟踪并显示最高得分,给玩家提供要超越的目标。我们将最高得分存储在GameStats 中:

game_stats.py

def init(self, ai_settings):

—snip—

在任何情况下都不应重置最高得分

self.high_score = 0

鉴于在任何情况下都不会重置最高得分,我们在init() 中而不是reset_stats() 中初始化high_score 。

下面来修改Scoreboard 以显示最高得分。先来修改方法init() :

scoreboard.py

def init(self, ai_settings, screen, stats):

—snip—

准备包含最高得分和当前得分的图像

self.prep_score()

❶ self.prep_high_score()

最高得分将与当前得分分开显示,因此我们需要编写一个新方法prep_high_score() ,用于准备包含最高得分的图像(见❶)。

方法prep_high_score() 的代码如下:

scoreboard.py

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.ai_settings.bg_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.score_rect.top

我们将最高得分圆整到最近的10的整数倍(见❶),并添加了用逗号表示的千分位分隔符(见❷)。然后,我们根据最高得分生成一幅图像(见❸),使其水平居中(见❹),

并将其top 属性设置为当前得分图像的top 属性(见❺)。

现在,方法show_score() 需要在屏幕右上角显示当前得分,并在屏幕顶部中央显示最高得分:

scoreboard.py

def show_score(self):

"""在屏幕上显示当前得分和最高得分"""

self.screen.blit(self.score_image, self.score_rect)

self.screen.blit(self.high_score_image, self.high_score_rect)

为检查是否诞生了新的最高得分,我们在game_functions.py中添加一个新函数check_high_score() :

game_functions.py

def check_high_score(stats, sb):

"""检查是否诞生了新的最高得分"""

❶ if stats.score > stats.high_score:

stats.high_score = stats.score

sb.prep_high_score()

函数check_high_score() 包含两个形参:stats 和sb 。它使用stats 来比较当前得分和最高得分,并在必要时使用sb 来修改最高得分图像。在❶处,我们比较当前得分

和最高得分,如果当前得分更高,就更新high_score 的值,并调用prep_high_score() 来更新包含最高得分的图像。

在check_bullet_alien_collisions() 中,每当有外星人被消灭,都需要在更新得分后调用check_high_score() :

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship, aliens, bullets): —snip—

if collisions:

for aliens in collisions.values():

stats.score += ai_settings.alien_points * len(aliens)

sb.prep_score()

check_high_score(stats, sb)

—snip—

字典collisions 存在时,我们根据消灭了多少外星人来更新得分,再调用check_high_score() 。

第一次玩这款游戏时,当前得分就是最高得分,因此两个地方显示的都是当前得分。但再次开始这个游戏时,最高得分出现在中央,而当前得分出现在右边,如图14-4所示。

06 - 图9

图14-4  最高得分显示在屏幕顶部中央

14.3.8  显示等级

为在游戏中显示玩家的等级,首先需要在GameStats 中添加一个表示当前等级的属性。为确保每次开始新游戏时都重置等级,在reset_stats() 中初始化它:

game_stats.py

def reset_stats(self):

"""初始化随游戏进行可能变化的统计信息"""

self.ships_left = self.ai_settings.ship_limit

self.score = 0

self.level = 1

为让Scoreboard 能够在当前得分下方显示当前等级,我们在init() 中调用了一个新方法prep_level() :

scoreboard.py

def init(self, ai_settings, screen, stats):

—snip—

准备包含得分的初始图像

self.prep_score()

self.prep_high_score()

self.prep_level()

prep_level() 的代码如下:

scoreboard.py

def prep_level(self):

"""将等级转换为渲染的图像"""

❶ self.level_image = self.font.render(str(self.stats.level), True,

self.text_color, self.ai_settings.bg_color)

将等级放在得分下方

self.level_rect = self.level_image.get_rect()

❷ self.level_rect.right = self.score_rect.right

❸ self.level_rect.top = self.score_rect.bottom + 10

方法prep_level() 根据存储在stats.level 中的值创建一幅图像(见❶),并将其right 属性设置为得分的right 属性(见❷)。然后,将top 属性设置为比得分图像

的bottom 属性大10像素,以便在得分和等级之间留出一定的空间(见❸)。

我们还需要更新show_score() :

scoreboard.py

def show_score(self):

"""在屏幕上显示飞船和得分"""

self.screen.blit(self.score_image, self.score_rect)

self.screen.blit(self.high_score_image, self.high_score_rect)

self.screen.blit(self.level_image, self.level_rect)

在这个方法中,添加了一行在屏幕上显示等级图像的代码。

我们在check_bullet_alien_collisions() 中提高等级,并更新等级图像:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship, aliens, bullets): —snip—

if len(aliens) == 0:

如果整群外星人都被消灭,就提高一个等级

bullets.empty()

ai_settings.increase_speed()

提高等级

❶ stats.level += 1

❷ sb.prep_level()

create_fleet(ai_settings, screen, ship, aliens)

如果整群外星人都被消灭,我们就将stats.level 的值加1(见❶),并调用prep_level() ,以确保正确地显示新等级(见❷)。

为确保开始新游戏时更新记分和等级图像,在按钮Play被单击时触发重置:

game_functions.py

def check_play_button(ai_settings, screen, stats, sb, play_button, ship, aliens, bullets, mouse_x, mouse_y): """在玩家单击Play按钮时开始新游戏"""

button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

if button_clicked and not stats.game_active:

—snip—

重置游戏统计信息

stats.reset_stats()

stats.game_active = True

重置记分牌图像

❶ sb.prep_score()

sb.prep_high_score()

sb.prep_level()

清空外星人列表和子弹列表

aliens.empty()

bullets.empty()

—snip—

check_play_button() 的定义需要包含对象sb 。为重置记分牌图像,我们在重置相关游戏设置后调用prep_score() 、prep_high_score() 和prep_level() (见

❶)。

在check_events() 中,现在需要向check_play_button() 传递sb ,让它能够访问记分牌对象:

game_functions.py

def check_events(ai_settings, screen, stats, sb, play_button, ship, aliens, bullets): """响应按键和鼠标事件"""

for event in pygame.event.get():

if event.type == pygame.QUIT:

—snip—

elif event.type == pygame.MOUSEBUTTONDOWN:

mouse_x, mouse_y = pygame.mouse.get_pos()

❶ check_play_button(ai_settings, screen, stats, sb, play_button,

ship, aliens, bullets, mouse_x, mouse_y)

check_events() 的定义需要包含形参sb ,这样调用check_play_button() 时,才能将sb 作为实参传递给它(见❶)。

最后,更新alien_invasion.py中调用check_events() 的代码,也向它传递sb :

alien_invasion.py

开始游戏主循环

while True:

gf.check_events(ai_settings, screen, stats, sb, play_button, ship,

aliens, bullets)

—snip—

现在你可以知道升到多少级了,如图14-5所示。

06 - 图10

图14-5  当前等级显示在当前得分的正下方

注意  在一些经典游戏中,得分带标签,如Score、High Score和Level。我们没有显示这些标签,因为开始玩这款游戏后,每个数字的含义将一目了然。要包含这些标

签,只需在Scoreboard 中调用font.render() 前,将它们添加到得分字符串中即可。

14.3.9  显示余下的飞船数

最后,我们来显示玩家还有多少艘飞船,但使用图形而不是数字。为此,我们在屏幕左上角绘制飞船图像来指出还余下多少艘飞船,就像众多经典的街机游戏那样。

首先,需要让Ship 继承Sprite ,以便能够创建飞船编组:

ship.py

import pygame

from pygame.sprite import Sprite

❶ class Ship(Sprite):

def init(self, ai_settings, screen):

"""初始化飞船,并设置其起始位置"""

❷ super(Ship, self).init()

—snip—

在这里,我们导入了Sprite ,让Ship 继承Sprite (见❶),并在init() 的开头就调用了super() (见❷)。

接下来,需要修改Scoreboard ,在其中创建一个可供显示的飞船编组。下面是其中的import 语句和方法init() :

scoreboard.py

import pygame.font

from pygame.sprite import Group

from ship import Ship

class Scoreboard():

"""报告得分信息的类"""

def init(self, ai_settings, screen, stats):

—snip—

self.prep_level()

self.prep_ships()

—snip—

鉴于要创建一个飞船编组,我们导入Group 和Ship 类。调用prep_level() 后,我们调用了prep_ships() 。

prep_ships() 的代码如下:

scoreboard.py

def prep_ships(self):

"""显示还余下多少艘飞船"""

❶ self.ships = Group()

❷ for ship_number in range(self.stats.ships_left):

ship = Ship(self.ai_settings, self.screen)

❸ ship.rect.x = 10 + ship_number * ship.rect.width

❹ ship.rect.y = 10

❺ self.ships.add(ship)

方法prep_ships() 创建一个空编组self.ships ,用于存储飞船实例(见❶)。为填充这个编组,根据玩家还有多少艘飞船运行一个循环相应的次数(见❷)。在这个循环

中,我们创建一艘新飞船,并设置其 x 坐标,让整个飞船编组都位于屏幕左边,且每艘飞船的左边距都为10像素(见❸)。我们还将 y 坐标设置为离屏幕上边缘10像素,让所有飞

船都与得分图像对齐(见❹)。最后,我们将每艘新飞船都添加到编组ships 中(见❺)。

现在需要在屏幕上绘制飞船了:

scoreboard.py

def show_score(self):

—snip—

self.screen.blit(self.level_image, self.level_rect)

绘制飞船

self.ships.draw(self.screen)

为在屏幕上显示飞船,我们对编组调用了draw() 。Pygame将绘制每艘飞船。

为在游戏开始时让玩家知道他有多少艘飞船,我们在开始新游戏时调用prep_ships() 。这是在game_functions.py的check_play_button() 中进行的:

game_functions.py

def check_play_button(ai_settings, screen, stats, sb, play_button, ship, aliens, bullets, mouse_x, mouse_y): """在玩家单击Play按钮时开始新游戏"""

button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

if button_clicked and not stats.game_active:

—snip—

重置记分牌图像

sb.prep_score()

sb.prep_high_score()

sb.prep_level()

sb.prep_ships()

—snip—

我们还在飞船被外星人撞到时调用prep_ships() ,从而在玩家损失一艘飞船时更新飞船图像:

game_functions.py

❶ def update_aliens(ai_settings, screen, stats, sb, ship, aliens, bullets): —snip—

检测外星人和飞船之间的碰撞

if pygame.sprite.spritecollideany(ship, aliens):

❷ ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets)

检查是否有外星人抵达屏幕底端

❸ check_aliens_bottom(ai_settings, screen, stats, sb, ship, aliens, bullets) ❹ def ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets): """响应被外星人撞到的飞船"""

if stats.ships_left > 0:

将ships_left减1

stats.ships_left -= 1

更新记分牌

❺ sb.prep_ships()

清空外星人列表和子弹列表

—snip—

首先,我们在update_aliens() 的定义中添加了形参sb (见❶)。然后,我们向ship_hit() (见❷)和check_aliens_bottom() (见❸)都传递了sb ,让它们都能

够访问记分牌对象。

接下来,我们更新了ship_hit() 的定义,使其包含形参sb (见❹)。我们在将ships_left 的值减1后调用了prep_ships() (见❺),这样每次损失了飞船时,显示的

飞船数都是正确的。

在check_aliens_bottom() 中需要调用ship_hit() ,因此对这个函数进行更新:

game_functions.py

def check_aliens_bottom(ai_settings, screen, stats, sb, ship, aliens,

bullets):

"""检查是否有外星人抵达屏幕底端"""

screen_rect = screen.get_rect()

for alien in aliens.sprites():

if alien.rect.bottom >= screen_rect.bottom:

像飞船被外星人撞到一样处理

ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets)

break

现在,check_aliens_bottom() 包含形参sb ,并在调用ship_hit() 时传递了实参sb 。

最后,在alien_invasion.py中修改调用update_aliens() 的代码,向它传递实参sb :

alien_invasion.py

开始游戏主循环

while True:

—snip—

if stats.game_active:

ship.update()

gf.update_bullets(ai_settings, screen, stats, sb, ship, aliens,

bullets)

gf.update_aliens(ai_settings, screen, stats, sb, ship, aliens,

bullets)

—snip—

图14-6显示了完整的记分系统,它在屏幕左上角指出了还余下多少艘飞船。

06 - 图11

图14-6  游戏《外星人入侵》的完整记分系统

动手试一试

14-4 历史最高分 :每当玩家关闭并重新开始游戏《外星人入侵》时,最高分都将被重置。请修复这个问题,调用sys.exit() 前将最高分写入文件,并当

在GameStats 中初始化最高分时从文件中读取它。

14-5 重构 :找出执行了多项任务的函数和方法,对它们进行重构,以让代码高效而有序。例如,对于check_bullet_alien_collisions() ,将其中在外星人

群被消灭干净时开始新等级的代码移到一个名为startnewlevel() 的函数中;又比如,对于Scoreboard 的方法__init() ,将其中调用四个不同方法的

代码移到一个名为prepimages() 的方法中,以缩短方法_init() 。如果你重构了check_play_button() ,方法prep_images() 也可

为check_play_button() 或start_game() 提供帮助。

注意  重构项目前,请阅读附录D,了解如果重构时引入了bug,如何将项目恢复到可正确运行的状态。

14-6 扩展游戏《外星人入侵》 :想想如何扩展游戏《外星人入侵》。例如,可让外星人也能够向飞船射击,或者添加盾牌,让飞船躲到它后面,使得只有从两边射

来的子弹才能摧毁飞船。另外,还可以使用像pygame.mixer 这样的模块来添加音效,如爆炸声和射击声。

14.4  小结

在本章中,你学习了如何创建用于开始新游戏的Play按钮,如何检测鼠标事件,以及在游戏处于活动状态时如何隐藏光标。你可以利用学到的知识在游戏中创建其他按钮,如用于

显示玩法说明的Help按钮。你还学习了如何随游戏的进行调整其节奏,如何实现记分系统,以及如何以文本和非文本方式显示信息。

项目2  数据可视化

第 15 章 生成数据

06 - 图12

数据可视化 指的是通过可视化表示来探索数据,它与数据挖掘 紧密相关,而数据挖掘指的是使用代码来探索数据集的规律和关联。数据集可以是用一行代码就能表

示的小型数字列表,也可以是数以吉字节的数据。

漂亮地呈现数据关乎的并非仅仅是漂亮的图片。以引人注目的简洁方式呈现数据,让观看者能够明白其含义,发现数据集中原本未意识到的规律和意义。

所幸即便没有超级计算机,也能够可视化复杂的数据。鉴于Python的高效性,使用它在笔记本电脑上就能快速地探索由数百万个数据点组成的数据集。数据点并非必须

是数字,利用本书前半部分介绍的基本知识,也可以对非数字数据进行分析。

在基因研究、天气研究、政治经济分析等众多领域,大家都使用Python来完成数据密集型工作。数据科学家使用Python编写了一系列令人印象深刻的可视化和分析工

具,其中很多也可供你使用。最流行的工具之一是matplotlib,它是一个数学绘图库,我们将使用它来制作简单的图表,如折线图和散点图。然后,我们将基于随机漫

步概念生成一个更有趣的数据集——根据一系列随机决策生成的图表。

我们还将使用Pygal包,它专注于生成适合在数字设备上显示的图表。通过使用Pygal,可在用户与图表交互时突出元素以及调整其大小,还可轻松地调整整个图表的尺

寸,使其适合在微型智能手表或巨型显示器上显示。我们将使用Pygal以各种方式探索掷骰子的结果。

15.1  安装matplotlib

首先,需要安装matplotlib,我们将使用它来制作开始的几个图表。如果你还未使用过pip,请参阅12.2.1节。

15.1.1  在Linux 系统中安装matplotlib 如果你使用的是系统自带的Python版本,可使用系统的包管理器来安装matplotlib,为此只需执行一行命令:

$ sudo apt-get install python3-matplotlib

如果你使用的是Python 2.7,请执行如下命令:

$ sudo apt-get install python-matplotlib

如果你安装了较新的Python版本,就必须安装matplotlib依赖的一些库:

$ sudo apt-get install python3.5-dev python3.5-tk tk-dev

$ sudo apt-get install libfreetype6-dev g++

再使用pip来安装matplotlib:

$ pip install —user matplotlib

15.1.2  在OS X 系统中安装matplotlib Apple的标准Python安装自带了matplotlib。要检查系统是否安装了matplotlib,可打开一个终端会话并尝试导入matplotlib。如果系统没有自带matplotlib,且你的Python是使用Homebrew安

装的,则可以像下面这样安装matplotlib:

$ pip install —user matplotlib

06 - 图13

注意  安装包时可能需要使用pip3 ,而不是pip 。另外,如果这个命令不管用,你可能需要删除标志—user 。

15.1.3  在Windows 系统中安装matplotlib 在Windows系统中,首先需要安装Visual Studio。为此,请访问https://dev.windows.com/ ,单击Downloads,再查找Visual Studio Community——一组免费的Windows开发工具。请下载并

运行该安装程序。

接下来,需要下载matplotlib安装程序。为此,请访问https://pypi.python.org/pypi/matplotlib/ ,并查找与你使用的Python版本匹配的wheel文件(扩展名为.whl的文件)。例如,如果你使

用的是32位的Python 3.5,则需要下载matplotlib-1.4.3-cp35-none-win32.whl。

注意  如果找不到与你安装的Python版本匹配的文件,请去http://www.lfd.uci.edu/-gohlke/pythonlibs/#matplotlib 看看,这个网站发布安装程序的时间通常比matplotlib官网早

些。

将这个.whl文件复制到你的项目文件夹,打开一个命令窗口,并切换到该项目文件夹,再使用pip来安装matplotlib:

> cd python_work

python_work> python -m pip install —user matplotlib-1.4.3-cp35-none-win32.whl 15.1.4  测试matplotlib 安装必要的包后,对安装进行测试。为此,首先使用命令python 或python3 启动一个终端会话,再尝试导入matplotlib:

$ python3

>>> import matplotlib

>>>

如果没有出现任何错误消息,就说明你的系统安装了matplotlib,可以接着阅读下一节。

注意  如果你在安装过程中遇到了麻烦,请参阅附录C。如果依然无济于事,请向他人寻求帮助。对于你遇到的问题,只要向经验丰富的Python程序员提供少量的信

息,他们很可能很快就能帮你解决。

15.1.5  matplotlib 画廊

要查看使用matplotlib可制作的各种图表,请访问http://matplotlib.org/ 的示例画廊。单击画廊中的图表,就可查看用于生成图表的代码。

15.2  绘制简单的折线图

下面来使用matplotlib绘制一个简单的折线图,再对其进行定制,以实现信息更丰富的数据可视化。我们将使用平方数序列1、4、9、16和25来绘制这个图表。

只需向matplotlib提供如下数字,matplotlib就能完成其他的工作:

mpl_squares.py

import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]

plt.plot(squares)

plt.show()

我们首先导入了模块pyplot ,并给它指定了别名plt ,以免反复输入pyplot 。在线示例大都这样做,因此这里也这样做。模块pyplot 包含很多用于生成图表的函数。

我们创建了一个列表,在其中存储了前述平方数,再将这个列表传递给函数plot() ,这个函数尝试根据这些数字绘制出有意义的图形。plt.show() 打开matplotlib查看器,并

显示绘制的图形,如图15-1所示。查看器让你能够缩放和导航图形,另外,单击磁盘图标可将图形保存起来。

图15-1  使用matplotlib 可制作的最简单的图表

15.2.1  修改标签文字和线条粗细

图15-1所示的图形表明数字是越来越大的,但标签文字太小,线条太细。所幸matplotlib让你能够调整可视化的各个方面。

06 - 图14

下面通过一些定制来改善这个图形的可读性,如下所示:

mpl_squares.py

import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]

❶ plt.plot(squares, linewidth=5)

设置图表标题,并给坐标轴加上标签

❷ plt.title("Square Numbers", fontsize=24)

❷ plt.xlabel("Value", fontsize=14)

plt.ylabel("Square of Value", fontsize=14)

设置刻度标记的大小

❹ plt.tick_params(axis='both', labelsize=14)

plt.show()

参数linewidth (见❶)决定了plot() 绘制的线条的粗细。函数title() (见❷)给图表指定标题。在上述代码中,出现了多次的参数fontsize 指定了图表中文字的大

小。

函数xlabel() 和ylabel() 让你能够为每条轴设置标题(见❸);而函数tick_params() 设置刻度的样式(见❹),其中指定的实参将影响 x 轴和 y 轴上的刻度

(axes='both' ),并将刻度标记的字号设置为14(labelsize=14 )。

最终的图表阅读起来容易得多了,如图15-2所示:标签文字更大,线条也更粗。

图15-2  现在图表阅读起来容易得多

15.2.2  校正图形

图形更容易阅读后,我们发现没有正确地绘制数据:折线图的终点指出4.0的平方为25!下面来修复这个问题。

当你向plot() 提供一系列数字时,它假设第一个数据点对应的 x 坐标值为0,但我们的第一个点对应的 x 值为1。为改变这种默认行为,我们可以给plot() 同时提供输入值和

输出值:

mpl_squares.py

import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5]

squares = [1, 4, 9, 16, 25]

plt.plot(input_values, squares, linewidth=5)

设置图表标题并给坐标轴加上标签

—snip—

现在plot() 将正确地绘制数据,因为我们同时提供了输入值和输出值,它无需对输出值的生成方式作出假设。最终的图形是正确的,如图15-3所示。

06 - 图15

06 - 图16

图15-3  根据数据正确地绘制了图形

使用plot() 时可指定各种实参,还可使用众多函数对图形进行定制。本章后面处理更有趣的数据集时,将继续探索这些定制函数。

15.2.3  使用scatter() 绘制散点图并设置其样式

有时候,需要绘制散点图并设置各个数据点的样式。例如,你可能想以一种颜色显示较小的值,而用另一种颜色显示较大的值。绘制大型数据集时,你还可以对每个点都设置同

样的样式,再使用不同的样式选项重新绘制某些点,以突出它们。

要绘制单个点,可使用函数scatter() ,并向它传递一对 x 和 y 坐标,它将在指定位置绘制一个点:

scatter_squares.py

import matplotlib.pyplot as plt

plt.scatter(2, 4)

plt.show()

下面来设置输出的样式,使其更有趣:添加标题,给轴加上标签,并确保所有文本都大到能够看清:

import matplotlib.pyplot as plt

❶ plt.scatter(2, 4, s=200)

设置图表标题并给坐标轴加上标签

plt.title("Square Numbers", fontsize=24)

plt.xlabel("Value", fontsize=14)

plt.ylabel("Square of Value", fontsize=14)

设置刻度标记的大小

plt.tick_params(axis='both', which='major', labelsize=14)

plt.show()

在❶处,我们调用了scatter() ,并使用实参s 设置了绘制图形时使用的点的尺寸。如果此时运行scatter_squares.py,将在图表中央看到一个点,如图15-4所示。

图15-4  绘制单个点

15.2.4  使用scatter() 绘制一系列点

要绘制一系列的点,可向scatter() 传递两个分别包含 x 值和 y 值的列表,如下所示:

scatter_squares.py

import matplotlib.pyplot as plt

06 - 图17

06 - 图18

x_values = [1, 2, 3, 4, 5]

y_values = [1, 4, 9, 16, 25]

plt.scatter(x_values, y_values, s=100)

设置图表标题并给坐标轴指定标签

—snip—

列表x_values 包含要计算其平方值的数字,而列表y_values 包含前述每个数字的平方值。将这些列表传递给scatter() 时,matplotlib依次从每个列表中读取一个值来绘制

一个点。要绘制的点的坐标分别为 (1, 1)、(2, 4)、(3, 9)、(4, 16)和(5, 25),最终的结果如图15-5所示。

图15-5  由多个点组成的散点图

15.2.5  自动计算数据

手工计算列表要包含的值可能效率低下,需要绘制的点很多时尤其如此。可以不必手工计算包含点坐标的列表,而让Python循环来替我们完成这种计算。下面是绘制1000个点的代

码:

scatter_squares.py

import matplotlib.pyplot as plt

❶ x_values = list(range(1, 1001))

y_values = [x**2 for x in x_values]

❷ plt.scatter(x_values, y_values, s=40)

设置图表标题并给坐标轴加上标签

—snip—

设置每个坐标轴的取值范围

❸ plt.axis([0, 1100, 0, 1100000])

plt.show()

我们首先创建了一个包含 x 值的列表,其中包含数字1~1000(见❶)。接下来是一个生成 y 值的列表解析,它遍历 x 值(for x in x_values ),计算其平方值(x**2 ),

并将结果存储到列表y_values 中。然后,将输入列表和输出列表传递给scatter() (见❷)。

由于这个数据集较大,我们将点设置得较小,并使用函数axis() 指定了每个坐标轴的取值范围(见❸)。函数axis() 要求提供四个值: x 和 y 坐标轴的最小值和最大值。在

这里,我们将 x 坐标轴的取值范围设置为0~1100,并将 y 坐标轴的取值范围设置为0~1 100 000。结果如图15-6所示。

图15-6  Python 绘制1000 个点与绘制5 个点一样容易

15.2.6  删除数据点的轮廓

06 - 图19

matplotlib允许你给散点图中的各个点指定颜色。默认为蓝色点和黑色轮廓,在散点图包含的数据点不多时效果很好。但绘制很多点时,黑色轮廓可能会粘连在一起。要删除数据

点的轮廓,可在调用scatter() 时传递实参edgecolor='none' :

plt.scatter(x_values, y_values, edgecolor='none', s=40)

将相应调用修改为上述代码后,如果再运行scatter_squares.py,在图表中看到的将是蓝色实心点。

15.2.7  自定义颜色

要修改数据点的颜色,可向scatter() 传递参数c ,并将其设置为要使用的颜色的名称,如下所示:

plt.scatter(x_values, y_values, c='red', edgecolor='none', s=40)

你还可以使用RGB颜色模式自定义颜色。要指定自定义颜色,可传递参数c ,并将其设置为一个元组,其中包含三个0~1之间的小数值,它们分别表示红色、绿色和蓝色分量。例

如,下面的代码行创建一个由淡蓝色点组成的散点图:

plt.scatter(x_values, y_values, c=(0, 0, 0.8), edgecolor='none', s=40) 值越接近0,指定的颜色越深,值越接近1,指定的颜色越浅。

15.2.8  使用颜色映射

颜色映射 (colormap)是一系列颜色,它们从起始颜色渐变到结束颜色。在可视化中,颜色映射用于突出数据的规律,例如,你可能用较浅的颜色来显示较小的值,并使用较深

的颜色来显示较大的值。

模块pyplot 内置了一组颜色映射。要使用这些颜色映射,你需要告诉pyplot 该如何设置数据集中每个点的颜色。下面演示了如何根据每个点的 y 值来设置其颜色:

scatter_squares.py

import matplotlib.pyplot as plt

x_values = list(range(1001))

y_values = [x**2 for x in x_values]

plt.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Blues,

edgecolor='none', s=40)

设置图表标题并给坐标轴加上标签

—snip—

我们将参数c 设置成了一个 y 值列表,并使用参数cmap 告诉pyplot 使用哪个颜色映射。这些代码将 y 值较小的点显示为浅蓝色,并将 y 值较大的点显示为深蓝色,生成的图形

如图15-7所示。

图15-7  使用颜色映射Blues 的图表

注意  要了解pyplot 中所有的颜色映射,请访问http://matplotlib.org/ ,单击Examples,向下滚动到Color Examples,再单击colormaps_reference。

15.2.9  自动保存图表

要让程序自动将图表保存到文件中,可将对plt.show() 的调用替换为对plt.savefig() 的调用:

plt.savefig('squares_plot.png', bbox_inches='tight')

第一个实参指定要以什么样的文件名保存图表,这个文件将存储到scatter_squares.py所在的目录中;第二个实参指定将图表多余的空白区域裁剪掉。如果要保留图表周围多余的空

白区域,可省略这个实参。

动手试一试

15-1 立方 :数字的三次方被称为其立方。请绘制一个图形,显示前5个整数的立方值,再绘制一个图形,显示前5000个整数的立方值。

15-2 彩色立方 :给你前面绘制的立方图指定颜色映射。

15.3  随机漫步

在本节中,我们将使用Python来生成随机漫步数据,再使用matplotlib以引人瞩目的方式将这些数据呈现出来。随机漫步 是这样行走得到的路径:每次行走都完全是随机的,没有

明确的方向,结果是由一系列随机决策决定的。你可以这样认为,随机漫步就是蚂蚁在晕头转向的情况下,每次都沿随机的方向前行所经过的路径。

在自然界、物理学、生物学、化学和经济领域,随机漫步都有其实际用途。例如,漂浮在水滴上的花粉因不断受到水分子的挤压而在水面上移动。水滴中的分子运动是随机的,

因此花粉在水面上的运动路径犹如随机漫步。我们稍后将编写的代码模拟了现实世界的很多情形。

15.3.1  创建RandomWalk() 类

为模拟随机漫步,我们将创建一个名为RandomWalk 的类,它随机地选择前进方向。这个类需要三个属性,其中一个是存储随机漫步次数的变量,其他两个是列表,分别存储随

机漫步经过的每个点的 x 和 y 坐标。

RandomWalk 类只包含两个方法:init() 和fillwalk() ,其中后者计算随机漫步经过的所有点。下面先来看看_init() ,如下所示:

random_walk.py

❶ from random import choice

class RandomWalk():

"""一个生成随机漫步数据的类"""

❷ def init(self, num_points=5000):

"""初始化随机漫步的属性"""

self.num_points = num_points

所有随机漫步都始于(0, 0)

❸ self.x_values = [0]

self.y_values = [0]

为做出随机决策,我们将所有可能的选择都存储在一个列表中,并在每次做决策时都使用choice() 来决定使用哪种选择(见❶)。接下来,我们将随机漫步包含的默认点数设

置为5000,这大到足以生成有趣的模式,同时又足够小,可确保能够快速地模拟随机漫步(见❷)。然后,在❸处,我们创建了两个用于存储 x 和 y 值的列表,并让每次漫步都从

点(0, 0)出发。

15.3.2  选择方向

我们将使用fill_walk() 来生成漫步包含的点,并决定每次漫步的方向,如下所示。请将这个方法添加到random_walk.py中:

random_walk.py

def fill_walk(self):

"""计算随机漫步包含的所有点"""

不断漫步,直到列表达到指定的长度

❶ while len(self.x_values) < self.num_points:

决定前进方向以及沿这个方向前进的距离

❷ x_direction = choice([1, -1])

x_distance = choice([0, 1, 2, 3, 4])

❸ x_step = x_direction * x_distance

y_direction = choice([1, -1])

y_distance = choice([0, 1, 2, 3, 4])

❹ y_step = y_direction * y_distance

拒绝原地踏步

❺ if x_step == 0 and y_step == 0:

continue

计算下一个点的x和y值

❻ next_x = self.x_values[-1] + x_step

next_y = self.y_values[-1] + y_step

self.x_values.append(next_x)

self.y_values.append(next_y)

在❶处,我们建立了一个循环,这个循环不断运行,直到漫步包含所需数量的点。这个方法的主要部分告诉Python如何模拟四种漫步决定:向右走还是向左走?沿指定的方向走多

远?向上走还是向下走?沿选定的方向走多远?

我们使用choice([1, -1]) 给x_direction 选择一个值,结果要么是表示向右走的1,要么是表示向左走的-1(见❷)。接下来,choice([0, 1, 2, 3, 4]) 随机地

选择一个0~4之间的整数,告诉Python 沿指定的方向走多远(x_distance )。(通过包含0,我们不仅能够沿两个轴移动,还能够沿 y 轴移动。)

在❸和❹处,我们将移动方向乘以移动距离,以确定沿 x 和 y 轴移动的距离。如果x_step 为正,将向右移动,为负将向左移动,而为零将垂直移动;如果y_step 为正,就意

味着向上移动,为负意味着向下移动,而为零意味着水平移动。如果x_step 和y_step 都为零,则意味着原地踏步,我们拒绝这样的情况,接着执行下一次循环(见❺)。

为获取漫步中下一个点的 x 值,我们将x_step 与x_values 中的最后一个值相加(见❻),对于 y 值也做相同的处理。获得下一个点的 x 值和 y 值后,我们将它们分别附加到

列表x_values 和y_values 的末尾。

15.3.3  绘制随机漫步图

下面的代码将随机漫步的所有点都绘制出来:

rw_visual.py

import matplotlib.pyplot as plt

from random_walk import RandomWalk

创建一个RandomWalk实例,并将其包含的点都绘制出来

❶ rw = RandomWalk()

rw.fill_walk()

❷ plt.scatter(rw.x_values, rw.y_values, s=15)

plt.show()

06 - 图20

我们首先导入了模块pyplot 和RandomWalk 类,然后创建了一个RandomWalk 实例,并将其存储到rw 中(见❶),再调用fill_walk() 。在❷处,我们将随机漫步包含的

x 和 y 值传递给scatter() ,并选择了合适的点尺寸。图15-8显示了包含5000个点的随机漫步图(本节的示意图未包含matplotlib查看器部分,但你运行rw_visual.py时,依然会看

到)。

图15-8  包含5000 个点的随机漫步

15.3.4  模拟多次随机漫步

每次随机漫步都不同,因此探索可能生成的各种模式很有趣。要在不多次运行程序的情况下使用前面的代码模拟多次随机漫步,一种办法是将这些代码放在一个while 循环中,

如下所示:

rw_visual.py

import matplotlib.pyplot as plt

from random_walk import RandomWalk

只要程序处于活动状态,就不断地模拟随机漫步

while True:

创建一个RandomWalk实例,并将其包含的点都绘制出来

rw = RandomWalk()

rw.fill_walk()

plt.scatter(rw.x_values, rw.y_values, s=15)

plt.show()

❶ keep_running = input("Make another walk? (y/n): ")

if keep_running == 'n':

break

这些代码模拟一次随机漫步,在matplotlib查看器中显示结果,再在不关闭查看器的情况下暂停。如果你关闭查看器,程序将询问你是否要再模拟一次随机漫步。如果你输入y ,可

模拟多次随机漫步:这些随机漫步都在起点附近进行,大多沿特定方向偏离起点,漫步点分布不均匀等。要结束程序,请输入n 。

注意  如果你使用的是Python 2.7,别忘了将❶处的input() 替换为raw_input() 。

15.3.5  设置随机漫步图的样式

在本节中,我们将定制图表,以突出每次漫步的重要特征,并让分散注意力的元素不那么显眼。为此,我们确定要突出的元素,如漫步的起点、终点和经过的路径。接下来确定

要使其不那么显眼的元素,如刻度标记和标签。最终的结果是简单的可视化表示,清楚地指出了每次漫步经过的路径。

15.3.6  给点着色

我们将使用颜色映射来指出漫步中各点的先后顺序,并删除每个点的黑色轮廓,让它们的颜色更明显。为根据漫步中各点的先后顺序进行着色,我们传递参数c ,并将其设置为

一个列表,其中包含各点的先后顺序。由于这些点是按顺序绘制的,因此给参数c 指定的列表只需包含数字1~5000,如下所示:

rw_visual.py

—snip—

while True:

创建一个RandomWalk实例,并将其包含的点都绘制出来

rw = RandomWalk()

rw.fill_walk()

❶ point_numbers = list(range(rw.num_points))

plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, edgecolor='none', s=15) plt.show()

keep_running = input("Make another walk? (y/n): ")

—snip—

在❶处,我们使用了range() 生成了一个数字列表,其中包含的数字个数与漫步包含的点数相同。接下来,我们将这个列表存储在point_numbers 中,以便后面使用它来设

置每个漫步点的颜色。我们将参数c 设置为point_numbers ,指定使用颜色映射Blues ,并传递实参edgecolor=none 以删除每个点周围的轮廓。最终的随机漫步图从浅蓝

色渐变为深蓝色,如图15-9所示。

06 - 图21

图15-9  使用颜色映射Blues 着色的随机漫步图

15.3.7  重新绘制起点和终点

除了给随机漫步的各个点着色,以指出它们的先后顺序外,如果还能呈现随机漫步的起点和终点就更好了。为此,可在绘制随机漫步图后重新绘制起点和终点。我们让起点和终

点变得更大,并显示为不同的颜色,以突出它们,如下所示:

rw_visual.py

—snip—

while True:

—snip—

plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, edgecolor='none', s=15) # 突出起点和终点

plt.scatter(0, 0, c='green', edgecolors='none', s=100)

plt.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none', s=100) plt.show()

—snip—

为突出起点,我们使用绿色绘制点(0, 0),并使其比其他点大(s=100 )。为突出终点,我们在漫步包含的最后一个 x 和 y 值处绘制一个点,将其颜色设置为红色,并将尺寸设置

为100。请务必将这些代码放在调用plt.show() 的代码前面,确保在其他点的上面绘制起点和终点。

如果你现在运行这些代码,将能准确地知道每次随机漫步的起点和终点(如果起点和终点不明显,请调整它们的颜色和大小,直到明显为止)。

15.3.8  隐藏坐标轴

下面来隐藏这个图表中的坐标轴,以免我们注意的是坐标轴而不是随机漫步路径。要隐藏坐标轴,可使用如下代码:

rw_visual.py

—snip—

while True:

—snip—

plt.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none', s=100) # 隐藏坐标轴

❶ plt.axes().get_xaxis().set_visible(False)

plt.axes().get_yaxis().set_visible(False)

plt.show()

—snip—

为修改坐标轴,使用了函数plt.axes() (见❶)来将每条坐标轴的可见性都设置为False 。随着你越来越多地进行数据可视化,经常会看到这种串接方法的方式。

如果你现在运行rw_visual.py,将看到一系列图形,但看不到坐标轴。

15.3.9  增加点数

下面来增加点数,以提供更多的数据。为此,我们在创建RandomWalk 实例时增大num_points 的值,并在绘图时调整每个点的大小,如下所示:

rw_visual.py

—snip—

while True:

创建一个RandomWalk实例,并将其包含的点都绘制出来

rw = RandomWalk(50000)

rw.fill_walk()

绘制点并将图形显示出来

point_numbers = list(range(rw.num_points))

plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, edgecolor='none', s=1) —snip—

06 - 图22

这个示例模拟了一次包含50 000个点的随机漫步(以模拟现实情况),并将每个点的大小都设置为1。最终的随机漫步图更纤细,犹如云朵,如图15-10所示。正如你看到的,我们

使用简单的散点图制作出了一件艺术品!

请尝试修改上述代码,看看将漫步包含的点数增加到多少后,程序的运行速度变得极其缓慢或绘制出的图形变得很难看。

图15-10  包含50 000 个点的随机漫步

15.3.10  调整尺寸以适合屏幕

图表适合屏幕大小时,更能有效地将数据中的规律呈现出来。为让绘图窗口更适合屏幕大小,可像下面这样调整matplotlib输出的尺寸:

rw_visual.py

—snip—

while True:

创建一个RandomWalk实例,并将其包含的点都绘制出来

rw = RandomWalk()

rw.fill_walk()

设置绘图窗口的尺寸

plt.figure(figsize=(10, 6))

—snip—

函数figure() 用于指定图表的宽度、高度、分辨率和背景色。你需要给形参figsize 指定一个元组,向matplotlib指出绘图窗口的尺寸,单位为英寸。

Python假定屏幕分辨率为80像素/英寸,如果上述代码指定的图表尺寸不合适,可根据需要调整其中的数字。如果你知道自己的系统的分辨率,可使用形参dpi 向figure() 传递

该分辨率,以有效地利用可用的屏幕空间,如下所示:

plt.figure(dpi=128, figsize=(10, 6))

动手试一试

15-3 分子运动 :修改rw_visual.py,将其中的plt.scatter() 替换为plt.plot() 。为模拟花粉在水滴表面的运动路径,向plt.plot() 传递rw.x_values 和rw.y_values ,并指定实参值linewidth 。使用5000个点而不是50 000个点。

15-4 改进的随机漫步 :在类RandomWalk 中,x_step 和y_step 是根据相同的条件生成的:从列表[1, -1] 中随机地选择方向,并从列表[0, 1, 2, 3, 4]