Pygame 外星人入侵(10)计分板

引言

本篇博文是《外星人入侵》系列的完结篇,在之前的博文中,我们实现了游戏的绝大多数功能并将相关代码封装起来,方便修改。
在本完结篇中,我们将要实现的功能有二:
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,我还有不少路要走,希望自己能从实际项目中收获更多。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值