接着做接着做接着做
来源:《Python编程:从入门到实践》
文章目录
5 重构:模块game_functions
- 重构:旨在简化既有代码的结构,使其更容易扩展
- 下面创建一个名为
game_functions
的新模块,它存储大量让《外星人入侵》运行的函数 - 创建模块
game_functions
,可避免alien_invasion.py
太长,逻辑也更易理解
5.1 函数check_events()
管理事件的代码移到check_events()的函数中,以简化run_game()并隔离事件管理循环
- 隔离事件循环,可将事件管理与游戏的其他方面(如更新屏幕)分离
game_functions.py
import sys
import pygame
def check_events():
"""响应按键和鼠标事件"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
- 导入事件检查循环要使用的
sys和pygame
函数check_events()不需要形参
,函数体复制alien_invasion.py的事件循环- 下面来修改alien_invasion.py,导入模块game_functions,再将事件循环替换为函数check_events():
alien_invasion.py
import pygame
import game_functions as gf
def run_game():
--snip--
# 开始游戏的主循环
while True:
gf.check_events()
# 每次循环时都重绘屏幕
--snip--
- 主程序文件不再需要直接导入
sys
,因为模块game_functions
中使用了它
5.2 函数update_screen()
- 进一步简化run_game(),将更新屏幕的代码移到update_screen()的函数中,并将此函数放在模块game_functions中
game_functions.py
--snip--
def check_events():
--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
- 现在将主程序的while循环
更新屏幕的代码替换为函数update_screen()
alien_invasion.py
--snip--
def run_game():
--snip--
# 开始游戏的主循环
while True:
gf.check_events()
gf.update_screen(ai_settings, screen, ship)
- 这两个函数让while更简单,让后续开发更容易:
在模块game_functions完成大部分工作而不是run_game()
练习12-1 蓝色天空:创建一个背景为蓝色的Pygame窗口
test12-1.py
import sys
import pygame
from settings import Settings
def run_game():
pygame.init()
settings = Settings()
screen = pygame.display.set_mode(
(settings.screen_width, settings.screen_height))
pygame.display.set_caption("程浩宇的蓝色天空")
# 开始游戏的主循环
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screen.fill(settings.bg_color)
# 让最近绘制的屏幕可见
pygame.display.flip()
run_game()
settings.py
class Settings():
def __init__(self):
self.screen_width = 1200
self.screen_height = 800
self.bg_color = (70, 130, 180)
运行主程序,结果如图:
练习12-2 游戏角色:找一幅你喜欢的游戏角色位图图像或将一副图像转换为位图。创建一个类,将该角色绘制到屏幕中央,并将该图像的背景色设置为屏幕背景色,或将屏幕背景色设置为该图像的背景色
test12-2.py
直接用现成的小飞船吧,背景色用上面的蓝色,再把小飞船的背景色改成屏幕背景色…
将角色绘制到屏幕中央,设置相应rect对象的属性centerx、centery即可
下面就把模块ship放上来吧,主程序和模块settings变动不大
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.centery = self.screen_rect.centery
def blitme(self):
"""在指定位置绘制飞船"""
self.screen.blit(self.image, self.rect)
运行主程序,发现小飞船成功绘制到屏幕中央了:
6 驾驶飞船
- 用户按左或右箭头时作出响应
- 首先专注于向右移动,再使用同样的道理来控制向左移动
- 通过这样做,你将学会如何控制屏幕图像的移动
6.1 响应按键
- 每当用户按键,都将在Pygame注册一个事件
- 事件通过
方法pygame.event.get()
获取的,因此在函数check_events()
中,每次按键都将被注册为一个KEYDOWN事件
- 例如,如果按下的是
右箭头键,我们就增大飞船的rect.centerx的值,将飞船右移
:
game_functions.py
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
- 现在
函数check_events()中包含形参ship
- 在函数
check_events()
内部,在事件循环中添加了一个elif代码块
,以便在Pygame检查到KEYDOWN事件时做出响应 - 读取
属性event.key
,检查按下的是否是右箭头键(pygame.K_RIGHT)
,若是,ship.rect.centerx的值加1,飞船右移
- 在alien_invasion.py中,更新check_events() 代码:
alien_invasion.py
# 开始游戏的主循环
while True:
gf.check_events(ship)
gf.update_screen(ai_settings, screen, ship)
- 不过现在运行程序的话,你会发现每按一次右箭头键,飞船向右移动1像素
- 但并非控制飞船的高效方式。下面改进控制方式,允许持续移动
6.2 允许不断移动
- 让游戏检测
pygame.KEYUP
事件,以便玩家松开右箭头键时我们知道这一点 结合使用KEYDOWN和KEYUP事件,以及一个moving_right的标志来实现持续移动
- 飞船不动时,moving_right为False
- 玩家按下→时,moving_right为True
- 玩家松开时,moving_right为False
- 飞船的属性由Ship类控制,下面添加一个
方法update()
和一个属性moving_right
:
ship.py
class Ship():
def __init__(self, screen):
--snip--
# 移动标志
self.moving_right = False
def update(self):
"""根据移动标志调整飞船的位置"""
if self.moving_right:
self.rect_centerx += 1
def blitme(self):
--snip--
- 下面来修改check_events(),玩家按下→时将moving_right设置为True,松开时将moving_right设置为False:
game_functions.py
def check_events(ship):
"""响应按键和鼠标事件"""
for event in pygame.event.get():
--snip--
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
- 上面修改了按下→时响应的方式:不直接调整飞船的位置,而只是将
moving_right设置为True
- 接着添加了一个
elif代码块
,用于响应KEYUP事件:玩家松开→时,将moving_right设置为False
- 最后,我们需要修改alien_invasion.py的
while循环
,以便每次执行循环时都调用飞船的方法update()
:
alien_invasion.py
# 开始游戏的主循环
while True:
gf.check_events(ship)
ship.update()
gf.update_screen(ai_settings, screen, ship)
- 现在运行alien_invasion.py,按住→,飞船能够不断右移,直到你松开为止
6.3 左右移动
- 飞船能够不断右移后,添加左移的逻辑很容易
- 下面再次修改
Ship类
和函数check_events()
:
ship.py
def __init__(self, screen):
--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
- 方法__init__()中,添加了
标志moving_left
方法update()中,添加了一个if代码块而不是elif代码块
:当玩家同时按下左右箭头键,将先增大self.rect.centerx,再降低这个值,即飞船的位置保持不变- 下面来修改
check_events()
:
game_functions.py
def check_events(ship):
"""响应按键和鼠标事件"""
for event in pygame.event.get():
--snip--
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代码块,是因为每个事件都只与一个键相关联
如果同时按下了←→,将检测到两个不同事件
- 此时运行主程序,发现能够不断地左右移动飞船;同时按←→键,飞船纹丝不动
6.4 调整飞船的速度
- 当前,每次执行while循环时,飞船最多移动1像素
- 现在在
Settings类
中添加属性ship_speed_factor
,用于控制飞船的速度
settings.py
class Settings():
def __init__(self):
--snip--
# 飞船的设置
self.ship_speed_factor = 1.5
- 通过将ship_speed_factor速度设置指定为小数值,可在后面加快游戏的节奏时更细致地控制飞船的速度
- 然而,rect的centerx等属性只能存储整数值,因此需要对Ship类做些修改:
ship.py
def __init__(self, ai_settings, screen):
"""初始化飞船并设置其初始位置"""
self.screen = screen
self.ai_settings = ai_settings
--snip--
# 将每艘新飞船放在屏幕底部中央
--snip--
#在飞船的属性center中存储小数值
self.center = float(self.rect.centerx)
# 移动标志
self.moving_right = False
self.moving_left = False
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
__init__()
的形参列表中添加了ai_settings,让飞船能够获取其速度设置
- 接下来,将形参ai_settings存储在一个属性中,以便update()使用它
- 调整飞船的位置时,将增加或减去一个单位为像素的小数值,因此需要将位置存储在一个能够存储小数值的变量中
可以使用小数来设置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
def run_game():
--snip--
# 创建一艘飞船
ship = Ship(ai_settings, screen)
--snip--
6.5 限制飞船的活动范围
- 运行程序,你会发现,按住箭头键的时间过长,飞船就跑到屏幕外面去了……
- 下面修复这个问题,让飞船到达屏幕边缘后停止移动
- 修改Ship类的方法update():
ship.py
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
- self.rect.right返回飞船外接矩形右边缘的x坐标,如果这个值小于self.screen_rect.right的值,就说明飞船未触及屏幕右边缘
- 左边缘情况与之类似:self.rect.left即rect的左边缘的x坐标,它大于零,就说明未触及屏幕左边缘
- 这就确保了飞船只有在屏幕内时,才能调整self.center的值
- 此时运行alien_invasion.py,飞船将在触及左右边缘不能移动
6.6 重构check_events()
- 随着游戏开发的进行,函数check_events()愈来愈长,现在将其部分代码放在两个函数中:一个处理KEYDOWN事件,另一个处理KEYUP事件:
game_functions.py
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():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
check_keydown_events(event, ship)
elif event.type == pygame.KEYUP:
check_keyup_events(event, ship)
7 射击
- 下面来添加射击功能:玩家按下空格键时发射子弹
- 子弹将在屏幕中向上穿行,抵达屏幕上边缘后消失
7.1 添加子弹设置
- 更新settings.py,在其
__init__()
末尾存储新类Bullet
所需的值:
settings.py
def __init__(self):
--snip--
# 子弹设置
self.bullet_speed_factor = 1
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = 60, 60, 60
- 创建宽3像素、高15像素的深灰色子弹
- 子弹速度比飞船稍低
7.2 创建Bullet类
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
Bullet类
继承从模块pygame.sprite中导入的Sprite类
- 通过使用
精灵
,可将游戏中相关的元素编组,进而同时操作编组中的所有元素
- 为创建子弹实例,需要向
__init__()传递ai_settings、screen和ship实例,还调用了super()来继承Sprite
注意:代码super(Bullet, self).init() 使用了Python2.7语法。这种语法也适用于Python3,但你也可以将这行代码简写为super().init()
- 下面是
bullet.py
的第二部分——方法update() 和 draw_bullet()
:
bullet.py
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)
方法update()管理子弹的位置
- 需要绘制子弹时,调用
draw_bullet()
函数draw.rect()使用存储在self.color中的颜色填充表示子弹的rect占据的屏幕部分
8.3 将子弹存储在编组中
- 完成上面的准备后,下面要做的是:在玩家每次按空格键时都射出一发子弹
- 首先,
在alien_invasion.py中创建一个编组(group)
,存储所有有效的子弹,以便能够管理发射出去的所有子弹 这个group将是pygame.sprite.Group类的一个实例
pygame.sprite.Group类 类似于列表,但提供有助于开发游戏的额外功能
- 在主循环中,使用这个
group
在屏幕上绘制子弹,以及更新每颗子弹的位置:
alien_invasion.py
from pygame.sprite import Group
--snip--
def run_game():
--snip--
# 创建一艘飞船
ship = Ship(ai_settings, screen)
# 创建一个用于存储子弹的编组
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--
创建了一个Group实例,将其命名为bullets
- 这个编组是在while循环外创建的 ,这样就无需每次运行该循环时都创建一个新的子弹编组,导致游戏慢得像蜗牛。如果游戏停滞不前,请仔细查看主while循环中发生的情况
注意:如果在循环内部创建这样的编组,游戏运行时将创建数千个子弹编组
当你对编组调用update()时,编组将自动对其中的每个精灵调用update()
,因此代码行bullets.update()将为编组bullets中的每颗子弹调用bullet.update()
8.4 开火
- game_functions.py中,需要修改check_keydown_events(),以便在玩家按空格键时发射一颗子弹
- 还需修改update_screen(),确保在调用flip()前在屏幕上重绘每颗子弹
game_functions.py
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():
--snip--
elif event.type == pygame.KEYDOWN:
check_keydown_events(event, ai_settings, screen, ship, bullets)
--snip--
def update_screen(ai_settings, screen, ship, bullets):
--snip--
# 在飞船和外星人后面重绘所有子弹
for bullet in bullets.sprites():
bullet.draw_bullet()
ship.blitme()
编组bullets传递给了check_keydown_events()
。- 玩家按空格键时,创建一颗新子弹(
一个名为new_bullet的Bullet实例
),并使用方法add()将其加入到编组bullets中
方法bullets.sprites()返回一个列表,其中包含编组bullets的所有精灵
- 为绘制发射的所有子弹,我们遍历编组bullets中的精灵,并对每个精灵都调用
draw_bullet()
- 现在运行alien_invasion.py,能够左右移动飞船,并发射任意数量的子弹
8.5 删除已消失的子弹
- 现在,子弹到达屏幕顶端后消失,但这些子弹实际上依然存在,y坐标为负数,且越来越小
- 这个问题将导致它们继续消耗内存和处理能力
- 我们需要将这些已消失的子弹删除:
alien_invasion.py
# 开始游戏的主循环
while True:
--snip--
bullets.update()
# 删除已消失的子弹
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
print(len(bullets))
--snip--
在for中,不应从列表或编组中删除条目,因此必须遍历编组的副本
- 使用方法
copy()
来设置for循环,这能够在循环中修改bullets - print语句显示还有多少颗子弹,从而核实已消失的子弹确实删除了
- 如果这些代码没有问题,将print语句删除。如果留下这个语句,游戏的速度将大大降低
8.6 限制子弹数量
- 很多设计游戏都对同时出现在屏幕上的子弹数量进行限制,以鼓励玩家有目标地设计
- 首先,在settings中存储所允许的最大子弹数:
settings.py
# 子弹设置
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
def check_keydown_events(event, ai_settings, screen, ship, bullets):
--snip--
# 创建一颗子弹,并将其加入到编组bullets中
if len(bullets) < ai_settings.bullets_allowed:
new_bullet = Bullet(ai_settings, screen, ship)
bullets.add(new_bullet)
- 现在运行这个游戏,屏幕上最多只能有3颗子弹
8.7 创建函数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 True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
gf.update_bullets(bullets)
gf.update_screen(ai_settings, screen, ship, bullets)
- 让主循环包含尽可能少的代码,这样只看函数名就能迅速知道游戏发生的情况
8.8 创建函数fire_bullet()
- 下面将发射子弹的代码移到一个独立的函数中:
game_functions.py
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)
目前,运行游戏,确认发射子弹时依然没有错误