目录
> 武装飞船
> 开始游戏项目
> 创建pygame窗口以及相应用户输入
>> 初始化程序
>> 创建surface对象
>> 事件监听
>> 游戏循环
> 设置背景色
> 创建设置类
> 添加飞船图像
> 创建ship类
>> pygame.image
>> get_rect( )
>> surface.blit( )
> 在屏幕上绘制飞船
> 重构:模块game_functions
> 函数 check_events( )
> 函数 update_screen()
> 驾驶飞船
> 响应按键
> 允许不断移动
> 左右移动
> 调整飞船的速度
> 限制飞船的活动范围
> 重构check_events()
> 简单回顾
> alien_invasion.py
> settings.py
> game_functions.py
> ship.py
> 射击
> 添加子弹设置
> 创建Bullet类
> 将子弹存储到编组中
> 开火
> 删除已消失的子弹
> 限制子弹数量
> 创建函数update_bullets( )
> 创建函数 fire_bullet( )
> 快捷结束游戏
> 外星人
> 创建第一个外星人
> 创建Alien类
> 创建Alien实例
> 让外星人出现在屏幕上
> 创建一群外星人
> 确定一行可容纳多少个外星人
> 创建多行外星人
> 创建外星人群
> 重构create_fleet( )
> 添加行
> 让外星人群移动
> 向右移动外星人
> 创建表示外星人移动方向的设置
> 检查外星人是否撞到了屏幕边缘
> 向下移动外星人群并改变移动方向
> 射杀外星人
> 检测子弹与外星人的碰撞
> 为测试创建大子弹
> 生成新的外星人群
> 提高子弹的速度
> 重构update_bullets( )
> 结束游戏
> 检测外星人和飞船碰撞
> 响应外星人和飞船碰撞
> 有外星人到达屏幕底端
> 游戏结束
> 确定应运行游戏的哪些部分
> 记分
> 添加Play按钮
> 创建Botton类
> 在屏幕上绘制按钮
> 开始游戏
> 重置游戏
> 将Play按钮切换到非活动状态
> 隐藏光标
> 提高等级
> 修改速度设置
> 重置速度
> 记分
> 显示得分
> 创建记分牌
> 在外星人被消灭时更新得分
> 将消灭的每个外星人的点数都计入得分
> 提高点数
> 将得分圆整
> 最高得分
> 显示等级
> 显示余下的飞船数
> End
> 武装飞船
> 开始游戏项目
> 创建pygame窗口以及相应用户输入
首先,我们创建一个空的pygame窗口。
# alien_invasion.py
import sys
import pygame
def run_game():
# 初始化游戏并创建一个屏幕文件
pygame.init()
screen = pygame.display.set_mode((900, 600)) # 指定窗口尺寸
pygame.display.set_caption("Alien Invasion") # 设置窗口标题
# 开始游戏的主循环
while True:
# 监视键盘和鼠标事件
for event in pygame.event.get(): # 访问python检测到的事件
if event.type == pygame.QUIT: # pygame.QUIT 指点击右上角窗口的"X"号
sys.exit() # 点击后,卸载所有pygame模块
# 让最近绘制的屏幕可见
pygame.display.flip()
run_game() # 初始化游戏并开始主循环
此时运行程序,会发现窗口已建立(其默认色为黑):
>> 初始化程序
在使用 Pygame 编写程序前,要做的第一个步骤是“初始化程序”,让Python能正确工作:
pygame.init()
>> 创建surface对象
在Pygame中,surface是屏幕的一部分,用于显示游戏元素。
screen = pygame.display.set_mode((1200, 800)) # 指定窗口尺寸
对象screen是一个surface。在这个游戏中,每个元素(如外星人、飞船)都是一个surface。display.set_mode() 返回的surface表示整个游戏窗口。
>> 事件监听
pygame提供了一个event事件模块,其包含了常用到的游戏事件。
# 循环获取事件,监听事件状态,使用get()获取事件
for event in pygame.event.get():
# 判断事件类型,用户是否点了"X"关闭按钮
# pygame.QUIT 指点击右上角窗口的"X"号
if event.type == pygame.QUIT:
# 点击后,卸载所有pygame模块
pygame.quit()
下面列出了常用event事件类型和事件方法:
事件类型 | 描述 | 成员属性 |
---|---|---|
QUIT | 用户按下窗口的关闭按钮 | none |
ATIVEEVENT | Pygame被激活或者隐藏 | gain,state |
KEYDOWN | 键盘按下 | unicode、key、mod |
KEYUP | 键盘放开 | key、mod |
MOUSEMOTION | 鼠标移动 | pos, rel, buttons |
MOUSEBUTTONDOWN | 鼠标按下 | pos, button |
MOUSEBUTTONUP | 鼠标放开 | pos, button |
JOYAXISMOTION | 游戏手柄(Joystick or pad) 移动 | joy, axis, value |
JOYBALLMOTION | 游戏球(Joy ball) 移动 | joy, axis, value |
JOYHATMOTION | 游戏手柄(Joystick) 移动 | joy, axis, value |
JOYBUTTONDOWN | 游戏手柄按下 | joy, button |
JOYBUTTONUP | 游戏手柄放开 | joy, button |
VIDEORESIZE | Pygame窗口缩放 | size, w, h |
VIDEOEXPOSE | Pygame窗口部分公开(expose) | none |
USEREVENT | 触发一个用户事件 | 事件代码 |
键盘事件常量名 | 描述 |
---|---|
K_BACKSPACE | 退格键(Backspace) |
K_TAB | 制表键(Tab) |
K_CLEAR | 清除键(Clear) |
K_RETURN | 回车键(Enter) |
K_PAUSE | 暂停键(Pause) |
K_ESCAPE | 退出键(Escape) |
K_SPACE | 空格键(Space) |
K_0...K_9 | 0...9 |
K_a...Kz | a...z |
K_DELETE | 删除键(delete) |
K_KP0...K_KP9 | 0(小键盘)...9(小键盘) |
K_F1...K_F15 | F1...F15 |
K_UP | 向上箭头(up arrow) |
K_DOWN | 向下箭头(down arrow) |
K_RIGHT | 向右箭头(right arrow) |
K_LEFT | 向左箭头(left arrow) |
KMOD_ALT | 同时按下Alt键 |
方法 | 说明 |
---|---|
pygame.event.get() | 从事件队列中获取一个事件,并从队列中删除该事件 |
pygame.event.wait() | 阻塞直至事件发生才会继续执行,若没有事件发生将一直处于阻塞状态 |
pygame.event.set_blocked() | 控制哪些事件禁止进入队列,如果参数值为None,则表示禁止所有事件进入 |
pygame.event.set_allowed() | 控制哪些事件允许进入队列 |
pygame.event.pump() | 调用该方法后,Pygame 会自动处理事件队列 |
pygame.event.poll() | 会根据实际情形返回一个真实的事件,或者一个None |
pygame.event.peek() | 检测某类型事件是否在队列中 |
pygame.event.clear() | 从队列中清除所有的事件 |
pygame.event.get_blocked() | 检测某一类型的事件是否被禁止进入队列 |
pygame.event.post() | 放置一个新的事件到队列中 |
pygame.event.Event() | 创建一个用户自定义的新事件 |
>> 游戏循环
这个游戏由while循环控制,其中包含一个事件循环以及管理屏幕更新的代码。
# 开始游戏的主循环
while True:
# 监视键盘和鼠标事件
for event in pygame.event.get(): # 访问python检测到的事件
if event.type == pygame.QUIT: # pygame.QUIT 指点击右上角窗口的"X"号
sys.exit() # 点击后,卸载所有pygame模块
# 让最近绘制的屏幕可见
pygame.display.flip()
游戏画面和操作状态会因动画效果和玩家操作而改变,因此需要以循环的方式实时更新screen显示内容,下面提供了两种方法:
pygame.display.flip() # 法1
pygame.display.update() # 法2
- 前者更新整个待显示的内容
- 后者可以根据选定的区域来部分更新内容(若无提供位置参数,则其作用与前者同)
> 设置背景色
Pygame默认创建一个黑色屏幕,我们可以将背景设置为另一种颜色。
# alien_invasion.py
--snip--
def run_game():
--snip--
pygame.display.set_caption("Alien Invasion")
bg_color = (230,230,230) # 设置背景色
# 开始游戏的主循环
while True:
--snip--
# 每次循环时都重绘屏幕
screen.fill(bg_color)
# 让最近绘制的屏幕可见
pygame.display.flip()
run_game() # 初始化游戏并开始主循环
在pygame中,颜色是以RGB值指定的。这种颜色由红色、绿色和蓝色值组成,每个值的取值范围都为0~255。(255,0,0)表示红色,(0,255,0)表示绿色,(0,0,255)表示蓝色。
在本例中,我们创建了一种背景色,并将其存储在bg_color中。紧接着调用方法screen.fill(),用背景色填充屏幕。这个方法只接受一个实参:一种颜色。
bg_color = (230,230,230) # 设置背景色
pygame.Surface.fill(bg_color) # 使用纯色填充 Surface 对象
> 创建设置类
编写一个settings模块, 其中包含一个名为Settings的类,用于将所有设置存储在一个地方。 这样,我们就只用传递一个设置对象,要修改游戏,只需修改settings.py中的一些值即可。
# settings.py
class Settings:
"""存储《外星人入侵》的所有设置的类"""
def __init__(self):
"""初始化游戏的设置"""
# 屏幕设置
self.screen_width = 900
self.screen_height = 600
self.bg_color = (230, 230, 230)
为创建Settings实例并使用它来访问设置,将alien_invasion.py修改如下:
# alien_invasion.py
import sys
import pygame
from settings import Settings
def run_game():
# 初始化游戏并创建一个屏幕文件
pygame.init()
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(): # 访问python检测到的事件
if event.type == pygame.QUIT: # pygame.QUIT 指点击右上角窗口的"X"号
sys.exit() # 点击后,卸载所有pygame模块
# 每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
# 让最近绘制的屏幕可见
pygame.display.flip()
run_game() # 初始化游戏并开始主循环
使用方式如上:在主程序文件中,导入了Settings类。调用pygame.init(),再创建一个Settings实例,并将其存储再变量ai_settings中。创建屏幕时,直接使用ai_settings的属性。
> 添加飞船图像
为游戏选择素材时,务必要注意许可。
https://pixabay.com/网站提供的图形无需许可,推荐使用。
在游戏中几乎可以使用任何类型的图像文件,但使用位图(.bmp)最简单,因为Pygame默认加载位图。虽可配置Pygame以使用其他文件类型,但有些类型要求你安装相应图像库。
选择好适合的图像文件后,在主项目文件夹(此处为alien_invasion)中新建一个文件夹,将其命名为images,并将图像文件保存在其中。
> 创建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 # 取屏幕中央x坐标赋值
self.rect.bottom = self.screen_rect.bottom # 取屏幕底部赋值
def blitme(self):
"""在指定位置绘制飞船"""
self.screen.blit(self.image, self.rect)
>> pygame.image
pygame.image是用于图像传输的pygame模块。
- pygame.image.load() — 从文件加载新图片
- pygame.image.save() — 将图像保存到磁盘上
- pygame.image.get_extended() — 检测是否支持载入扩展的图像格式
- pygame.image.tostring() — 将图像转换为字符串描述
- pygame.image.fromstring() — 将字符串描述转换为图像
- pygame.image.frombuffer() — 创建一个与字符串描述共享数据的 Surface 对象
>> get_rect( )
使用get_rect( ) 获取相应surface的属性rect.
常用rect函数的参数有以下形式:
x,y
top, left, bottom, right
topleft, bottomleft, topright, bottomright
midtop, midleft, midbottom, midright
center, centerx, centery
size, width, height
w,h
要将游戏元素居中,可设置相应rect对象的属性center、centerx或centery;
要让游戏元素与屏幕边缘对齐,可使用属性top、bottom、left或right;
要调整游戏元素的水平或垂直位置,可使用属性x和y(分别为相应矩形左上角的x、y坐标)
注意:在Pygame中,原点(0,0) 位于屏幕左上角,向右下方移动时,坐标值将增大。在1200x800的屏幕上,原点位于左上角,而右下角的坐标为(1200,800)
例:将self.rect.centerx(飞船中心的x坐标)设为表示屏幕的矩形的属性centerx,
并将self.rect.bottom(飞船下边缘的y坐标)设置为表示屏幕的矩形的属性bottom。
Pygame将使用这些rect属性来放置飞船图像,使其与屏幕下边缘对齐并水平居中。self.rect.centerx = self.screen_rect.centerx self.rect.bottom = self.screen_rect.bottom
>> surface.blit( )
pygame.Surface.blit() | 将一个图像(Surface 对象)绘制到另一个图像上 |
"""在指定位置绘制飞船"""
self.screen.blit(self.image, self.rect)
> 在屏幕上绘制飞船
更新alien_invasion.py,使其创建一艘飞船,并调用其方法blitme( ) :
--snip--
from ship import Ship
def run_game():
--snip--
pygame.display.set_caption("Alien Invasion") # 设置窗口标题
# 创建一艘飞船
ship = Ship(screen)
# 开始游戏的主循环
while True:
--snip--
# 每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
ship.blitme()
# 让最近绘制的屏幕可见
pygame.display.flip()
run_game() # 初始化游戏并开始主循环
- 必须在主while循环之前创建Ship实例,以免每次循环时都创建一艘飞船。
- 填充背景后再调用ship.blitme()将飞船绘到屏幕上,确保它出现在背景前面。
> 重构:模块game_functions
重构旨在简化既有代码的结构,使其更容易扩展。我们将创建新模块game_functions,它将存储大量让游戏 《外星人入侵》运行的函数。通过创建该模块,可避免alien_invasion.py太长,并使其逻辑更容易理解。
> 函数 check_events( )
首先把管理事件的代码移到一个名为check_events( )的函数中,以简化run_game( )并隔离事件管理循环。通过隔离事件循环,可将事件管理与游戏的其他方面(如更新屏幕)分离。
# game_functions.py
import sys
import pygame
def check_events():
# 响应按键和鼠标事件
for event in pygame.event.get(): # 访问python检测到的事件
if event.type == pygame.QUIT: # pygame.QUIT 指点击右上角窗口的"X"号
sys.exit() # 点击后,卸载所有pygame模块
同时要记得修改alien_invasion.py,导入该模块并将事件循环替换为对check_events()的调用。
> 函数 update_screen()
为进一步简化run_game(),将更新屏幕的代码移到函数update_screen()中:
# game_functions.py
--snip--
def update_screen(ai_settings, screen, ship):
"""更新屏幕上的图像,并切换到新屏幕"""
# 每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
ship.blitme()
# 让最近绘制的屏幕可见
pygame.display.flip()
修改alien_invasion.py,导入该模块,并将更新屏幕替换为对函数update_screen()的调用:
# alien_invasion.py
--snip--
import game_functions as gf
def run_game():
--snip--
# 开始游戏的主循环
while True:
gf.check_events()
gf.update_screen(ai_settings, screen, ship)
run_game() # 初始化游戏并开始主循环
在主程序文件中,不再需要直接导入sys,因为当前只有在模块game_functions中使用了它。出于简化的目的,我们给导入的模块game_functions指定了别名gf。
> 驾驶飞船
编写代码,在用户按左或者右箭头时作出响应。
> 响应按键
每当用户按键时,都将在Pygame中注册一个事件。事件都是通过方法pygame.event.get()获取的,因此在函数check_events()中,我们需要指定要检查哪些类型的事件。每次按键都被注册为一个KEYDOWN事件。检测到KEYDOWN事件时,我们需要检查按下的是否是特定的键。
# game_functions.py
# 例如,如果按下的是右箭头键,我们就增大飞船的rect.centerx值,将飞船向右移动:
--snip--
def check_events(ship):
# 响应按键和鼠标事件
for event in pygame.event.get(): # 访问python检测到的事件
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
ship.rect.centerx += 1 # 向右移动飞船
# alien_invasion.py 更新check_events()参数
--snip--
# 开始游戏的主循环
while True:
gf.check_events(ship)
gf.update_screen(ai_settings, screen, ship)
现在运行alien_invasion.py,则每按右箭头键一次,飞船都将向右移动1像素。
> 允许不断移动
在上面的实验中,我们发现,当玩家按住右箭头键不放时,飞船只会移动一次,即1像素。现在我们希望飞船不断地向右移动,直到玩家松开为止。
我们将让游戏检测pygame.KEYUP事件,以便玩家松开右箭头键时我们能够得知。
飞船的属性都由Ship类控制,因此我们将给这个类添加一个名为moving_right的属性和一个名为update()的方法。方法update()检查标志moving_right的状态,如果这个标志为True,就调整飞船的位置。每当需要调整飞船的位置时,我们都调用这个方法。
# ship.py
--snip--
class Ship:
def __init__(self, screen): # 后者指定了要将飞船绘往哪
--snip--
# 移动标志
self.moving_right = False
def update(self):
"""根据移动标志调整飞船的位置"""
if self.moving_right:
self.rect.centerx += 1
--snip--
同时修改check_events()中当玩家按下/松开右箭头时的事件响应代码:
# 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.moving_right = True # 按下设置为True
elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT:
ship.moving_right = False # 松开设置为False
--snip--
最后修改alien_invasion中的while循环,以便每次执行循环时都调用方法update():
# alien_invasion.py
while True:
gf.check_events(ship)
ship.update()
gf.update_screen(ai_settings, screen, ship)
飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新。这样,玩家输入时,飞船的位置将更新,从而确保使用更新后的位置将飞船绘制到屏幕上。
> 左右移动
完成了向右移动后,向左移动则同理。再次对Ship类和函数check_events() 进行修改:
# 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
在方法update()中,我们添加了if而不是elif代码块。
若玩家同时按下左右箭头键,将先增大飞船的rect.centerx值,再降低,即飞船的位置保持不变。若用elif代码块来处理向左移动的情况,右箭头键将始终处于优先地位。从向左移动切换到向右移动时,玩家可能同时按住左右箭头键,在这种情况下,前面的做法让移动更准确。(测试可知,同时按下左右箭头键,if下飞船不动,而elif下飞船始终向右)
# game_functions.py
--snip--
def check_events(ship):
# 响应按键和鼠标事件
for event in pygame.event.get(): # 访问python检测到的事件
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
ship.moving_right = True
elif event.key == pygame.K_LEFT:
ship.moving_left = True
elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT:
ship.moving_right = False
elif event.key == pygame.K_LEFT:
ship.moving_left = False
这里之所以可以使用elif代码块,是因为每个事件都只与一个键相关联。如果玩家同时按下左右箭头键,将检测到两个不同的事件。
> 调整飞船的速度
我们可以在Settings类中添加属性ship_speed_factor,用于控制飞船的速度:
# settings.py
class Settings:
"""存储《外星人入侵》的所有设置的类"""
def __init__(self):
--snip--
# 飞船的设置
self.ship_speed_factor = 1.5
然而,rect的centerx等属性只能存储整数值,因此我们需要对Ship类作修改:
# ship.py
import pygame
class Ship:
def __init__(self, ai_settings, screen): # 后者指定了要将飞船绘往哪
"""初始化飞船并设置其初始位置"""
self.screen = screen
self.ai_settings = ai_settings
--snip--
# 在飞船的属性center中存储小数值
self.center = float(self.rect.centerx)
--snip--
def update(self):
"""根据移动标志调整飞船的位置"""
# 更新飞船的center值,而不是rect
if self.moving_right:
self.center += self.ai_settings.ship_speed_factor
if self.moving_left:
self.center -= self.ai_settings.ship_speed_factor
# 根据self.center更新rect对象
self.rect.centerx = self.center
可以使用小数来设置rect的属性,但rect将只存储这个值的整数部分。为准确地存储飞船的位置,我们定义了一个可存储小数值的新属性self.center。
(使用函数float()将self.rect.centerx的值转换为小数,并将结果存储到self.center中。)
更新self.center后,我们再根据它来更新控制飞船位置的 self.rect.centerx。self.rect.centerx将只存储self.center的整数部分,但对显示飞船而言,这问题不大。
因为在Ship类中__init__( )形参中添加了ai_settings,让飞船能够获取其速度设置。 所以在alien_invasion.py中创建Ship实例时,需传入实参ai_settings:
# alien_invasion.py
--snip--
def run_game():
--snip--
ship = Ship(ai_settings, screen)
--snip--
> 限制飞船的活动范围
当前,若我们按住箭头键一段时间,就会发现飞船移到了屏幕之外,无法看见。下面就来限制其活动范围,让飞船到达屏幕边缘后停止移动。
# ship.py
--snip--
def update(self):
"""根据移动标志调整飞船的位置"""
# 更新飞船的center值,而不是rect
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_keydown_events(event, ship):
"""响应按键"""
if event.key == pygame.K_RIGHT:
ship.moving_right = True
elif 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
elif event.key == pygame.K_LEFT:
ship.moving_left = False
def check_events(ship):
# 响应按键和鼠标事件
for event in pygame.event.get(): # 访问python检测到的事件
if event.type == pygame.QUIT: # pygame.QUIT 指点击右上角窗口的"X"号
sys.exit() # 点击后,卸载所有pygame模块
elif event.type == pygame.KEYDOWN:
check_keydown_events(event, ship)
elif event.type == pygame.KEYUP:
check_keyup_events(event, ship)
--snip--
> 简单回顾
下一节将添加射击功能,这需要新增一个名为bullet.py的文件,并对一些既有文件进行修改。 当前,我们有四个文件,其中包含很多类、函数和方法。添加其他功能之前,为让你清楚这个项目的组织结构,先来回顾一下这些文件。
> alien_invasion.py
主文件alien_invasion.py创建一系列整个游戏都要用到的对象:存储在ai_settings中的设置、 存储在screen中的主显示surface以及一个飞船实例。文件alien_invasion.py还包含游戏的主循环, 这是一个调用check_events()、ship.update()和update_screen()的while循环。
# alien_invasion.py
import pygame
from settings import Settings
from ship import Ship
import game_functions as gf
def run_game():
# 初始化游戏并创建一个屏幕文件
pygame.init()
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)
# 开始游戏的主循环
while True:
gf.check_events(ship)
ship.update()
gf.update_screen(ai_settings, screen, ship)
run_game() # 初始化游戏并开始主循环
> settings.py
文件settings.py包含Settings类,这个类只包含方法__init__(),它初始化控制游戏外观和飞船速度的属性。
# settings.py
class Settings:
"""存储《外星人入侵》的所有设置的类"""
def __init__(self):
"""初始化游戏的设置"""
# 屏幕设置
self.screen_width = 900
self.screen_height = 600
self.bg_color = (230, 230, 230)
# 飞船的设置
self.ship_speed_factor = 1.5
> game_functions.py
文件game_functions.py包含一系列函数,游戏的大部分工作都是由它们完成的。函数 check_events()检测相关的事件,如按键和松开,并使用辅助函数check_keydown_events()和check_keyup_events()来处理这些事件。就目前而言,这些函数管理飞船的移动。模块 game_functions还包含函数update_screen(),它用于在每次执行主循环时都重绘屏幕。
> ship.py
文件ship.py包含Ship类,这个类包含方法__init__()、管理飞船位置的方法update()以及在屏幕上绘制飞船的方法blitme()。表示飞船的图像存储在文件夹images下的文件ship.bmp中。
# ship.py
import pygame
class Ship:
def __init__(self, ai_settings, screen): # 后者指定了要将飞船绘往哪
"""初始化飞船并设置其初始位置"""
self.screen = screen
self.ai_settings = ai_settings
# 加载飞船图像并获取其外接矩形
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
# 在飞船的属性center中存储小数值
self.center = float(self.rect.centerx)
# 移动标志
self.moving_right = False
self.moving_left = False
def update(self):
"""根据移动标志调整飞船的位置"""
# 更新飞船的center值,而不是rect
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
# 根据self.center更新rect对象
self.rect.centerx = self.center
def blitme(self):
"""在指定位置绘制飞船"""
self.screen.blit(self.image, self.rect)
> 射击
添加射击功能(玩家按空格键发射子弹[小矩形]的代码)。
子弹将在屏幕中向上穿行,抵达屏幕上边缘后消失。
> 添加子弹设置
首先,更新settings.py,在其方法__init__( )末尾存储新类Bullet所需的值:
# settings.py
class Settings:
"""存储《外星人入侵》的所有设置的类"""
def __init__(self):
--snip--
# 子弹设置 (创建宽3像素、高15像素的深灰色子弹,速度为1)
self.bullet_speed_factor = 1
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = 60, 60, 60
> 创建Bullet类
# bullet.py
import pygame
from pygame.sprite import Sprite
class Bullet(Sprite):
"""一个对飞船发射的子弹进行管理的类"""
def __init__(self, ai_settings, screen, ship):
"""在飞船所处的位置创建一个子弹对象"""
super(Bullet, self).__init__()
self.screen = screen
# 在(0,0)处创建一个表示子弹的矩形,再设置正确的位置
self.rect = pygame.Rect(0, 0, ai_settings.bullet_width,
ai_settings.bullet_height)
self.rect.centerx = ship.rect.centerx
self.rect.top = ship.rect.top
# 存储用小数表示的子弹位置
self.y = float(self.rect.y)
self.color = ai_settings.bullet_color
self.speed_factor = ai_settings.bullet_speed_factor
def update(self):
"""向上移动子弹"""
# 更新表示子弹位置的小数值
self.y -= self.speed_factor
# 更新表示子弹的rect的位置
self.rect.y = self.y
def draw_bullet(self):
"""在屏幕上绘制子弹"""
pygame.draw.rect(self.screen, self.color, self.rect)
Bullet类继承了我们从模块pygame.sprite中导入的Sprite类。通过使用精灵,可将游戏中相关的元素编组,进而同时操作编组中的所有元素。这里还调用了super()来继承Sprite.【python游戏编程之旅】第六篇---pygame中的Sprite(精灵)模块和加载动画 - 走看看
from pygame.sprite import Sprite class Bullet(Sprite): def __init__(self, ai_settings, screen, ship): """在飞船所处的位置创建一个子弹对象""" # 这里使用了Python2.7语法,也可简写为 super().__init__() super(Bullet, self).__init__()
子弹并非基于图像的,因此我们必须使用pygame.Rect()类从空白开始创建一个矩形。
# 在(0,0)处创建一个表示子弹的矩形,再设置正确的位置 # Rect(left,top,width,height) self.rect = pygame.Rect(0, 0, ai_settings.bullet_width, ai_settings.bullet_height) # 根据飞船位置调整子弹正确位置,让子弹看起来像是从飞船中射出的 self.rect.centerx = ship.rect.centerx self.rect.top = ship.rect.top
参数 含义 left 与窗口左边界的距离 right 与窗口左边界的距离+图像本身的宽度(width) top 与窗口上边界的距离 bottom 与窗口上边界的距离 + 图像本身的高度(height) 在pygame中,以左上角为坐标原点,水平向右为x轴正方向,竖直向下为y轴正方向。 子弹发射向上移动,本质上y坐标将不断减小,x坐标不变,直线往上,据此更新子弹位置:
def update(self): """向上移动子弹""" # 更新表示子弹位置的小数值 self.y -= self.speed_factor # 更新表示子弹的rect的位置 # 先前用self.y存储小数表示的子弹位置 self.y = float(self.rect.y) self.rect.y = self.y
调用pygame.draw.rect() ——绘制矩形,绘制子弹。
def draw_bullet(self): """在屏幕上绘制子弹""" # pygame.draw.rect(Surface, color, Rect, width=0): return Rect # 在Surface上绘制矩形,color:颜色,Rect形式是((x, y),(width,height)), # width表示线条的粗细,单位为像素;默认值为0,表示填充矩形内部。 pygame.draw.rect(self.screen, self.color, self.rect)
pygame.draw —— Pygame中绘制图形的模块
pygame.draw.rect() —— 绘制矩形
> 将子弹存储到编组中
首先,我们将在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)
run_game() # 初始化游戏并开始主循环
这个编组是在while循环外创建的,这样就无需每次运行该循环时创建一个新的子弹编组
注意! 如果在循环内部创建这样的编组,游戏运行时将创建数千个子弹编组,导致游戏慢的像蜗牛。如果游戏停滞不前,请仔细检查主while循环中发生的情况!
我们将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()前在屏幕上重绘每颗子弹。
# game_functions.py
--snip--
from bullet import Bullet
def check_keydown_events(event, ai_settings, screen, ship, bullets):
--snip--
elif event.key == pygame.K_SPACE:
# 创建一颗子弹,并将其加入到编组bullets中
new_bullet = Bullet(ai_settings, screen, ship)
bullets.add(new_bullet)
--snip--
def check_events(ai_settings, screen, ship, bullets):
# 响应按键和鼠标事件
for event in pygame.event.get(): # 访问python检测到的事件
--snip--
elif event.type == pygame.KEYDOWN:
check_keydown_events(event, ai_settings, screen, ship,bullets)
--snip--
def update_screen(ai_settings, screen, ship, bullets):
"""更新屏幕上的图像,并切换到新屏幕"""
# 每次循环时都重绘屏幕
screen.fill(ai_settings.bg_color)
# 在飞船和外星人后面重绘所有子弹
for bullet in bullets.sprites():
bullet.draw_bullet()
ship.blitme()
# 让最近绘制的屏幕可见
pygame.display.flip()
方法 bullets.sprites()返回一个列表,其中包含编组bullets中的所有精灵。为在屏幕上绘制发射的所有子弹,我们遍历编组bullets中的精灵,并对每个精灵都调用draw_bullet().
for bullet in bullets.sprites(): bullet.draw_bullet()
此时运行alien_invasion.py,将能够左右移动飞船,并发射任意数量的子弹。子弹在屏幕上向上穿行,抵达屏幕底部后消失:
Q:game_functions.py中函数update_screen飞船和子弹绘制顺序先后有影响吗?
# 绘制子弹 for bullet in bullets.sprites(): bullet.draw_bullet() # 绘制飞船 ship.blitme()
> 删除已消失的子弹
在前一个步骤,子弹抵达屏幕顶端后消失,这仅仅是因为Pygame无法在屏幕外面绘制它们。这些子弹实际上依然存在,它们的y坐标为负数,且越来越小。这是个问题,因为它们将继续消耗内存和处理能力。
子弹消除的检测条件即子弹已穿过屏幕顶端,
此时子弹的rect的bottom属性为零
# alien_invasion.py
--snip--
def run_game():
---snip--
while True:
--snip--
# 删除已消失的子弹
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
print(len(bullets)) # 检查语句
gf.update_screen(ai_settings, screen, ship, bullets) # 更新屏幕
run_game() # 初始化游戏并开始主循环
Q:在删除子弹时,为什么要遍历bullets.copy() 【其副本】而非bullets本身?
A:在for循环中,不应从列表或编组中删除条目,因此必须遍历编组的副本。
因为在删除列表元素时,Python会自动对列表内存进行收缩并移动列表元素以保证所有元素之间没有空隙,因此元素列表的索引是会变化的。
循环bullets删除时,删除1 后,2 补位1,3补位2,循环看似循环到2,实际上被删除的是3,而2并没有被检测到。
循环bullets.copy删除时,删除1后,2补位1,但因为bullets.copy中的1没有被删除,2不会前移,仍能被检测,这样就避免了上面的问题。虽然bullets中的元素被删除了,但是bullets.copy的元素并没有改动,所以会循环到bullets(=bullets.copy)的中的所有元素。
通过print语句可以检测子弹是否被删除,检测完后记得将print语句去掉,以免降低游戏运行速度。
> 限制子弹数量
对同时出现在屏幕上的子弹数量进行限制。
首先,在settings.py中存储所允许的最大子弹数:
# settings.py
--snip--
# 子弹设置
self.bullet_speed_factor = 1 # 子弹速度
self.bullet_width = 3 # 宽
self.bullet_height = 15 # 高
self.bullet_color = 60, 60, 60 # 颜色(深灰)
self.bullets_allowed = 3 # 允许的最大子弹数
创建新子弹即玩家按下空格,因此在创建新子弹前(即game_functions.py的check_keydown_events()中),检查未消失的子弹数是否小于该设置:
# game_functions.py
--snip--
def check_keydown_events(event, ai_settings, screen, ship, bullets):
"""响应按键"""
--snip--
elif event.key == pygame.K_SPACE:
# 创建一颗子弹,并将其加入到编组bullets中
if len(bullets) < ai_settings.bullets_allowed: # 检查子弹数量
new_bullet = Bullet(ai_settings, screen, ship)
bullets.add(new_bullet)
--snip--
> 创建函数update_bullets( )
编写并检查子弹管理代码后,可将其移到模块game_functions中,以让主程序文件alien_invasion.py尽可能简单。我们创建一个名为update_bullets()的新函数,并将其添加到game_functions.py的末尾:
# game_functions.py
--snip--
def update_bullets(bullets):
"""更新子弹的位置,并删除已消失的子弹"""
# 更新子弹位置
bullets.update()
# 删除已消失的子弹
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
# print(len(bullets))
同时用该函数替代原在alien_invasion.py中的代码:
# alien_invasion.py
--snip--
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()中只需使
用一行代码来发射子弹,让elif代码块变得非常简单:
# game_functions.py
--snip--
def check_keydown_events(event, ai_settings, screen, ship, bullets):
--snip--
elif event.key == pygame.K_SPACE:
fire_bullet(ai_settings, screen, ship, bullets)
def fire_bullet(ai_settings, screen, ship, bullets):
"""如果还没有到达限制,就发射一颗子弹"""
# 创建一颗新子弹,并将其加入到编组bullets中
if len(bullets) < ai_settings.bullets_allowed: # 检查限制
new_bullet = Bullet(ai_settings, screen, ship)
bullets.add(new_bullet)
> 快捷结束游戏
在每次测试时都需要用鼠标来关闭它,并不方便,因此我们来添加一个结束游戏的快捷键Q:
# game_functions.py
--snip--
def check_keydown_events(event, ai_settings, screen, ship, bullets):
--snip--
elif event.key == pygame.K_q:
sys.exit()
添加此代码块,以便玩家按Q时结束游戏。这样的修改很安全,因为Q键离箭头键和空格键很远,玩家不小心按Q键而导致游戏结束的可能性不大。
> 外星人
> 创建第一个外星人
> 创建Alien类
# alien.py
import pygame
from pygame.sprite import Sprite
class Alien(Sprite):
"""表示单个外星人的类"""
def __init__(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.x = float(self.rect.x)
def blitme(self):
"""在指定位置绘制外星人"""
self.screen.blit(self.image, self.rect)
除位置不同外,这个类的大部分代码都与Ship类相似。每个外星人最初都位于屏幕左上角附近,我们将每个外星人的左边距都设置为外星人的宽度,并将上边距设置为外星人的高度。
> 创建Alien实例
在alien_invasion.py中创建一个Alien实例:
# alien_invasion.py
--snip--
from alien import Alien
def run_game():
--snip--
# 创建一个外星人
alien = Alien(ai_settings,screen)
# 开始游戏的主循环
while True:
--snip--
gf.update_screen(ai_settings, screen, ship, alien, bullets) # 更新屏幕
run_game() # 初始化游戏并开始主循环
> 让外星人出现在屏幕上
为了让外星人出现在屏幕上,我们在update_screen( ) 中调用其方法blitme( ):
# game_functions.py
--snip--
def update_screen(ai_settings, screen, ship, alien, bullets):
--snip--
alien.blitme()
# 让最近绘制的屏幕可见
pygame.display.flip()
完成后执行程序,可以看到一个外星人成功的显示在了屏幕上:
> 创建一群外星人
绘制一群外星人,需要确定一行能容纳多少个外星人以及要绘制多少行外星人。我们首先计算外星人之间的水平间距,并创建一行外星人,再确定可用的垂直空间,并创建整群外星人。
> 确定一行可容纳多少个外星人
屏幕宽度存储在 ai_settings.screen_width中,但需要在屏幕两边留下一定的边距(此处设为外星人的宽度),因此可用于放置外星人的水平空间为屏幕宽度减去外星人宽度的两倍:
available_space_x = ai_settings.screen_width – (2 * alien_width)
外星人之间也需留出一定的空间,即外星人宽度。因此,显示一个外星人所需的水平空间为外星人宽度的两倍:一个宽度用于放置外星人,另一个宽度为外星人右边的空白区域。
number_aliens_x = available_space_x / (2 * alien_width)
> 创建多行外星人
创建一行外星人,首先在alien_invasion.py中创建一个名为aliens的空编组,用于存储全部
外星人,再调用game_functions.py中创建外星人群的函数:
# alien_invasion.py
--snip--
def run_game():
--snip--
# 创建一个外星人编组
aliens = Group()
# 创建外星人群
gf.create_fleet(ai_settings, screen, aliens)
# 开始游戏的主循环
while True:
--snip--
gf.update_screen(ai_settings, screen, ship, aliens, bullets) # 更新屏幕
run_game() # 初始化游戏并开始主循环
# game_functions.py
--snip--
def update_screen(ai_settings, screen, ship, aliens, bullets):
--snip--
aliens.draw(screen) # 在屏幕上绘制编组的每个外星人
# 让最近绘制的屏幕可见
pygame.display.flip()
对编组调用draw( )时,Pygame自动绘制编组的每个元素,绘制位置由元素的属性rect决定。在这里,aliens.draw(screen)在屏幕上绘制编组中的每个外星人。
> 创建外星人群
创建新函数create_fleet(),利用其来创建外星人群:
# game_functions.py
--snip--
from alien import Alien
--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)
alien.x = alien_width + 2 * alien_width * alien_number
alien.rect.x = alien.x
aliens.add(alien)
int(available_space_x / (2 * alien_width)) 此处使用了int() 来确保计算得到的外星人数量为整数,因为我们不希望某个外星人只显示一部分,而且函数range()也需要一个整数。函数int()相当于向下圆整。
此时屏幕如上所示, 这行外星人在屏幕上稍微偏向了左边,这实际上是有好处的,因为我们将让外星人群往右移, 触及屏幕边缘后稍微往下移,然后往左移,以此类推。就像经典游戏《太空入侵者》,相比于只往下移,这种移动方式更有趣。我们将让外形人群不断这样移动,直到所有外星人都被击落或有 外星人撞上飞船或抵达屏幕底端。
> 重构create_fleet( )
鉴于创建外星人的工作还未完成,我们稍微清理一下函数creat_fleet();
下面creat_fleet()和两个新韩淑,get_number_aliens_x()和create_alien():
# game_functions.py
--snip--
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)
> 添加行
要创建外星人群,需计算屏幕可容纳多少行,并对创建一行外星人的循环重复相应的次数。
垂直空间:将屏幕高度减去第一行外星人的上边距(外星人高度)、飞船的高度以及最初外星人群与飞船的距离(外星人高度的两倍):
available_space_y = ai_settings.screen_height – 3 * alien_height – ship_height
最初外星人群与飞船的距离是为在飞船上方留出一定区域,给玩家留出射杀外星人的时间。
每行下方都要留出一定的空白区域,并将其设置为外星人的高度。为计算可容纳的行数,我们将可用垂直空间除以外星人高度的两倍。
number_rows = available_space_y / (2 * alien_height)
# game_functions.py
--snip--
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.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
aliens.add(alien)
def create_fleet(ai_settings, screen, ship, aliens):
--snip--
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)
# alien_invasion.py
# 创建外星人群 (补充新增参数ship)
gf.create_fleet(ai_settings, screen, ship, aliens)
此时运行游戏,将看到一群外星人:
> 让外星人群移动
下面来让外星人群在屏幕上向右移动,撞到屏幕边缘后下移一定的距离,再沿相反的方向移动。所有外星人将不断移动直到都被消灭,有外星人撞上飞船,或有外星人抵达屏幕底端。
> 向右移动外星人
首先在settings.py中添加一个控制外星人速度的设置:
# settings.py
class Settings:
"""存储《外星人入侵》的所有设置的类"""
def __init__(self):
--snip--
# 外星人设置
self.alien_speed_factor = 1
然后,使用这个设置来实现update( ):
# alien.py
--snip--
def update(self):
"""向右移动外星人"""
self.x += self.ai_settings.alien_speed_factor
self.rect.x = self.x
这里同样先使用属性self.x跟踪每个外星人的准确位置(该属性可存储小数值),再使用该值来更新外星人的rect的位置。
在主while循环中更新每个外星人的位置,同时在文件gf.py末尾添加新函数update_aliens():
# alien_invasion.py
--snip--
while True:
gf.check_events(ai_settings, screen, ship, bullets) # 检查事件
ship.update() # 更新飞船状态
gf.update_bullets(bullets) # 更新子弹状态
gf.update_aliens(aliens) # 更新外星人状态
--snip--
# game_functions.py
--snip--
def update_aliens(aliens):
"""更新外星人群中所有外星人的位置"""
aliens.update()
我们对编组aliens()调用方法update(),这将自动对每个外星人调用方法update().
此时运行游戏,会看到外星人群向右移,并逐渐在屏幕右边缘消失。
> 创建表示外星人移动方向的设置
接着来创建让外星人撞到屏幕右边缘后向下移动,再向左移动的设置。
先在settings.py中定义需要的设置:
# settings.py
--snip--
# 外星人设置
self.alien_speed_factor = 1
self.fleet_drop_speed = 10 # 向下移动速度
# fleet_direction为1表示向右移,为-1表示向左移
self.fleet_direction = 1
向下移动速度fleet_drop_speed:区分于水平速度,这样可分别调整两种速度
控制移动方向fleet_direction: 使用值-1与1来表示
注:实现fleet_direction设置,可以将其设置为文本值,如'left'或'right',但这样就必须编写if-elif语句来检查外星人群的移动方向。鉴于只有两个可能的方向,我们使用值1和-1来表 示它们,并在外星人群改变方向时在这两个值之间切换。另外,鉴于向右移动时需要增大每个外星人的x坐标,而向左移动时需要减小每个外星人的x坐标,使用数字来表示方向更合理。
> 检查外星人是否撞到了屏幕边缘
现在编写方法check_edges()检查是否有外星人撞到了屏幕边缘,并修改update()让外星人更准确的移动(将移动量设置为外星人速度和方向±1的乘积,让其向左或向右移)
# alien.py
--snip--
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):
"""向右(dirction为1)或左(dirction为-1)移动外星人"""
self.x += (self.ai_settings.alien_speed_factor *
self.ai_settings.fleet_dirction)
self.rect.x = self.x
> 向下移动外星人群并改变移动方向
有外星人到达屏幕边缘时,需要将整群外星人下移。
# game_functions.py
--snip--
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()
# alien_invasion.py 更新函数(添加新增参数ai_settings)
--snip--
gf.update_aliens(ai_settings, aliens) # 更新外星人状态
--snip--
现在运行游戏,外星人群将在屏幕上来回移动,并在抵达屏幕边缘后向下移动。
> 射杀外星人
运行游戏会发现,当我们发射的子弹击中外星人时,子弹将穿过外星人,这是因为我们还没有检查碰撞。在游戏编程中,碰撞指的是游戏元素重叠在一起。要让子弹能够击落外星人,我们将使用sprite.groupcollide()检测两个编组的成员之间的碰撞。
> 检测子弹与外星人的碰撞
子弹击中外星人时,我们要马上知道,以便碰撞发生后让外星人立即消失。为此,我们将在更新子弹的位置后立即检测碰撞。
方法sprite.groupcollide()将每颗子弹的rect同每个外星人的rect进行比较,并返回一个字典,其中包含发生了碰撞的子弹和外星人。在这个字典中,每个键都是一颗子弹,而相应的值都是被击中的外星人。
# game_functions.py
--snip--
def update_bullets(aliens, bullets):
"""更新子弹的位置,并删除已消失的子弹"""
# 更新子弹位置
bullets.update()
# 删除已消失的子弹
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
# print(len(bullets))
# 检查是否有子弹击中了外星人(击中则删除相应的子弹和外星人)
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
# alien_invasion.py 补充传递实参aliens
--snip--
gf.update_bullets(aliens, bullets) # 更新子弹状态
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
新增的这行代码遍历编组bullets中的每颗子弹,再遍历编组aliens中的每个外星人。每当有子弹和外星人的rect重叠时,groupcollide()就在它返回的字典中添加一个键值对。两个实参True告诉Pygame删除发生碰撞的子弹和外星人。(要模拟能够穿行到屏幕顶端的高能子弹——消灭它击中的每个外星人,可将第一个布尔实参设置为False,并让第二个布尔实参为True。这样被击中的外星人将消失,但所有的子弹都始终有效,直到抵达屏幕顶端后消失。)
此时运行游戏,被击中的外星人将消失。如下图(其中一部分外星人已被击落):
> 为测试创建大子弹
只需通过运行这个游戏就可以测试其很多功能,但有些功能在正常情况下测试起来比较烦琐。例如,要测试代码能否正确地处理外星人编组为空的情形,需花长时间将屏幕上的外星人都击落。
测试有些功能时,可以修改游戏的某些设置,以便专注于游戏的特定方面。例如,可以缩小屏幕以减少需要击落的外星人数量,也可以提高子弹的速度,以便能够在单位时间内发射大量子弹。 测试这个游戏时,我喜欢做的一项修改是增大子弹的尺寸,使其在击中外星人后依然有效。
请尝试将bullet_width设置为300,看看将所有外星人都射杀有多快! 类似这样的修改可提高测试效率,还可能激发出如何赋予玩家更大威力的思想火花。(完成测试后,别忘了将设置恢复正常。)
> 生成新的外星人群
要在外星人群被消灭后又显示一群外星人,首先需要检查编组aliens是否为空。如果为空, 就调用create_fleet()。我们将在update_bullets()中执行这种检查,因为外星人都是在这里被消灭的:
# game_functions.py
--snip--
def update_bullets(ai_settings, screen, ship, aliens, bullets):
--snip--
if len(aliens) == 0:
# 删除现有的子弹并新建一群外星人
bullets.empty()
create_fleet(ai_settings, screen, ship, aliens)
# alien_invasion.py 更新其中对update_bullets()的调用,补充参数
--snip--
while True:
--snip--
gf.update_bullets(ai_settings, screen, ship, aliens, bullets) # 更新子弹状态
--snip--
检查到aliens为空时,我们用bullets.empty() 删除现有的所有子弹
其意义在于以免残留子弹攻击到了下一轮次外星人
在去掉该语句后的测试过程中,残留的子弹直接攻击了第三排的外星人
现在,当前外星人群消灭干净后,将立即出现一个新的外星人群。
> 提高子弹的速度
现在尝试在游戏中射杀外星人,可能发现子弹的速度比以前慢,这是因为在每次循环中 ,Pygame需要做的工作变多了。想提高子弹的速度,可调整 settings.py 中 bullet_speed_factor的值。这里将其从1增大为3:
# settings.py
--snip--
# 子弹设置
self.bullet_speed_factor = 3 # 子弹速度
> 重构update_bullets( )
重构update_bullets(),使其不再完成那么多任务。(新建函数check_bullet_alien_collisions()来检测子弹和外星人之间的碰撞,以及在整群外星人都被消灭干净时采取相应的措施。这避免了update_bullets()太长,简化了后续的开发工作)
# game_functions.py
--snip--
def update_bullets(ai_settings, screen, ship, aliens, bullets):
"""更新子弹的位置,并删除已消失的子弹"""
# 更新子弹位置
bullets.update()
# 删除已消失的子弹
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
# print(len(bullets))
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)
> 结束游戏
如果玩家没能在足够短的时间内将整群外星人都消灭干净,且有外星人撞到了飞船或抵达屏幕底端时,飞船将被摧毁。与此同时,我们还限制了可供玩家使用的飞船数。
> 检测外星人和飞船碰撞
我们首先检查外星人和飞船之间的碰撞,以便外星人撞上飞船时我们能够作出合适的响应。
我们在更新每个外星人的位置后立即检测外星人和飞船之间的碰撞。
# game_functions.py
--snip--
def update_aliens(ai_settings, ship, aliens):
"""检查是否有外星人位于屏幕边缘,并更新整群外星人的位置"""
check_fleet_edges(ai_settings, aliens)
aliens.update()
# 检测外星人和飞船之间的碰撞
if pygame.sprite.spritecollideany(ship, aliens):
print("Ship hit!!!")
# alien_invasion.py 更新调用函数参数
--snip--
while True:
--snip--
gf.update_aliens(ai_settings, ship, aliens) # 更新外星人状态
gf.update_screen(ai_settings, screen, ship, aliens, bullets) # 更新屏幕
方法spritecollideany()接受两个实参:一个精灵和一个编组。它检查编组是否有成员与精灵发生了碰撞,并在找到与精灵发生了碰撞的成员后就停止遍历编组。在这里,它遍历编组 aliens,并返回它找到的第一个与飞船发生了碰撞的外星人。(有外星人撞到飞船时,需要执行的任务很多:需要删除余下的所有外星人和子弹,让飞船重新居中,以及创建一群新的外星人。编写完成这些任务的代码前,需要确定检测外星人和飞船碰撞的方法是否可行。而为确定这一点,最简单的方式是编写一条print语句。)
现在运行游戏,就能看到每当外星人撞到飞船时,终端窗口都显示该print语句:
> 响应外星人和飞船碰撞
首先我们需要将一开始玩家拥有的飞船数存储在settings.py的ship_limit中:
# settings.py
--snip--
# 飞船的设置
self.ship_speed_factor = 1.5
self.ship_limit = 3
--snip--
现在需要确定外星人与飞船发生碰撞时,该做些什么。我们不销毁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
Q: 为什么在reset_stats()中初始化大部分统计信息,而非在__init__() 中直接初始化它们?
A: 在这个游戏运行期间,我们只创建一个GameStats实例,但每当玩家开始新游戏时,需要重置一些统计信息。我们在__init__()中调用方法reset_stats(),这样创建GameStats实例时将妥善地设置这些统计信息,同时在玩家开始新游戏时也能调用reset_stats()。
我们还需对alien_invasion.py做些修改,以创建一个GameStats实例:
# alien_invasion.py
--snip--
from game_stats import Gamestats
def run_game():
--snip--
# 创建一个用于存储游戏统计信息的实例
stats = Gamestats(ai_settings)
--snip--
有外星人撞到飞船时,我们将余下的飞船数减1,创建一群新的外星人,并将飞船重新放置到屏幕底端中央(我们还将让游戏暂停一段时间,让玩家在新外星人群出现前注意到发生了碰撞):
# game_functions.py
--snip--
from time import sleep
--snip--
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
"""响应被外星人撞到的飞船"""
# 将ship_left减1
stats.ships_left -= 1
# 清空外星人列表和子弹列表
aliens.empty()
bullets.empty()
# 创建一群新的外星人,并将飞船放到屏幕底部中央
create_fleet(ai_settings, screen, ship, aliens)
ship.center_ship() # !新建函数于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)
# alien_invasion.py 补充参数
--snip--
gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets) # 更新外星人状态
--snip--
# ship.py 补充函数
--snip--
def center_ship(self):
"""让飞船在屏幕上居中"""
self.center = self.screen_rect.centerx # 将飞船的属性center设置为屏幕中心的x坐标
注意:我们根本没有创建多艘飞船,在整个游戏运行期间,我们只创建了一个飞船实例,并在该飞船被撞到时将其居中,统计信息ships_left让我们知道飞船是否用完。
此时运行这个游戏,射杀几次外星人,并让一个外星人撞到飞船。游戏暂停后,将出现一群新的外星人,而飞船将在屏幕底端居中。但可以通过打印统计信息ships_left知,此时我们并未对其作出一定的限制反应,因此当3个飞船耗尽时,仍会继续生成新飞船(-1)在屏幕底部居中。
> 有外星人到达屏幕底端
如果有外星人到达屏幕底端,我们将像有外星人撞到飞船那样作出响应。请添加一个执行这项任务的新函数,并将其加到函数update_aliens():
# game_functions.py
--snip--
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)
现在运行游戏,每当有外星人撞到飞船或抵达屏幕底端时,都将出现一群新的外星人。
> 游戏结束
前面提到了,游戏现在不会结束,只是ship_left不断变成更小的负数。下面在GameStats中添加一个作为标志的属性game_active,以便玩家的飞船用完后结束游戏:
# game_stats.py
--snip--
def __init__(self, ai_settings):
--snip--
# 游戏刚启动时处于活动状态
self.game_active = True
并在ship_hit()中添加代码,在玩家的飞船都用完后将game_active设置为False:
# game_functions.py
--snip--
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
"""响应被外星人撞到的飞船"""
if stats.ships_left > 0: # 若还有飞船
stats.ships_left -= 1
--snip--
else: # 若飞船用完,将状态置为False
stats.game_active = False
> 确定应运行游戏的哪些部分
在主循环中,在任何情况下都需要调用check_events(),即便游戏处于非活动状态时亦如此。 例如,我们需要知道玩家是否按了Q键以退出游戏,或单击关闭窗口的按钮。我们还需要不断更新屏幕,以便在等待玩家是否选择开始新游戏时能够修改屏幕。其他的函数仅在游戏处于活动状态时才需要调用,因为游戏处于非活动状态时,我们不用更新游戏元素的位置。
# alien_invasion.py
--snip--
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) # 更新屏幕
现在运行游戏,会发现它将在飞船用完后停止不动。
> 记分
我们将添加一个Play按钮,用于根据需要启动游戏以及在游戏结束后重启游戏。 我们还将修改这个游戏,使其在玩家的等级提高时加快节奏,并实现一个记分系统。
> 添加Play按钮
添加一个play按钮,它在游戏开始前出现,并在游戏结束后再次出现,让玩家能开始新游戏。
之前我们的游戏在玩家运行alien_invasion.py时就已经开始了,现在我们对其进行修改,使游戏一开始处于非活动状态,并提示玩家单击Play按钮来开始游戏。
# game_stats.py
--snip--
def __init__(self, ai_settings):
--snip--
# 游戏刚启动时处于活动状态
self.game_active = False
> 创建Botton类
由于Pygame没有内置创建按钮的方法,我们创建一个Button类,用于创建带标签的实心矩形。
# 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__() 接受参数msg,msg是要在按钮中显示的文本。
- 使用 pygame.font.SysFont() 函数从系统内加载字体。实参None让Pygame使用默认字体,而48 指定了文本的字号。
- Pygame通过将你要显示的字符串渲染为图像来处理文本(调用prep_msg())
# button.py
--snip--
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
pygame.font.Font.render()
功能:在新Surface上绘制文本
属性:render(text, antialias, color, background=None) -> Surface
- 创建一个新Surface,并在其上呈现指定的文本。pygame不提供直接在现有surface上绘制文本的方法:若要创建文本的图像(Surface)必须使用 Font.render(),然后将此图像blit到另一个surface上。
- 文本只能是单独一行:换行符不能呈现。空字符('x00') 会引发TypeError。Unicode和字符(byte)字符串都可以。对于Unicode字符串,只能识别UCS-2字符('u0001' to 'uFFFF')。任何更大的值都会引发UnicodeError。对于字符字符串,假定采用 LATIN1 编码。抗锯齿参数(antialias)是布尔值:如果为真,字符将具有平滑的边。颜色参数是文本的颜色[例如:(0,0,255)表示蓝色]。可选的背景参数是用于文本背景的颜色。如果没有传递背景,文本外部的区域将是透明的。
- 返回的Surface应保存文本所需尺寸。(与Font.size()一致)。如果为文本传递空字符串,则将返回零像素宽和高的空白surface。
- 根据背景和抗锯齿使用的类型,返回不同类型的曲面。出于性能原因,最好知道将使用哪种类型的图像。如果不使用抗锯齿,则返回图像将始终是带有双调色板的8-bit图像。如果背景是透明的,则设置colorkey。抗锯齿图像被渲染为24-bit RGB图像。如果背景是透明的,将包括像素alpha。
创建方法draw_button(),通过调用它将这个按钮显示到屏幕上。
- 调用screen.fill()来绘制代表按钮的矩形
- 调用screen.blit()在屏幕上绘制文本图像
# button.py
def draw_button(self):
# 绘制一个用颜色填充的按钮,再绘制文本
self.screen.fill(self.button_color, self.rect)
self.screen.blit(self.msg_image, self.msg_image_rect)
> 在屏幕上绘制按钮
我们使用Button类来创建一个Play按钮:
# alien_invasion.py
--snip--
from button import Button
def run_game():
--snip--
pygame.display.set_caption("Alien Invasion") # 设置窗口标题
# 创建Play按钮
play_button = Button(ai_settings, screen, "Play")
--snip--
接下来,修改update_screen(),以便活动处于非活动状态时显示Play按钮,并将play_button传递给update_screen(),以便能够在屏幕更新时显示按钮:
# alien_invasion.py
--snip--
gf.update_screen(ai_settings, screen, stats, ship, aliens, bullets, play_button) # 更新屏幕
# game_functions.py
def update_screen(ai_settings, screen, stats, ship, aliens, bullets, play_button):
--snip--
# 如果游戏处于非活动状态,就绘制Play按钮
# 为了让Play按钮位于所有屏幕元素上面,最后才绘制它
if not stats.game_active:
play_button.draw_button()
# 让最近绘制的屏幕可见
pygame.display.flip()
现在运行游戏,将在屏幕中央看见一个Play按钮:
> 开始游戏
为在玩家单击Play按钮时开始新游戏,我们添加监视与这个按钮相关的鼠标事件:
# game_functions.py
--snip--
def check_events(ai_settings, screen, stats, play_button, ship, bullets):
# 响应按键和鼠标事件
--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
# alien_invasion.py 更新实参
gf.check_events(ai_settings, screen, stats, play_button, ship, bullets)
玩家单击屏幕时,Pygame将检测到一个MOUSEBUTTONDOWN事件,为了让游戏在玩家用鼠标单击Play按钮时作出响应。我们使用了pygame.mouse. get_pos(),它返回一个元组,其中包含玩家单击时鼠标的x和y坐标。我们将这些值传递给函数check_play_button(),其使用collidepoint()检查鼠标单击位置是否在Play按钮的rect内,在则将game_active设置为True。
此时,点击按钮能开始游戏。游戏结束后,Play按钮会重新显示。
> 重置游戏
前面只处理了玩家第一次单击Play按钮的情况,而没有处理游戏结束的情况,因为没有重置导致游戏结束的条件。为在玩家每次单击Play按钮时都重置游戏,需要重置统计信息,删除现有的外星人和子弹,创建一群新的外星人,并让飞船居中:
# game_functions.py 更新参数
def check_events(ai_settings, screen, stats, play_button, ship, aliens, bullets):
--snap--
check_play_button(ai_settings, screen,stats, play_button, ship, aliens, bullets, mouse_x, mouse_y)
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()
# alien_invasion.py 更新参数
--snip--
while True:
gf.check_events(ai_settings, screen, stats, play_button, ship, aliens, bullets) # 检查事件
现在,每当玩家单击Play按钮时,这个游戏都将正确地重置。
> 将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--
此时运行游戏,会发现开始游戏后单击Play按钮原所在区域没有任何影响了。
> 隐藏光标
为让玩家能够开始游戏,我们要让光标可见,但游戏开始后,光标只会添乱,因此我们将游戏开始后的光标隐藏(在游戏处于活动状态时):
# 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--
pygame的mouse鼠标事件:通过向set_visible()传递False,在光标位于窗口时将其隐藏。
游戏结束后,我们将重新显示光标,让玩家能够单击Play按钮来开始新游戏:
# game_functions.py
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
"""响应被外星人撞到的飞船"""
if stats.ships_left > 0:
--snip--
else:
stats.game_active = False
pygame.mouse.set_visible(True)
> 提高等级
每当玩家将屏幕上的外星人都消灭干净后,加快游戏的节奏
> 修改速度设置
我们首先重新组织Settings类,将游戏设置划分为静态的和动态的两组:
# settings.py
class Settings:
"""存储《外星人入侵》的所有设置的类"""
def __init__(self):
"""初始化游戏的设置"""
# 屏幕设置 900 600
self.screen_width = 900
self.screen_height = 600
self.bg_color = (230, 230, 230)
# 飞船的设置
self.ship_speed_factor = 1.5
self.ship_limit = 3
# 子弹设置
self.bullet_speed_factor = 3 # 子弹速度
self.bullet_width = 3 # 宽
self.bullet_height = 15 # 高
self.bullet_color = 60, 60, 60 # 颜色(深灰)
self.bullets_allowed = 3 # 允许的最大子弹数
# 外星人设置
self.alien_speed_factor = 1
self.fleet_drop_speed = 10 # 向下移动速度
# 以什么样的速度加快游戏节奏
self.speedup_scale = 1.1
self.initialize_dynamic_settings()
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
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
# game_functions.py
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()
ai_settings.increase_speed()
create_fleet(ai_settings, screen, ship, aliens)
我们在整群外星人都被消灭后调用increase_speed()来加快游戏的节奏,再创建一群新的外星人。
> 重置速度
每当玩家开始新游戏时,我们都需将发生了变化的设置重置为初始值,否则新游戏开始时,速度设置将是前一次游戏增加了的值:
# 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()
--snip--
> 记分
实现一个记分系统,以实时地跟踪玩家的得分,并显示最高得分,当前等级和余下的飞船数。
我们在GameStats中添加一个score属性,为在每次开始游戏时都重置得分,我们在reset_stats()中初始化score:
# game_stats.py
class Gamestats():
--snip--
def reset_stats(self):
"""初始化在游戏运行期间可能变化的统计信息"""
self.ships_left = self.ai_settings.ship_limit
self.score = 0 # 玩家得分
> 显示得分
为在屏幕上显示得分,我们首先创建一个新类Scoreboard:
# 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()
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
def show_score(self):
"""在屏幕上显示得分"""
self.screen.blit(self.score_image, self.score_rect)
为确保得分始终锚定在屏幕右边,我们创建了一个名为score_rect的rect,让其右边缘与屏幕右边缘相距20像素,并让其上边缘与屏幕上边缘也相距20像素。
pygame.surface.blit( )方法
旨在将一个图像绘制到另一个图像上
其主要格式:blit(source,dest=None,special_flags=0)
将source参数指定的Surface对象绘制到该对象上。dest参数指定绘制的位置,其值可以是source的左上角坐标,如果传入一个rect对象给dest,那么blit()会使用它的左上角坐标。
> 创建记分牌
我们在alien_invasion.py中创建一个Scoreboard实例:
# alien_invasion.py
--snip--
from scoreboard import Scoreboard
def run_game():
--snip--
# 创建一个用于存储游戏统计信息的实例,并创建记分牌
stats = Gamestats(ai_settings)
sb = Scoreboard(ai_settings, screen, stats)
--snip--
为显示得分,我们修改update_screen() 并在alien_invasion.py中补充实参:
# game_functions.py
def update_screen(ai_settings, screen, stats, sb, ship, aliens, bullets, play_button):
--snip--
# 显示得分
sb.show_score()
# 如果游戏处于非活动状态,就绘制Play按钮
# 为了让Play按钮位于所有屏幕元素上面,最后才绘制它
if not stats.game_active:
play_button.draw_button()
# 让最近绘制的屏幕可见
pygame.display.flip()
# alien_invasion.py
--snip--
gf.update_screen(ai_settings, screen, stats, sb, ship, aliens, bullets, play_button) # 更新屏幕
--snip--
现在运行游戏,将在屏幕右上角看到0:
> 在外星人被消灭时更新得分
为在屏幕上实时显示得分,每当有外星人被击中时都需更新stats.score的值,再调用prep_score()更新得分图像。但在此之前,我们需要指定玩家每击落一个外星人都将得到多少个点。随着游戏的进行,我们将提高每个外星人值的点数。为确保每次开始新游戏时这个值都会被重置,我们在initialize_dynamic_settings()中设置它。
# settings.py
--snip--
def initialize_dynamic_settings(self):
"""初始化随游戏进行而变化的设置"""
--snip--
# 记分
self.alien_points = 50
在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--
# 更新实参
def update_bullets(ai_settings, screen, stats, sb, ship, aliens, bullets):
"""更新子弹的位置,并删除已消失的子弹"""
--snip--
check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship, aliens, bullets)
# alien_invasion.py 更新实参
--snip--
gf.update_bullets(ai_settings, screen, stats, sb, ship, aliens, bullets) # 更新子弹状态
--snip--
有子弹撞到外星人时,Pygame返回一个字典(collisions)。我们检查这个字典是否存在,如果存在,就将得分加上一个外星人值的点数。接下来,我们调用 prep_score()来创建一幅显示最新得分的新图像。(更新图像后由update_screen调用blit()绘制新的得分表)
现在运行游戏,会发现击落外星人时,得分将不断增加:
> 将消灭的每个外星人的点数都计入得分
当前,我们的代码可能遗漏了一些被消灭的外星人。例如,如果在一次循环中有两颗子弹射中了外星人,或者因子弹更宽而同时击中了多个外星人,玩家将只能得到一个被消灭的外星人的点数。为修复这种问题,我们来调整检测子弹和外星人碰撞的方式。
在check_bullet_alien_collisions()中,与外星人碰撞的子弹都是字典collisions中的一个键(key);而与每颗子弹相关的值都是一个列表(value),其中包含该子弹撞到的外星人。我们遍历字典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: # 检查是否有返回值
for aliens in collisions.values():
stats.score += ai_settings.alien_points * len(aliens)
sb.prep_score()
--snip--
将子弹宽度调大核实结果是否正确:(图为子弹两次打中共8个外星人得分)
> 提高点数
玩家等级提升时,外星人的点数也应相应提高。
# settings.py
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)
print(self.alien_points)
此时运行游戏,通过print语句检测等级提高后点数的变化:
确认完效果后,记得将print语句删去,以免影响游戏性能。
> 将得分圆整
大多数街机风格的射击游戏都将得分显示为10的整数倍,下面让我们的记分系统遵循这个原则。我们还将设置得分的格式,在大数字中添加用逗号表示的千位分隔符。
# scoreboard.py
--snip--
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 2.7中,round()总是返回一个小数值,因此我们使用int()来确保报告的得分为 整数。如果你使用的是Python 3,可省略对int()的调用。
下面使用了一个字符串格式设置指令,它让Python将数值转换为字符串时在其中插入逗号, 例如,输出1,000,000而不是1000000。
> 最高得分
跟踪并显示最高得分,给玩家提供要超越的目标。鉴于在任何情况下都不会重置最高得分,我们将其存储在__init__()中:
# game_stats.py
class Gamestats():
"""跟踪游戏的统计信息"""
def __init__(self, ai_settings):
--snip--
# 在任何情况下都不应重置最高得分
self.high_score = 0
编写方法准备包含最高得分的图像以及图像的显示:
# scoreboard.py
import pygame.font
class Scoreboard():
"""显示得分信息的类"""
def __init__(self, ai_settings, screen, stats):
--snip--
# 准备包含最高得分和当前得分的图像
self.prep_score()
self.prep_high_score()
def prep_high_score(self):
"""将得分最高转换为渲染的图像"""
high_score = int(round(self.stats.high_score, -1))
high_score_str = "{:,}".format(high_score)
self.high_score_image = self.font.render(high_score_str, True,
self.text_color, self.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.screen_rect.top
def show_score(self):
"""在屏幕上显示当前得分和最高得分"""
self.screen.blit(self.score_image, self.score_rect)
self.screen.blit(self.high_score_image, self.high_score_rect)
为检测是否有新的最高得分诞生,我们添加新函数check_high_score(),它使用stats来比较当前得分和最高得分,并在必要时使用sb来修改最高得分图像:
# 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():
# 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: # 检查是否有返回值
for aliens in collisions.values():
stats.score += ai_settings.alien_points * len(aliens)
sb.prep_score()
check_high_score(stats, sb)
此时两次运行游戏,第一次时,当前得分就是最高得分,因此两处均显示为当前得分。第二次时,可以明显看到最高得分与当前得分的区别。
> 显示等级
先添加一个表示等级的属性,为了每次开始新游戏时都重置等级,我们在reset_stats()初始化:
# game_stats.py
def reset_stats(self):
"""初始化在游戏运行期间可能变化的统计信息"""
self.ships_left = self.ai_settings.ship_limit
self.score = 0 # 玩家得分
self.level = 1 # 玩家等级
我们调用新方法prep_level(),让其能够在当前得分下方显示当前等级,并更新show_score():
# scoreboard.py
import pygame.font
class Scoreboard():
"""显示得分信息的类"""
def __init__(self, ai_settings, screen, stats):
--snip--
# 准备包含最高得分和当前得分的图像
self.prep_score()
self.prep_high_score()
self.prep_level()
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
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)
在整群外星人被消灭时,我们设置其提升一个等级:
# 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)
为确保开始新游戏时更新记分和等级图像,在按钮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--
# 重置记分牌图像
sb.prep_score()
sb.prep_high_score()
sb.prep_level()
--snip--
# 更新参数(新增参数sb)
def check_events(ai_settings, screen, stats, sb, play_button, ship, aliens, bullets):
# 响应按键和鼠标事件
for event in pygame.event.get(): # 访问python检测到的事件
--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)
# alien_invasion.py 更新参数(新增参数sb)
while True:
gf.check_events(ai_settings, screen, stats, sb, play_button, ship, aliens, bullets) # 检查事件
此时运行游戏如下:
注:在一些经典游戏中,得分带标签,如Score、HighScore和Level。我们没有显示这些标签, 因为开始玩这款游戏后,每个数字的含义将一目了然。要包含这些标签,只需在Scoreboard 中调用font.render()前,将它们添加到得分字符串中即可。
> 显示余下的飞船数
最后,我们来显示玩家还有多少艘飞船,但使用图形而不是数字。为此,我们在屏幕左上角绘制飞船图像来指出还余下多少艘飞船,就像众多经典的街机游戏那样。
首先,需要让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--
接下来,我们修改Scoreboard,在其中创建一个可供显示的飞船编组self.ships,并通过循环向其内存储飞船实例。为在屏幕上显示飞船,我们对编组调用draw(),Pygame将绘制每艘飞船:
# scoreboard.py
import pygame.font
from pygame.sprite import Group
from ship import Ship
class Scoreboard():
"""显示得分信息的类"""
def __init__(self, ai_settings, screen, stats):
--snip--
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)
def show_score(self):
--snip--
# 绘制飞船 ( 对编组调用draw,Pygame将绘制每一艘飞船)
self.ships.draw(self.screen)
为在游戏开始时让玩家知道他有多少艘飞船,我们在开始游戏时调用prep_ships():
# 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()
还需在飞船被外星人撞到时调用prep_ships(),从而在玩家损失一艘飞船时更新飞船图像:
# game_functions.py
def ship_hit(ai_settings, stats, screen, sb, ship, aliens, bullets):
"""响应被外星人撞到的飞船"""
if stats.ships_left > 0:
# 将ship_left减1
stats.ships_left -= 1
# print(stats.ships_left)
# 更新记分牌
sb.prep_ships()
--snip--
# 更新参数(新增参数sb)
def update_aliens(ai_settings, stats, screen, sb, ship, aliens, bullets):
--snip--
# 检测外星人和飞船之间的碰撞
if pygame.sprite.spritecollideany(ship, aliens):
ship_hit(ai_settings, stats, screen, sb, ship, aliens, bullets)
# 检查是否有外星人到达屏幕底端
check_aliens_bottom(ai_settings, stats, screen, sb, ship, aliens, bullets)
def check_aliens_bottom(ai_settings, stats, screen, 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, stats, screen, sb, ship, aliens, bullets)
break
# alien_invasion.py
while True:
--snip--
if stats.game_active:
--snip--
gf.update_aliens(ai_settings, stats, screen, sb, ship, aliens, bullets) # 更新外星人状态
此时运行游戏:
尝试go die 一次:文章来源:https://www.toymoban.com/news/detail-484598.html
> End
最终呈现结果如下(此处调整了high_score的高度):文章来源地址https://www.toymoban.com/news/detail-484598.html
到了这里,关于Python Project- Alien_invasion(外星人入侵)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!