武装飞船
开始游戏项目
创建Pygame窗口以及响应用户输入
首先,我们创建一个空的Pygame窗口。使用Pygame编写的游戏的基本结构如下:
#alien_invasion.py
import sys
import pygame
def run_game():
# 初始化游戏并创建一个屏幕对象
pygame.init()
screen = pygame.display.set_mode((1200, 800))#注意这里是元组
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窗口。
设置背景色
Pygame默认创建一个黑色屏幕,这太乏味了。下面来将背景设置为另一种颜色:
#alien_invasion.py
--snip--
pygame.display.set_caption("Alien Invasion")
bg_color = (230, 230, 230)
# 开始游戏的主循环
while True:
# 监视键盘和鼠标事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# 每次循环时都重绘屏幕
screen.fill(bg_color)
--snip--
创建设置类
下面是最初的Settings 类:
#settings.py
class Settings():
def __init__(self):
"""初始化游戏设置"""
self.screen_width = 1200
self.screen_height = 800
self.bg_color = (230, 230, 230)
为创建Settings 实例并使用它来访问设置,将alien_invasion.py修改成下面这样:
#alien_invasion.py
from settings import Settings
--snip--
ai_settings = Settings()
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)
--snip--
添加飞船图像
创建Ship 类
选择用于表示飞船的图像后,需要将其显示到屏幕上。我们将创建一个名为ship 的模块,其中包含Ship 类,它负责管理飞船的大部分行为。
#ship.py
import pygame
class Ship():
def __init__(self, screen):
"""初始化飞船并设置初始位置"""
self.screen = screen
# 加载飞船图像并获取其外接矩阵
self.image = pygame.image.load('images/ship.bmp')
self.rect = self.image.get_rect()
self.screen_rect = 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)
首先,我们导入了模块pygame 。Ship 的方法__init__() 接受两个参数:引用self 和screen ,其中后者指定了要将飞船绘制到什么地方。为加载图像,我们调用 了pygame.image.load() 。这个函数返回一个表示飞船的surface,而我们将这个surface存储到了self.image 中。
要将游戏元素居中,可设置相应rect 对象的属性center 、centerx 或centery 。要让游戏元素与屏幕边缘对齐,可使用属性top 、bottom 、left 或right ;要调整游 戏元素的水平或垂直位置,可使用属性x 和y ,它们分别是相应矩形左上角的 x 和 y 坐标。
最后,我们定义了方法blitme() ,它根据self.rect 指定的位置将图像绘制到屏幕上。
在屏幕上绘制飞船
下面来更新alien_invasion.py,使其创建一艘飞船,并调用其方法blitme():
#alien_invasion.py
from ship import Ship
--snip--
pygame.display.set_caption("Alien Invasion")
# 创建一艘飞船
ship = Ship(screen)
# 开始游戏的主循环
while True:
# 监视键盘和鼠标事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# 每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
ship.blitme()
# 让最近绘制的屏幕可见
pygame.display.flip()
--snip--
重构:模块game_functions
函数check_events()
将check_events() 放在一个名为game_functions 的模块中:
#game_functions.py
import sys
import pygame
def check_events():
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
下面来修改alien_invasion.py,使其导入模块game_functions ,并将事件循环替换为对函数check_events() 的调用:
#alien_invasion.py
import game_functions as gf
--snip--
while True:
# 监视键盘和鼠标事件
gf.check_events()
# 每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
--snip--
在主程序文件中,不再需要直接导入sys ,因为当前只在模块game_functions 中使用了它。出于简化的目的,我们给导入的模块game_functions 指定了别名gf 。
函数update_screen()
为进一步简化run_game() ,下面将更新屏幕的代码移到一个名为update_screen() 的函数中,并将这个函数放在模块game_functions.py 中:
#game_functions.py
--snip--
def update_screen(ai_settings, screen, ship):
# 每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
ship.blitme()
pygame.display.flip()
新函数update_screen() 包含三个形参:ai_settings 、screen 和ship 。现在需要将alien_invasion.py的while 循环中更新屏幕的代码替换为对函数update_screen() 的调用:
#alien_invasion.py
--snip--
while True:
# 监视键盘和鼠标事件
gf.check_events()
# 每次循环时都重绘屏幕
gf.update_screen(ai_settings, screen, ship)
--snip--
这两个函数让while 循环更简单,并让后续开发更容易:在模块game_functions 而不是run_game() 中完成大部分工作。
驾驶飞船
响应按键
每当用户按键时,都将在Pygame中注册一个事件。事件都是通过方法pygame.event.get() 获取的,因此在函数check_events() 中,我们需要指定要检查哪些类型的事 件。每次按键都被注册为一个KEYDOWN 事件。
#game_functions.py
--snip--
def check_events(ship):
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
#向右移动飞船
ship.rect.centerx +=1
--snip--
在alien_invasion.py中,我们需要更新调用的check_events() 代码,将ship 作为实参传递给它:
#alien_invasion.py
--snip--
while True:
# 监视键盘和鼠标事件
gf.check_events(ship)
# 每次循环时都重绘屏幕
gf.update_screen()
--snip--
如果现在运行alien_invasion.py,则每按右箭头键一次,飞船都将向右移动1像素。这是一个开端,但并非控制飞船的高效方式。下面来改进控制方式,允许持续移动。
允许不断移动
#ship.py
--snip--
self.rect.centerx = self.screen_rect.centerx
self.rect.bottom = self.screen_rect.bottom
self.moving_right = False
def update(self):
if self.moving_right:
self.rect.centerx +=1
--snip--
在方法__init__() 中,我们添加了属性self.moving_right ,并将其初始值设置为False 。接下来,我们添加了方法update() ,它在前述标志为True 时向 右移动飞船。 下面来修改check_events() ,使其在玩家按下右箭头键时将moving_right 设置为True ,并在玩家松开时将moving_right 设置为False :
#game_functions.py
--snip--
def check_events(ship):
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit(0)
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
--snip--
最后,我们需要修改alien_invasion.py 中的while 循环,以便每次执行循环时都调用飞船的方法update() :
#alien_invasion.py
--snip--
while True:
# 监视键盘和鼠标事件
gf.check_events(ship)
ship.update()
# 每次循环时都重绘屏幕
gf.update_screen(ai_settings, screen, ship)
--snip--
左右移动
飞船能够不断地向右移动后,添加向左移动的逻辑很容易。我们将再次修改Ship 类和函数check_events() 。下面显示了对Ship 类的方法__init__() 和update() 所做 的相关修改:
#ship.py
--snip--
self.moving_right = False
self.moving_left = False
def update(self):
if self.moving_right:
self.rect.centerx +=1
if self.moving_left:
self.rect.centerx -=1
--snip--
我们还需对check_events() 作两方面的调整:
#game_functions.py
--snip--
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
ship.moving_right = True
if event.key == pygame.K_LEFT:
ship.moving_left = True
elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT:
ship.moving_right = False
if event.key == pygame.K_LEFT:
ship.moving_left = False
--snip--
调整飞船的速度
当前,每次执行while 循环时,飞船最多移动1像素,但我们可以在Settings 类中添加属性ship_speed_factor ,用于控制飞船的速度。我们将根据这个属性决定飞船在每次循环时最多移动多少距离。
#settings.py
class Settings():
def __init__(self):
"""初始化游戏设置"""
self.screen_width = 1200
self.screen_height = 800
self.bg_color = (230, 230, 230)
self.ship_speed_factor = 1.5
通过将速度设置指定为小数值,可在后面加快游戏的节奏时更细致地控制飞船的速度。然而,rect 的centerx 等属性只能存储整数值,因此我们需要对Ship 类做些修改:
#ship.py
class Ship():
def __init__(self, ai_settings, screen):
"""初始化飞船并设置初始位置"""
self.screen = screen
self.ai_settings = ai_settings
--snip--
self.center = float(self.rect.centerx)
self.moving_right = False
self.moving_left = False
def update(self):
if self.moving_right:
self.center +=self.ai_settings.ship_speed_factor
if self.moving_left:
self.rect.centerx -=self.ai_settings.ship_speed_factor
# 根据self.center更新rect对象
self.rect.centerx = self.center
--snip--
鉴于现在调整飞船的位置时,将增加或减去一个单位为像素的小数值,因此需要将位置存储在一个能够存储小数值的变量中。可以使用小数来 设置rect 的属性,但rect 将只存储这个值的整数部分。为准确地存储飞船的位置,我们定义了一个可存储小数值的新属性self.center。我们使用函数float() 将self.rect.centerx 的值转换为小数,并将结果存储到self.center 中。 现在在update() 中调整飞船的位置时,将self.center 的值增加或减去ai_settings.ship_speed_factor 的值。更新self.center 后,我们再根据它来 更新控制飞船位置的self.rect.centerx 。self.rect.centerx 将只存储self.center 的整数部分,但对显示飞船而言,这问题不大。
在alien_invasion.py中创建Ship 实例时,需要传入实参ai_settings :
#alien_invasion.py
--snip--
# 创建一艘飞船
ship = Ship(ai_settings,screen)
# 开始游戏的主循环
while True:
--snip--
限制飞船的活动范围
当前,如果玩家按住箭头键的时间足够长,飞船将移到屏幕外面,消失得无影无踪。下面来修复这种问题,让飞船到达屏幕边缘后停止移动。为此,我们将修改Ship 类的方 法update() :
#ship.py
--snip--
def update(self):
if self.moving_right and self.rect.right < self.screen_rect.right:
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
--snip--
重构check_events()
随着游戏开发的进行,函数check_events() 将越来越长,我们将其部分代码放在两个函数中:一个处理KEYDOWN 事件,另一个处理KEYUP 事件:
#game_functions.py
--snip--
def check_events(ship):
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit(0)
elif event.type == pygame.KEYDOWN:
check_keydown_events(event, ship)
elif event.type == pygame.KEYUP:
check_keyup_events(event, ship)
def check_keydown_events(event, ship):
if event.key == pygame.K_RIGHT:
ship.moving_right = True
if event.key == pygame.K_LEFT:
ship.moving_left = True
def check_keyup_events(event, ship):
if event.key == pygame.K_RIGHT:
ship.moving_right = False
if event.key == pygame.K_LEFT:
ship.moving_left = False
--snip--
射击
添加子弹设置
首先,更新settings.py
#settings.py
class Settings():
def __init__(self):
--snip--
self.bullet_speed_factor = 1
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = (60, 60, 60)
创建Bullet 类
下面来创建存储Bullet 类的文件bullet.py,其前半部分如下:
#bullet.py
import pygame
from pygame.sprite import Sprite
class Bullet(Sprite):
"""创建一个子弹类"""
def __init__(self, ai_settings, screen, ship):
super().__init__()
self.screen = screen
self.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_factor = ai_settings.bullet_speed_factor
Bullet 类继承了我们从模块pygame.sprite 中导入的Sprite 类。通过使用精灵,可将游戏中相关的元素编组,进而同时操作编组中的所有元素。
下面是bullet.py的第二部分——方法update() 和draw_bullet() :
#bullet.py
def update(self):
"""更新子弹位置"""
self.y -= self.speed_factor#将子弹速度存为小数,方便后期更改,因为self.rect.y无法存储小数
self.rect.y = self.y
def draw_bullet(self):
"""绘制子弹"""
pygame.draw.rect(self.screen, self.color, self.rect)
将子弹存储到编组中
定义Bullet 类和必要的设置后,就可以编写代码了,在玩家每次按空格键时都射出一发子弹。首先,我们将在alien_invasion.py中创建一个编组(group),用于存储所有有效的子 弹,以便能够管理发射出去的所有子弹。这个编组将是pygame.sprite.Group 类的一个实例;pygame.sprite.Group 类类似于列表,但提供了有助于开发游戏的额外功 能。在主循环中,我们将使用这个编组在屏幕上绘制子弹,以及更新每颗子弹的位置:
#alien_invasion.py
--snip--
from pygame.sprite import Group
def run_game():
--snip--
bullets = Group()
# 开始游戏的主循环
while True:
# 监视键盘和鼠标事件
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
bullets.update()
# 每次循环时都重绘屏幕
gf.update_screen(ai_settings, screen, ship, bullets)
--snip--
当你对编组调用update() 时,编组将自动对其中的每个精灵调用update() ,因此代码行bullets.update() 将为编组bullets 中的每颗子弹调用bullet.update() 。
开火
在game_functions.py中,我们需要修改check_keydown_events() ,以便在玩家按空格键时发射一颗子弹。我们无需修改check_keyup_events() ,因为玩家松开空格键 时什么都不会发生。我们还需修改update_screen() ,确保在调用flip() 前在屏幕上重绘每颗子弹。
#game_functions.py
import sys
import pygame
from bullet import Bullet
--snip--
def check_keydown_events(event, ai_settings, screen, ship, bullets):
if event.key == pygame.K_RIGHT:
ship.moving_right = True
if event.key == pygame.K_LEFT:
ship.moving_left = True
if event.key == pygame.K_SPACE:
new_bullet = Bullet(ai_settings, screen, ship)
bullets.add(new_bullet)
--snip--
def update_screen(ai_settings, screen, ship, bullets):
# 每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
ship.blitme()
for bullet in bullets.sprites():#疑问:可不可以写成bullets.draw_bullet()
bullet.draw_bullet()
pygame.display.flip()
编组bulltes 传递给了check_keydown_events()。玩家按空格键时,创建一颗新子弹(一个名为new_bullet 的Bullet 实例),并使用方法add() 将其加入 到编组bullets 中;代码bullets.add(new_bullet) 将新子弹存储到编组bullets 中。
删除已消失的子弹
#alien_invasion.py
--snip--
def run_game():
--snip--
#删除已消失的子弹
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
# 每次循环时都重绘屏幕
gf.update_screen(ai_settings, screen, ship, bullets)
--snip--
限制子弹数量
首先,在settings.py中存储所允许的最大子弹数:
#settings.py
self.bullets_allowed = 3
这将未消失的子弹数限制为3颗。在game_functions.py的check_keydown_events() 中,我们在创建新子弹前检查未消失的子弹数是否小于该设置:
#game_functions.py
if event.key == pygame.K_SPACE:
if len(bullets) < ai_settings.bullets_allowed:
new_bullet = Bullet(ai_settings, screen, ship)
bullets.add(new_bullet)
创建函数update_bullets()
编写并检查子弹管理代码后,可将其移到模块game_functions 中,以让主程序文件alien_invasion.py尽可能简单。我们创建一个名为update_bullets() 的新函数,并将其添 加到game_functions.py的末尾:
#game_functions.py
def update_bullets(bullets):
#更新子弹位置
bullets.update()
#删除已消失的子弹
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
alien_invasion.py中的while 循环又变得很简单了:
#alien_invasion.py
while True:
# 监视键盘和鼠标事件
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
gf.update_bullets(bullets)
# 每次循环时都重绘屏幕
gf.update_screen(ai_settings, screen, ship, bullets)
创建函数fire_bullet()
下面将发射子弹的代码移到一个独立的函数中,这样,在check_keydown_events() 中只需使用一行代码来发射子弹
#game_functions.py
--snip--
def check_keydown_events(event, ai_settings, screen, ship, bullets):
--snip--
if event.key == pygame.K_SPACE:
fire_bullet(ai_settings, screen, ship, bullets)
--snip--
def fire_bullet(ai_settings, screen, ship, bullets):
if len(bullets) < ai_settings.bullets_allowed:
new_bullet = Bullet(ai_settings, screen, ship)
bullets.add(new_bullet)