1.回顾项目
开发较大的项目时,进入每个开发阶段前回顾一下开发计划,搞清楚接下来要通过编写代码来完成哪些任务都是不错的主意。本章涉及以下内容。
- 研究既有代码,确定实现新功能前是否要进行重构。
- 在屏幕左上角添加一个外星人,并指定合适的边距。
- 根据第一个外星人的边距和屏幕尺寸计算屏幕上可容纳多少个外星人。我们将编写一个循环来创建一系列外星人,这些外星人填满了屏幕的上半部分。
- 让外星人群向两边和下方移动,直到外星人被全部击落,有外星人撞到飞船,或有外星人抵达屏幕底端。如果整群外星人都被击落,我们将再创建一群外星人。如果有外星人撞到了飞船或抵达屏幕底端,我们将销毁飞船并再创建一群外星人。
- 限制玩家可用的飞船数量,配给的飞船用完后,游戏结束。
我们将在实现功能的同时完善这个计划,但就目前而言,该计划已足够详尽。在给项目添加新功能前,还应审核既有代码。每进入一个新阶段,通常项目都会更复杂,因此最好对混乱或低效的代码进行清理。我们在开发的同时一直不断地重构,因此当前需要做的清理工作不多,但每次为测试新功能而运行这个游戏时,都必须使用鼠标来关闭它,这太讨厌了。下面来添加一个结束游戏的快捷键Q:
game_functions.py
def check_keydown_events(event, ai_settings, screen, ship, bullets):
-- snip --
elif event.key == pygame.K_q:
sys.exit()
在 check_keydown_events() 中,我们添加了一个代码块,以便在玩家按Q时结束游戏。这样的修改很安全,因为Q键离箭头键和空格键很远,玩家不小心按Q键而导致游戏结束的可能性不大。现在测试时可按Q关闭游戏,而无需使用鼠标来关闭窗口了。
2.创建第一个外星人
在屏幕上放置外星人与放置飞船类似。每个外星人的行为都由 Alien 类控制,我们将像创建Ship 类那样创建这个类。出于简化考虑,我们也使用位图来表示外星人。你可以自己寻找表示外星 人 的 图 像 , 也 可 使 用 如图 所 示 的 图 像 , 这幅图像的背景为灰色,与屏幕背景色一致。请务必将你选择的图像文件保存到文件夹images中。
2.1创建 Alien 类
下面来编写 Alien 类:
alien.py
import pygame
from pygame.sprite import Sprite
class Alien(Sprite):
"""表示单个外星人的类"""
def __int__(self,ai_settings,screen):
"""初始化外星人并设置其起始位置"""
super(Alien,self).__init__()
self.screen = screen
self.ai_settings = ai_settings
#加載外星人圖像,并设置其rect属性
self.image = pygame.image.load('images/alien.bmp')
self.rect = self.image.get_rect()
#每个外星人最初都在屏幕左上角附件
self.rect.x = self.rect.width
self.rect.y = self.rect.height
#存储外星人的准确位置
self.rect.x = self.rect.width
self.rect.y = self.rect.height
#存储外星人的准确位置
self.x=float(self.rect.x)
def blitme(self):
"""在指定位置绘制外星人"""
self.screen.blit(self.image,self.rect)
除位置不同外,这个类的大部分代码都与 Ship 类相似。每个外星人最初都位于屏幕左上角附近,我们将每个外星人的左边距都设置为外星人的宽度,并将上边距设置为外星人的高度(见1)。
2.2 创建 Alien 实例
下面在alien_invasion.py中创建一个 Alien 实例:
alien_invasion.py
#alien_invasion.py
import pygame
from pygame.sprite import Group
from settings import Settings
from ship import Ship
from alien import Alien
import game_functions as gf
def run_game():
#初始化pygame、设置和屏幕对象
pygame.init() #1
ai_settings = Settings()
screen = pygame.display.set_mode(
(ai_settings.screen_width, ai_settings.screen_height))
pygame.display.set_caption("Alien Invasion")
#创建一艘飞船
ship = Ship(ai_settings,screen)
bullets = Group()
#创建一个外星人
alien = Alien(ai_settings,screen)
#开始游戏的主循环
while True:
gf.check_events(ai_settings, screen, ship, bullets) #1
ship.update() #2
#删除已消失的子弹 #3
gf.update_bullets(bullets)
gf.update_screen(ai_settings, screen, ship,alien,bullets) #4
run_game()
在这里,我们导入了新创建的 Alien 类,并在进入主 while 循环前创建了一个 Alien 实例。我们没有修改外星人的位置,因此该 while 循环没有任何新东西,但我们修改了对 update_screen() 的调用,传递了一个外星人实例。
2.3 让外星人出现在屏幕上
为让外星人出现在屏幕上,我们在 update_screen() 中调用其方法 blitme() :
game_functions.py
def update_screen(ai_settings, screen, ship, alien, bullets):
-- snip --
# 在飞船和外星人后面重绘所有的子弹
for bullet in bullets:
bullet.draw_bullet()
ship.blitme()
alien.blitme()
# 让最近绘制的屏幕可见
pygame.display.flip()
我们先绘制飞船和子弹,再绘制外星人,让外星人在屏幕上位于最前面。如图显示了屏幕上的第一个外星人。
3.创建一群外星人
要绘制一群外星人,需要确定一行能容纳多少个外星人以及要绘制多少行外星人。我们将首先计算外星人之间的水平间距,并创建一行外星人,再确定可用的垂直空间,并创建整群外星人。
3.1 确定一行可容纳多少个外星人
为确定一行可容纳多少个外星人,我们来看看可用的水平空间有多大。屏幕宽度存储在ai_settings.screen_width 中,但需要在屏幕两边都留下一定的边距,把它设置为外星人的宽度。由于有两个边距,因此可用于放置外星人的水平空间为屏幕宽度减去外星人宽度的两倍:
available_space_x = ai_settings.screen_width – (2 * alien_width)
我们还需要在外星人之间留出一定的空间,即外星人宽度。因此,显示一个外星人所需的水平空间为外星人宽度的两倍:一个宽度用于放置外星人,另一个宽度为外星人右边的空白区域。为确定一行可容纳多少个外星人,我们将可用空间除以外星人宽度的两倍:
number_aliens_x = available_space_x / (2 * alien_width)
我们将在创建外星人群时使用这些公式。
注意 令人欣慰的是,在程序中执行计算时,一开始你无需确定公式是正确的,而可以尝试直
接运行程序,看看结果是否符合预期。即便是在最糟糕的情况下,也只是屏幕上显示的
外星人太多或太少。你可以根据在屏幕上看到的情况调整计算公式。
3.2 创建多行外星人
为创建一行外星人,首先在alien_invasion.py中创建一个名为 aliens 的空编组,用于存储全部外星人,再调用game_functions.py中创建外星人群的函数:
alien_invasion.py
#alien_invasion.py
import pygame
from pygame.sprite import Group
from settings import Settings
from ship import Ship
import game_functions as gf
def run_game():
#初始化pygame、设置和屏幕对象
pygame.init() #1
ai_settings = Settings()
screen = pygame.display.set_mode(
(ai_settings.screen_width, ai_settings.screen_height))
pygame.display.set_caption("Alien Invasion")
#创建一艘飞船
ship = Ship(ai_settings,screen)
bullets = Group()
aliens = Group() #1
#创建外星人群
gf.create_fleet(ai_settings,screen,aliens) #2
#创建一个外星人
alien = Alien(ai_settings,screen)
#开始游戏的主循环
while True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
#删除已消失的子弹
gf.update_bullets(bullets)
gf.update_screen(ai_settings, screen, ship, aliens, bullets) #3
run_game()
由于我们不再在alien_invasion.py中直接创建外星人,因此无需在这个文件中导入 Alien 类。1 处创建了一个空编组,用于存储所有的外星人。接下来,调用稍后将编写的函数create_fleet() (见2),并将 ai_settings 、对象 screen 和空编组 aliens 传递给它。然后,修改对
update_screen() 的调用,让它能够访问外星人编组(见3)。我们还需要修改 update_screen() :
game_functions.py
def update_screen(ai_settings, screen, ship, aliens, bullets):
-- snip --
ship.blitme()
aliens.draw(screen)
# 让最近绘制的屏幕可见
pygame.display.flip()
对编组调用 draw() 时,Pygame自动绘制编组的每个元素,绘制位置由元素的属性 rect 决定。在这里, aliens.draw(screen) 在屏幕上绘制编组中的每个外星人。
3.3 创建外星人群
现在可以创建外星人群了。下面是新函数 create_fleet() ,我们将它放在 game_functions.py 的末尾。我们还需要导入 Alien 类,因此务必在文件game_functions.py开头添加相应的 import语句:
game_functions.py
-- snip --
from bullet import Bullet
from alien import Alien
-- snip --
def create_fleet(ai_settings, screen, aliens):
"""创建外星人群"""
# 创建一个外星人,并计算一行可容纳多少个外星人
# 外星人间距为外星人宽度
alien = Alien(ai_settings, screen) #1
alien_width = alien.rect.width #2
available_space_x = ai_settings.screen_width - 2 * alien_width #3
number_aliens_x = int(available_space_x / (2 * alien_width)) #4
# 创建第一行外星人
for alien_number in range(number_aliens_x): #5
# 创建一个外星人并将其加入当前行
alien = Alien(ai_settings, screen) #6
alien.x = alien_width + 2 * alien_width * alien_number
alien.rect.x = alien.x
aliens.add(alien)
这些代码大都在前面详细介绍过。为放置外星人,我们需要知道外星人的宽度和高度,因此在执行计算前,我们先创建一个外星人(见1)。这个外星人不是外星人群的成员,因此没有将它加入到编组 aliens 中。在2处,我们从外星人的 rect 属性中获取外星人宽度,并将这个值存储
到 alien_width 中,以免反复访问属性 rect 。在3处,我们计算可用于放置外星人的水平空间,以及其中可容纳多少个外星人。相比于前面介绍的工作,这里唯一的不同是使用了 int() 来确保计算得到的外星人数量为整数(见4),因为我们不希望某个外星人只显示一部分,而且函数 range() 也需要一个整数。函数int() 将小数部分丢弃,相当于向下圆整(这大有裨益,因为我们宁愿每行都多出一点点空间,也不希望每行的外星人之间过于拥挤)。接下来,我们编写了一个循环,它从零数到要创建的外星人数(见5)。在这个循环的主体中,我们创建一个新的外星人,并通过设置x坐标将其加入当前行(见6)。将每个外星人都往右推一个外星人的宽度。接下来,我们将外星人宽度乘以2,得到每个外星人占据的空间(其中包括其右边的空白区域),再据此计算当前外星人在当前行的位置。最后,我们将每个新创建的外星人都添加到编组 aliens 中。
如果你现在运行这个游戏,将看到第一行外星人,如图所示。
这行外星人在屏幕上稍微偏向了左边,这实际上是有好处的,因为我们将让外星人群往右移,触及屏幕边缘后稍微往下移,然后往左移,以此类推。就像经典游戏《太空入侵者》,相比于只往下移,这种移动方式更有趣。我们将让外形人群不断这样移动,直到所有外星人都被击落或有外星人撞上飞船或抵达屏幕底端。
注意 根据你选择的屏幕宽度,在你的系统中,第一行外星人的位置可能稍有不同。
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): #1
"""计算每行可容纳多少个外星人"""
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 #2
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) #3
# 创建第一行外星人
for alien_number in range(number_aliens_x):
# 创建一个外星人并将其加入当前行
create_alien(ai_settings, screen, aliens, alien_number) #4
函数 get_number_aliens_x() 的代码都来自 create_fleet() ,且未做任何修改(见1)。函数create_alien() 的代码也都来自 create_fleet() ,且未做任何修改,只是使用刚创建的外星人来获取外星人宽度(见2)。在3处,我们将计算可用水平空间的代码替换为对 get_number_aliens_x()的调用,并删除了引用 alien_width 的代码行,因为现在这是在 create_alien() 中处理的。在4处, 我们调用 create_alien() 。通过这样的重构,添加新行进而创建整群外星人将更容易。
3.5 添加行
要创建外星人群,需要计算屏幕可容纳多少行,并对创建一行外星人的循环重复相应的次数。为计算可容纳的行数,我们这样计算可用垂直空间:将屏幕高度减去第一行外星人的上边距(外星人高度)、飞船的高度以及最初外星人群与飞船的距离(外星人高度的两倍):
available_space_y = ai_settings.screen_height – 3 * alien_height – ship_height
这将在飞船上方留出一定的空白区域,给玩家留出射杀外星人的时间。每行下方都要留出一定的空白区域,并将其设置为外星人的高度。为计算可容纳的行数,我们将可用垂直空间除以外星人高度的两倍(同样,如果这样的计算不对,我们马上就能发现,继而将间距调整为合理的值)。
number_rows = available_height_y / (2 * alien_height)
知道可容纳多少行后,便可重复执行创建一行外星人的代码:
game_functions.py
def get_number_rows(ai_settings, ship_height, alien_height): #1
"""计算屏幕可容纳多少行外星人"""
available_space_y = (ai_settings.screen_height - #2
(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):
"""创建一个外星人并将其放在当前行"""
alien = Alien(ai_settings, screen)
alien_width = alien.rect.width
alien.x = alien_width + 2 * alien_width * alien_number
alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number #3
alien.rect.x = alien.x
aliens.add(alien)
def create_fleet(ai_settings, screen, ship, aliens):
"""创建外星人群"""
# 创建一个外星人,并计算一行可容纳多少个外星人
# 外星人间距为外星人宽度
alien = Alien(ai_settings, screen)
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): #4
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 的公式(见1)这个函数与 get_number_aliens_x() 类似。计算公式用括号括起来了,这样可将代码分成两行,以遵循每行不超过79字符的建议(见2)。这里使用了 int() ,因为我们不想创建不完整的外星人行。为创建多行,我们使用两个嵌套在一起的循环:一个外部循环和一个内部循环(见3)。其中的内部循环创建一行外星人,而外部循环从零数到要创建的外星人行数。Python将重复执行创建单行外星人的代码,重复次数为 number_rows 。为嵌套循环,我们编写了一个新的 for 循环,并缩进了要重复执行的代码。(在大多数文本编辑器中,缩进代码块和取消缩进都很容易,详情请参阅附录B。)我们调用 create_alien() 时,传递了一个表示行号的实参,将每行都沿屏幕依次向下放置。create_alien() 的定义需要一个用于存储行号的形参。在 create_alien() 中,我们修改外星人的y坐标(见4),并在第一行外星人上方留出与外星人等高的空白区域。相邻外星人行的y坐标相差外星人高度的两倍,因此我们将外星人高度乘以2,再乘以行号。第一行的行号为0,因此第一行的垂直位置不变,而其他行都沿屏幕依次向下放置。在 create_fleet() 的 定 义 中 , 还 新 增 了 一 个 用 于 存 储 ship 对 象 的 形 参 , 因 此 在alien_invasion.py 中调用 create_fleet() 时,需要传递实参 ship :
alien_invasion.py
# 创建外星人群
gf.create_fleet(ai_settings, screen, ship, aliens)
如果你现在运行这个游戏,将看到一群外星人,如图所示。
4.让外星人群移动
下面来让外星人群在屏幕上向右移动,撞到屏幕边缘后下移一定的距离,再沿相反的方向移动。我们将不断地移动所有的外星人,直到所有外星人都被消灭,有外星人撞上飞船,或有外星人抵达屏幕底端。下面先来让外星人向右移动。
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 #1
self.rect.x = self.x #2
每次更新外星人位置时,都将它向右移动,移动量为 alien_speed_factor 的值。我们使用属性 self.x 跟踪每个外星人的准确位置,这个属性可存储小数值(见1)。然后,我们使用 self.x的值来更新外星人的 rect 的位置(见2)。
在主 while 循环中已调用了更新飞船和子弹的方法,但现在还需更新每个外星人的位置:alien_invasion.py
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() 。如果你现在运行这个游戏,会看到外星人群向右移,并逐渐在屏幕右边缘消失。
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坐标,使用数字来表示方向更合理。
4.3 检查外星人是否撞到了屏幕边缘
现在需要编写一个方法来检查是否有外星人撞到了屏幕边缘,还需修改 update() ,以让每个外星人都沿正确的方向移动:
alien.py
def check_edges(self):
"""如果外星人位于屏幕边缘,就返回True"""
screen_rect = self.screen.get_rect()
if self.rect.right >= screen_rect.right: #1
return True
elif self.rect.left <= 0: #2
return True
def update(self):
"""向左或向右移动外星人"""
self.x += (self.ai_settings.alien_speed_factor * #3
self.ai_settings.fleet_direction)
self.rect.x = self.x
我们可对任何外星人调用新方法 check_edges() ,看看它是否位于屏幕左边缘或右边缘。如果外星人的 rect 的 right 属性大于或等于屏幕的 rect 的 right 属性,就说明外星人位于屏幕右边缘(见1)。如果外星人的 rect 的 left 属性小于或等于0,就说明外星人位于屏幕左边缘(见2)。我们修改了方法 update() ,将移动量设置为外星人速度和 fleet_direction 的乘积,让外星人向左或向右移。如果 fleet_direction 为1,就将外星人当前的x坐标增大 alien_speed_factor ,从而将外星人向右移;如果 fleet_direction 为1,就将外星人当前的x坐标减去 alien_speed_factor ,从而将外星人向左移。
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(): #1
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 #2
ai_settings.fleet_direction *= -1
def update_aliens(ai_settings, aliens):
"""
检查是否有外星人位于屏幕边缘,并更新整群外星人的位置
"""
check_fleet_edges(ai_settings, aliens) #3
aliens.update()
在 check_fleet_edges() 中,我们遍历外星人群,并对其中的每个外星人调用 check_edges()(见1)。如果 check_edges() 返回 True ,我们就知道相应的外星人位于屏幕边缘,需要改变外星人群的方向,因此我们调用 change_fleet_direction() 并退出循环。在 change_fleet_direction() 中,
我们遍历所有外星人,将每个外星人下移 fleet_drop_speed 设置的值(见2);然后,将 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)
如果你现在运行这个游戏,外星人群将在屏幕上来回移动,并在抵达屏幕边缘后向下移动。现在可以开始射杀外星人,检查是否有外星人撞到飞船,或抵达了屏幕底端。
补充:内容来源 《python编程:从入门到实践》
本文只做学习记录使用,其中部分代码出现纰漏,可参照原文或源代码
这里附上相关资源(包括原文,原代码,及本人最后修订代码):https://download.csdn.net/download/idealhunting/11012497