目录
引言
本篇博文是《外星人入侵》系列的完结篇,在之前的博文中,我们实现了游戏的绝大多数功能并将相关代码封装起来,方便修改。
在本完结篇中,我们将要实现的功能有二:
1、游戏难度的提升
2、计分板的实现
一、游戏难度的提升
既然要提升游戏的难度,那么就要思考:难度提升体现在哪些方面?
1、外星人移动速度(指横向移动的速度)
提升游戏难度的时机是什么时候?
1、玩家击落一整群外星人时
和外星人速度一起提升的还有什么?
1、飞船速度
2、子弹速度
如果不这样设置,游戏会很容易结束
1、提升速度
我们对于所有速度值的设置都是放在 设置模块 中的 设置类中,在游戏初始化时,飞船、子弹、外星人的速度都被好了。
现在我们希望定义一个方法,可以提升速度值,而提升的比率不能太高。
# 当玩家击落一群外星人后,增加游戏难度的方法
def increase_speed(self):
# 提升飞船、外星人、子弹的速度
self.ship_speed *= self.speedup_scale
self.bullet_speed *= self.speedup_scale
self.alien_speed *= self.speedup_scale
其中, speedup_scale 就是我们速度的提升率
class Settings():
def __init__(self):
...
# 飞船、子弹、外星人速度的提升率
self.speedup_scale = 1.1
...
我们每次击落一整群外星人后,速度都会是之前的 1.1 倍,这样处理,会让速度在多次提升后达到一个恐怖的境界。这还只是我们提升外星人横向移动的速度,下落速度还没有改变。
2、何时提升速度
如前文所说,我们是要在玩家击落一整群外星人后,提升游戏难度,也就是调用 increase_speed() 函数
因此,我们需要在 check_bullet_alien_collide() 方法中做修改。
我们之前在玩家击落整群外星人后,仅仅只是清空子弹、重新生成一群新的外星人,而现在,还需要提升速度
# 当外星人全都被打完时,删除现有子弹,并重新创建一群外星人
if len(aliens) == 0:
# 清空子弹
bullets.empty()
# 新一批外星人的速度提高
settings.increase_speed()
# 生成一群新的外星人
create_fleet(settings, screen, aliens, ship)
注意,三者的逻辑顺序。
先清空子弹队列,然后提升速度值,这样一来,之后生成的新外星人群就会有着提升后的速度。
3、重置速度
当玩家成功击落外星人大军时,我们提升游戏难度。
但是当玩家失败时,我们也需要重置游戏难度,以免玩家再次开始游戏时,就面对难以匹敌的外星大军。
因此,需要定义一个重置动态属性的方法
# 当玩家按下开始按钮后,重置游戏难度的方法
def initialize_dynamic_settings(self):
self.ship_speed = 1.5
self.bullet_speed = 3
self.alien_speed = 1
self.fleet_direction = 1
每当我们玩家失败时,就调用这个方法,实现难度的重置。
因此,我们需要修改 check_play_botton() 方法,当玩家按下开始按钮后,重置游戏难度
# 玩家点击Play按钮后开始新游戏
def check_play_button(mouse_x, mouse_y, play_button, game_stats, aliens, bullets, settings, screen, ship, scoreboard):
button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
# 当玩家在游戏处于非活跃状态下点击Play按钮时
if button_clicked and not game_stats.game_active:
# 游戏难度初始化(重置)
settings.initialize_dynamic_settings()
# 游戏活跃时隐藏光标
pygame.mouse.set_visible(False)
# 重置游戏设置
game_stats.reset_stats()
# 绘制重置后的游戏分数、最高分、等级
scoreboard.prep_score()
# 将游戏切换回活跃状态
game_stats.game_active = True
# 清空子弹和外星人
aliens.empty()
bullets.empty()
# 创建一群新的外星人,并让飞船居中
create_fleet(settings, screen, aliens, ship)
ship.center_ship()
至此,我们就实现了游戏难度的提升。随着玩家一次次击落外形大军,新出现的外星人将会更难对付!而如果玩家不幸落败、重新开始时,也能从零来实锻炼自己。
二、计分板功能
现在,我们想给每个外星人一定的分数,当我们击落外星人时,玩家的得分将会随之提升。而分数要体现在游戏屏幕上,能让我们实时观察到。
1、玩家的得分
当我们游戏开始时,玩家的得分肯定是 0 ,因为我们没有击落任何一只外星人,所以,将玩家得分这个属性添加到 游戏状态类 中
同时,由于每次玩家失败时,都要重置得分值,所以,将初始化和重置两个操作都保存到 reset() 方法中。
class GameStats():
# 初始化游戏状态,设置玩家开局将有多少架飞船可以使用
def __init__(self, settings):
self.settings = settings
self.reset_stats()
def reset_stats(self):
# 游戏刚开始时,处于非活跃状态
self.game_active = False
# 游戏初始化时,分数为0
self.score = 0
这样,当游戏开始和玩家重置时,玩家得分值都会从 0 开始计算。
2、绘制玩家得分
现在我们的得分值一直是 0 ,但想要先把这个值绘制到屏幕上,让我们能直观地看到它。
通过前面学到的知识,我们处理的是一个 整型数据,想要将其显示到屏幕上,可以这么做:
1、将 玩家分数(int) 转换成 字符串
2、将 分数字符串 渲染成 Surface
3、将 分数Surface 显示到屏幕上
因此,我们定义 计分板类,用户维护绘制计分板所需的所有方法和设置。
再定义prep_score() 方法,以实现将分数值绘制到屏幕右上角的效果。
# 计分板类
class ScoreBoard():
def __init__(self, screen, settings, stats):
# 初始化信息
self.screen = screen
self.screen_rect = self.screen.get_rect()
self.settings = settings
self.stats = stats
# 分数图形的参数
self.text_color = (30, 30, 30)
self.font = pygame.font.SysFont(None, 48)
# 获取分数图形
self.prep_score()
def prep_score(self):
# 将数字分数转换成字符串分数
str_score = str(self.stats.score)
# 将字符串转换成 Surface对象
self.score_image = self.font.render(str_score, True, self.text_color, self.settings.background_color)
# 获取分数图形的坐标位置
self.score_rect = self.score_image.get_rect()
# 将分数图形绘制到屏幕右上角
self.score_rect.right = self.screen_rect.right - 20
self.score_rect.top = 20
我们需要提前设置好:
1、分数的字体
2、分数的颜色
3、背景颜色
另外,我们需要编写一个绘制 Surface对象的方法,用来将分数绘制到屏幕上
def show_scoreboard(self):
# 绘制当前分数
self.screen.blit(self.score_image, self.score_rect)
最后,将这个方法在 update_screen() 方法中调用,并实例化一个计分板,我们就可以在屏幕上看到右上角有一个显示为 0 的分数了。
def update_screen(screen, setting, ship, bullets, aliens, game_stats, button, scoreboard):
# 为纯黑的游戏屏幕填充上不一样的颜色
screen.fill(setting.background_color)
# 在背景之上绘制我们的飞船,注意这里的逻辑,必须是飞船在背景之后绘制,确保飞船在背景的上层
ship.blitme()
# 在屏幕和飞船之上,绘制子弹
for bullet in bullets.sprites():
bullet.draw_bullet()
# 在屏幕上绘制外星人
aliens.draw(screen)
# 绘制计分板
scoreboard.show_scoreboard()
# 如果游戏处于非活跃状态,那么就绘制按钮
if not game_stats.game_active:
button.draw_button()
# 刷新屏幕,使得元素能够不断刷新位置
pygame.display.flip()
3、提高玩家得分
当玩家击落外星人时,提高得分值。因此,每只外星人都有自己的分数,击落多少只外星人,就相应地提高玩家得分值。
# 当玩家按下开始按钮后,重置游戏难度的方法
def initialize_dynamic_settings(self):
self.ship_speed = 1.5
self.bullet_speed = 3
self.alien_speed = 1
self.fleet_direction = 1
# 外星人的价值(击落一只外星人能得多少分)
self.alien_points = 50
我们这里之所以将外星人分数值放在重置难度方法中,而不放在初始化方法中,是有原因的,等下文提到了再详述。
而提高得分值的时机则是,当子弹和外星人发生碰撞时,玩家得分,因此修改碰撞检测代码即可
def check_bullet_alien_collide(bullets, aliens, settings, screen, ship, scoreboard, stats):
# 检测子弹队列中的每颗子弹是否击中外星人队列中的外星人
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
# 每击落一只外星人,就加上相应的分数
if collisions:
stats.score += settings.alien_points
# 当外星人全都被打完时,删除现有子弹,并重新创建一群外星人
if len(aliens) == 0:
bullets.empty()
# 玩家等级 +1
stats.level += 1
# 绘制等级图形
scoreboard.prep_level()
# 新一批外星人的速度提高
settings.increase_speed()
create_fleet(settings, screen, aliens, ship)
至此,当我们击落外星人时,玩家的得分就会50 50 地提高,并实时显示在屏幕右上角。
4、优化1:正确计算每一只外星人的分数
在之前的博文中提到过,当我们想要快速检验一个新功能是否成功时,我们会临时地更改子弹宽度值,将其设置为和屏幕一样宽,这样发射子弹就会像推土机一样碾压外星人群,从而加快游戏进度。
而现在,我们碾压外星人时,一次只能加 50 分,这显然是不合理的,因为我们一颗子弹消灭了多只外星人,应该加 50 * 外星人数量 的分数才对。
基于这样的想法,我们修改刚刚碰撞检测中的代码
def check_bullet_alien_collide(bullets, aliens, settings, screen, ship, scoreboard, stats):
# 检测子弹队列中的每颗子弹是否击中外星人队列中的外星人
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
# 每击落一只外星人,就加上相应的分数
if collisions:
# collisions是一个字典,其中每一个键值对分别是 子弹:子弹击中的外星人列表
# 我们需要遍历每一个值,来判断击中了多少只外星人
for aliens in collisions.values():
stats.score += settings.alien_points * len(aliens)
# 重新绘制新的分数的图形
scoreboard.prep_score()
# 当外星人全都被打完时,删除现有子弹,并重新创建一群外星人
if len(aliens) == 0:
bullets.empty()
# 玩家等级 +1
stats.level += 1
# 绘制等级图形
scoreboard.prep_level()
# 新一批外星人的速度提高
settings.increase_speed()
create_fleet(settings, screen, aliens, ship)
需要注意的是,通过子弹队类和外星人队列碰撞生成的这个 collisions ,是一个字典,其中每一个键值对的内容分别是:
键:发生碰撞的子弹
值:该子弹碰撞到的外星人,以列表形式保存
因此,当我们一颗子弹击落多只外星人时,可以通过判断字典值中的列表长度来获取击落的外星人数量,然后提高相应的分数。这样不需要关心键,只需要关心值的长度,即列表的长度。
至此,当我们碾压外星人时,也会正确地计算每一只外星人的得分。
5、优化2:外星人分数提升
基于前面我们不断提升游戏难度的想法,当我们不断击落外星人群时,再想继续击落就会越来越难(速度提升了),那么是不是应该将外星人的分数值根据游戏难度也相应地提高呢?
这也就是我们将外星人分数值设置在 initialize_dynamic_settings() 方法中的原因了,因为外星人分数值会随着玩家重置游戏而变回初始值。
现在,我们需要为分数值的提升率设置属性,并将提升分数的操作封装到 increase_speed() 方法中
# 提升难度后外星人分数的提升率
self.score_scale = 1.5
# 当玩家击落一群外星人后,增加游戏难度的方法
def increase_speed(self):
# 提升飞船、外星人、子弹的速度
self.ship_speed *= self.speedup_scale
self.bullet_speed *= self.speedup_scale
self.alien_speed *= self.speedup_scale
# 提升每只外星人的分数
self.alien_points = int(self.alien_points * self.score_scale)
# 当玩家按下开始按钮后,重置游戏难度的方法
def initialize_dynamic_settings(self):
self.ship_speed = 1.5
self.bullet_speed = 3
self.alien_speed = 1
self.fleet_direction = 1
# 外星人的价值(击落一只外星人能得多少分)
self.alien_points = 50
这时再运行游戏,当玩家不断击落外星人后,每次击落的得分都会增加。
6、优化3:将得分标准化
很多街机类的游戏,它们的分数都是整十整百的,很少出现有个位数非0的情况,这是为了让玩家看起来更舒服。
另外,当玩家得分值很高时,理应按照国际货币表示的方法来显示,即每三位一个逗号。
现在,尝试将《外星人入侵》的玩家得分也设置成更加标准的表示法。
1、通过 round( , -1) 来实现分数整十化
2、通过 格式化字符串 来实现每三位一逗号
def prep_score(self):
# 将分数近似转换为10的倍数
round_score = round(self.stats.score, -1)
# 将数字分数转换成字符串分数,按照三位一逗号的形式
str_score = '{:,}'.format(round_score)
# 将字符串转换成 Surface对象
self.score_image = self.font.render(str_score, True, self.text_color, self.settings.background_color)
# 获取分数图形的坐标位置
self.score_rect = self.score_image.get_rect()
# 将分数图形绘制到屏幕右上角
self.score_rect.right = self.screen_rect.right - 20
self.score_rect.top = 20
此时,游戏屏幕上显示的就是标准化后的玩家得分了。
三、计分板的其他功能
除了记录玩家得分外,我们还想新增一点计分板功能。
比如玩家最高得分。
1、玩家最高得分
关于最高得分:
1、不会在玩家重新开始时被重置,因此需要将这个值设置在初始化方法中,而不能设置在重置方法中。
2、第一次游戏时,最高得分就是玩家得分。
3、最高得分显示在屏幕顶部中间,最显眼的位置。
class GameStats():
# 初始化游戏状态,设置玩家开局将有多少架飞船可以使用
def __init__(self, settings):
self.settings = settings
# 游戏最高分
self.high_score = 0
self.reset_stats()
绘制最高得分的逻辑,与我们绘制玩家得分的逻辑完全一致
def prep_high_score(self):
# 将最高分转换成10的倍数
high_score = int(round(self.stats.high_score, -1))
# 将最高分数字转换成字符串
str_high_score = '{:,}'.format(high_score)
# 将最高分字符串转换成 Surface对象
self.high_score_image = self.font.render(str_high_score, True, self.text_color, self.settings.background_color)
# 获取最高分图形的坐标
self.high_score_rect = self.high_score_image.get_rect()
# 将最高分图形绘制到屏幕顶部中间
self.high_score_rect.centerx = self.screen_rect.centerx
self.high_score_rect.top = self.score_rect.top
修改绘制计分板的代码,使其也能绘制最高得分
def show_scoreboard(self):
# 绘制当前分数
self.screen.blit(self.score_image, self.score_rect)
# 绘制最高分
self.screen.blit(self.high_score_image, self.high_score_rect)
最后,我们将在每次玩家得分提升后,判断当前玩家得分是否超过了最高得分呢,如果超过了,则更新最高得分
def check_bullet_alien_collide(bullets, aliens, settings, screen, ship, scoreboard, stats):
# 检测子弹队列中的每颗子弹是否击中外星人队列中的外星人
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
# 每击落一只外星人,就加上相应的分数
if collisions:
# collisions是一个字典,其中每一个键值对分别是 子弹:子弹击中的外星人列表
# 我们需要遍历每一个值,来判断击中了多少只外星人
for aliens in collisions.values():
stats.score += settings.alien_points * len(aliens)
# 重新绘制新的分数的图形
scoreboard.prep_score()
# 判断当前分数是否超过最高分
check_high_score(stats, scoreboard)
# 判断当前分数是否大于最高分的方法
def check_high_score(stats, scoreboard):
if stats.score > stats.high_score:
stats.high_score = stats.score
scoreboard.prep_high_score()
现在,我们就可以在屏幕顶部中间看到最高得分的信息了。
2、玩家等级
我们设计每当玩家击落一整群外星人后,都会提升一级自己的玩家等级。
同时,实时观察到玩家当前的等级信息。
1、玩家等级初始值为 1
2、每次重置游戏时,等级会重置
3、绘制逻辑与绘制分数相同,位置绘制在玩家得分的下方。
class GameStats():
# 初始化游戏状态,设置玩家开局将有多少架飞船可以使用
def __init__(self, settings):
self.settings = settings
# 游戏最高分
self.high_score = 0
self.reset_stats()
def reset_stats(self):
self.ship_left = self.settings.ship_limit
# 游戏刚开始时,处于非活跃状态
self.game_active = False
# 游戏初始化时,分数为0
self.score = 0
# 游戏初始化时,等级为1
self.level = 1
def prep_level(self):
str_level = 'Lv.' + str(self.stats.level)
self.level_image = self.font.render(str_level, True, self.text_color, self.settings.background_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
同时,需要修改绘制计分板的代码,使之能够绘制玩家等级
def show_scoreboard(self):
# 绘制当前分数
self.screen.blit(self.score_image, self.score_rect)
# 绘制最高分
self.screen.blit(self.high_score_image, self.high_score_rect)
# 绘制等级
self.screen.blit(self.level_image, self.level_rect)
另外,我们提升玩家等级的时机是在击落一群外星人后,所以需要修改 check_bullet_alien_collide() 方法
def check_bullet_alien_collide(bullets, aliens, settings, screen, ship, scoreboard, stats):
# 检测子弹队列中的每颗子弹是否击中外星人队列中的外星人
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
# 每击落一只外星人,就加上相应的分数
if collisions:
# collisions是一个字典,其中每一个键值对分别是 子弹:子弹击中的外星人列表
# 我们需要遍历每一个值,来判断击中了多少只外星人
for aliens in collisions.values():
stats.score += settings.alien_points * len(aliens)
# 重新绘制新的分数的图形
scoreboard.prep_score()
# 判断当前分数是否超过最高分
check_high_score(stats, scoreboard)
# 当外星人全都被打完时,删除现有子弹,并重新创建一群外星人
if len(aliens) == 0:
bullets.empty()
# 玩家等级 +1
stats.level += 1
# 绘制等级图形
scoreboard.prep_level()
# 新一批外星人的速度提高
settings.increase_speed()
create_fleet(settings, screen, aliens, ship)
最后,当玩家按下开始按钮重置游戏时,我们也需要重置玩家等级等信息。
# 玩家点击Play按钮后开始新游戏
def check_play_button(mouse_x, mouse_y, play_button, game_stats, aliens, bullets, settings, screen, ship, scoreboard):
button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
# 当玩家在游戏处于非活跃状态下点击Play按钮时
if button_clicked and not game_stats.game_active:
# 游戏难度初始化(重置)
settings.initialize_dynamic_settings()
# 游戏活跃时隐藏光标
pygame.mouse.set_visible(False)
# 重置游戏设置
game_stats.reset_stats()
# 绘制重置后的游戏分数、最高分、等级
scoreboard.prep_score()
scoreboard.prep_high_score()
scoreboard.prep_level()
# 将游戏切换回活跃状态
game_stats.game_active = True
# 清空子弹和外星人
aliens.empty()
bullets.empty()
# 创建一群新的外星人,并让飞船居中
create_fleet(settings, screen, aliens, ship)
ship.center_ship()
至此,我们就实现了计分板显示玩家等级的功能
4、余下的飞船数
每次游戏,都会限制玩家可使用的飞船数,因此我们想让玩家直观地看到自己还剩下多少架飞船可用。
我们可以将飞船类进行一些改造,使其继承自精灵类,目的是通过编组来管理多个飞船图形,使他们显示在屏幕左上角。
class Ship(Sprite):
# 初始化飞船,飞船刚出现时,是位于屏幕的中间靠最底边的位置
def __init__(self, screen, settings): # 这里需要传入屏幕,是因为我们设置飞船的坐标需要用到屏幕的坐标
# 父类初始化
super().__init__()
...
接着,我们需要在计分板类中定义一个可以绘制一系列飞船图形的方法
def prep_ships(self):
# 保存飞船的编组
self.ships = Group()
for ship_number in range(self.stats.ship_left):
ship = Ship(self.screen, self.settings)
ship.rect.left = 10 + ship_number * ship.rect.width
ship.rect.bottom = ship.rect.height
self.ships.add(ship)
然后,修改绘制计分板的代码,使之可以绘制飞船编组
def show_scoreboard(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)
# 绘制玩家剩余飞船
self.ships.draw(self.screen)
最后,在我们重置游戏时,也需要重新绘制剩余飞船的图形
# 玩家点击Play按钮后开始新游戏
def check_play_button(mouse_x, mouse_y, play_button, game_stats, aliens, bullets, settings, screen, ship, scoreboard):
button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
# 当玩家在游戏处于非活跃状态下点击Play按钮时
if button_clicked and not game_stats.game_active:
# 游戏难度初始化(重置)
settings.initialize_dynamic_settings()
# 游戏活跃时隐藏光标
pygame.mouse.set_visible(False)
# 重置游戏设置
game_stats.reset_stats()
# 绘制重置后的游戏分数、最高分、等级
scoreboard.prep_score()
scoreboard.prep_high_score()
scoreboard.prep_level()
scoreboard.prep_ships()
# 将游戏切换回活跃状态
game_stats.game_active = True
# 清空子弹和外星人
aliens.empty()
bullets.empty()
# 创建一群新的外星人,并让飞船居中
create_fleet(settings, screen, aliens, ship)
ship.center_ship()
至此,我们就实现了绘制玩家剩余飞船的功能。
四、总结
本篇博文为《外星人入侵》的完结篇,我们实现了这个游戏的所有功能,并封装了其功能代码和设置参数,当我们想要修改某个游戏元素的值时,只需要到设置模块中去修改就可以影响到整个游戏。
游戏的初始效果如下:
游戏进行过程的效果:
所有模块加起来有 707 行代码,按照PEP8的格式进行了空行,所以里面有大量的空行,且一个函数内部不同部分我也空了行,所以实际代码量也许不是很大。
但我在看书学习的过程中,学到了大型项目必备的思想:
1、代码封装
2、代码重构
这在一个项目刚开始设计时,也许作用不明显,甚至还会显得很冗长,但项目一旦开始复杂,就会显现优势,我们新增某个功能或者优化某个功能,都只需要找到相应的函数或类,进行代码修改即可。
这使得这个游戏,越做到后面,就越能跟上步伐,甚至很多地方,不看书都能知道应该修改哪里。这是之前的我所做不到的。
对于Python,我还有不少路要走,希望自己能从实际项目中收获更多。