曾经沧海难为水,除却巫山不是云。|

Joey-Wang

园龄:4年3个月粉丝:17关注:0

📂Python
🔖Python
2021-03-18 03:09阅读: 1251评论: 0推荐: 0

Python项目实践1——外星人入侵

目标:使用Pygame开发一个《外星人入侵》的游戏。

GitHub地址:https://github.com/Angelia-Wang/alien_invasion

第十二章 武装飞船

首先要安装Pygame库:

pip install pygame

如果不行,就更新下pip:

conda update pip

建立一个名为alien_invasion的文件夹,我们之后的开发工作就在此文件夹中进行。

可以通过Git进行项目管理。

Git与GitHub使用指南

🌟 Pycharm使用Git以及GitHub的pull request流程

12.1 开始游戏项目

我们首先应创建一个Pygame窗口并能响应用户的输入。

12.1.1 涉及到的函数

导入的模块有Pygame、sys。

  1. pygame.init() 初始化背景设置,让Pygame能正常工作。

  2. pygame.display.set_mode((x,y)) 创建显示窗口。参数为元组,指定了游戏窗口的尺寸,单位为像素。返回值为surface,此处表示整个游戏窗口。

    在Pygame中,surface是屏幕的一部分,用于显示游戏元素。在此游戏中,每个元素(如外星人或飞船)都是一个surface。游戏每次重绘都会重绘所有surface。

  3. pygame.display.set_caption(string) 设置游戏窗口标题。参数为字符串。

  4. 对surface调用 fill((255,255,255),[rect]) 用某种颜色填充调用的 surface 的 rect 区域。参数1为颜色,如红色为(255,0,0);绿色为(0,255,0);蓝色为(0,0,255);个数字相同则为灰色,数字的大小决定灰度。参数2 为rect对象,可不填,不填时表示用某种颜色填充调用的 surface

  5. pygame.display.flip() 命令 Pygame 让最近绘制的屏幕可见。

  6. pygame.event.get() 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)。

    事件 是用户玩游戏时执行的操作,如按键or移动鼠标等。

  7. type() 事件的type方法能获取时间的类型。

  8. pygame.QUIT 当玩家单击游戏窗口的关闭按钮时,会检测到此事件。

  9. sys.exit() 退出游戏。

12.1.2 分析

了解了可以调用的函数后,我们在此步骤要做的就是:

  1. 创建游戏窗口(要设定基本的数据,如窗口颜色,窗口大小等)

    • 关于游戏的设置信息,可以单独创建一个设置类
  2. 获取用户每时每刻的输入并作出响应

    • 因为每时每刻的输入都要检测到,所以肯定要用循环,再循环中获取输入并作出响应

    • 获取输入——监听事件

    • 作出响应——对事件处理后更新屏幕

12.1.3 代码

setting.py

class Settings:
    """存储游戏《外星人入侵》中所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200  # 屏幕宽度,单位像素
        self.screen_height = 800  # 屏幕高度,单位像素
        self.bg_color = (230, 230, 230)  # 背景颜色

alien_invasion.py

import pygame
import sys
from settings import Settings


class AlienInvastion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化Pygame背景设置
        self.settings = Settings()
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height)) #创建显示窗口,返回值为surface对象
        pygame.display.set_caption("外星人入侵")  # 设置窗口标题
        

    def run_game(self):
        """开始游戏的主循环"""
        while True:
            for event in pygame.event.get():  # 监听事件
                if event.type == pygame.QUIT:
                    sys.exit()  # 退出游戏
            self.screen.fill(self.settings.bg_color)  # 设置surface背景颜色,此处surface为屏幕
            pygame.display.flip()  # 重绘屏幕


if __name__ == "__main__":
    ai = AlienInvastion()
    ai.run_game()

12.2 添加飞船图像

我们已经创建了Pygame的窗口且能响应用户的输入。现在我们需给用户一个操纵对象来进行游戏——飞船。

12.2.1 涉及到的函数

  1. pygame.image.load(string) 加载飞船图像。参数为图片地址,返回图像的位置surface。

    Pygame默认加载位图(.bmp),大多数图像为 .jpg、.png、.gif格式,可通过Photoshop等工具转换为位图。

  2. get_rect() 获取相应 surface 的属性 rect。

    Pygame之所以高效,是因为它能让你像处理矩形(rect对象)一样处理所有的游戏元素 surface,即使其形状并非矩形。

    处理 rect 对象时,可通过设置矩形四角和中心的x坐标、y坐标来指定矩形的位置。

    • 让游戏元素居中:设置相应 rect 对象的属性 center、centerx、centery
    • 让游戏元素与屏幕边缘对齐:使用属性 top、bottom、left、right 或 midtop、midbottom、midleft、midright
    • 要调整游戏元素的水平或垂直位置,可使用属性 x 和 y,分别是相应矩形左上角的 x 坐标和 y 坐标。

    ⚠️rect的x等属性只能存储整数值。

    Pygame中,原点 (0, 0) 位于屏幕左上角,向右下移动坐标值变大。

  3. blit(surface对象, rect对象) 将某surface (调用者)中的 surface 对象绘制到 rect 对象指定的位置。

    self.screen.blit(self.image, self.rect) # 代码中的例子
    

12.2.2 分析

涉及到两步:

  1. 创建飞船对象(涉及到飞船图片、飞船在屏幕中的位置)
  2. 将飞船添加到屏幕中

12.2.3 代码

我们先在当前 alien_invasion 文件夹中新建 images 文件夹,并存放飞船图片 ship.bmp。

ship.py

import pygame


class Ship:
    """管理飞船的类"""

    def __init__(self, ai_game):  # 参数:引用self和指向当前AlienInvasion实例的引用,这让Ship能访问AlienInvasion中定义的所有游戏资源
        """初始化飞船并设置其初始位置"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()  # get_rect()访问屏幕的属性rect

        # 加载飞船图片并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对每艘新飞船,都将其放在屏幕底部的中央
        self.rect.midbottom = self.screen_rect.midbottom

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

alien_invasion.py

import sys
import pygame

from settings import Settings
from ship import Ship


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.ship = Ship(self)  # !!!!!!!
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            """响应按键和鼠标事件"""
            for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
                if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                    sys.exit()  # 退出游戏
            # 每次循环时都重绘屏幕
            """更新屏幕上的图像,并切换到新屏幕"""
            self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
            self.ship.blitme()  #  !!!!!!! 绘制飞船
            pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

12.3 重构 run_game()方法

在大型项目中经常涉及到重构代码问题,重构目的在于简化既有代码的结构,使其更容易扩展。

重构某方法,可将其拆为多个 辅助方法辅助方法在类中执行任务,不通过实例调用,以单个下划线打头。

12.3.1 分析

我们目前编写的代码中,可以预见在 run_game() 方法中,随着相应用户按键和鼠标的事件操作越来越细化,代码会越来越长。所以我们需对这个方法进行重构。

目前来看此方法中的 while 循环中的工作可分为两个部分:监听用户事件,重绘屏幕。因此我们可将 run_game() 方法划分为两个辅助方法:

  1. _check_events() 进行事件管理
  2. _update_screen() 更新屏幕

12.3.2 代码

alien_invasion.py

import sys
import pygame

from settings import Settings
from ship import Ship


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.ship = Ship(self)
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

12.4 驾驶飞船

我们现在已经有了飞船,接下来要做的就是让飞船能响应用户的操作(左右移动,但不能移出屏幕外)。

另外我们还希望:提高飞船的速度;按下q按键,退出游戏;将游戏全屏显示。

12.4.1 涉及到的函数

  1. pygame.KEYDOWN 每次按键都会被注册为一个KEYDOWN事件。

  2. pygame.FULLSCREE 参数,全屏参数。

    pygame.display.set_mode((0, 0), pygame.FULLSCREE) 能生成一个覆盖整个显示器的屏幕

12.4.2 分析

  1. 按下左右按键控制飞船移动,则要监听键盘事件,监听到则更改飞船的rect对象的坐标 x
    • 要使飞船能连续移动,则光监听按键 pygame.KEYDOWN 是不够的的,要记录键盘按下和松开之间的状态。所以我们在 Ship 类中定义两个属性 moving_right、moving_left,来判断是否应该左右移动。根据二者True还是False更新飞船位置。
    • 更新位置前,要判断当前位置是否会超出屏幕,不会才更新位置。
    • 因为要增加飞船移动的速度,则我们需要给飞船速度一个变量方便更改,若飞船移动的单位路程(速度)为小数,而rect对象的x等属性只能是整数,则为了准确性,我们应用float计算,最后赋值给rect对象。
  2. 按下q按键,退出游戏;将游戏全屏显示等功能都较简单,此处不赘述。
  3. 可以预见,_check_events() 方法由于响应按键的不同将会越来越复杂,所以我们对其进行解构,分为两个辅助方法:
    1. _check_keydown_events() 处理按键事件
    2. _check_keyup_events() 处理松开按键事件

12.4.3 代码

ship.py

import pygame

from settings import Settings


class Ship:
    """管理飞船的类"""

    def __init__(self, ai_game):  # 参数:引用self和指向当前AlienInvasion实例的引用,这让Ship能访问AlienInvasion中定义的所有游戏资源
        """初始化飞船并设置其初始位置"""
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()  # get_rect()访问屏幕的属性rect

        # 加载飞船图片并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对每艘新飞船,都将其放在屏幕底部的中央
        self.rect.midbottom = self.screen_rect.midbottom
        self.x = float(self.rect.x)  # x属性存储小数值,因为rect的x等属性只能存储整数值

        # 移动标志
        self.moving_right = False
        self.moving_left = False

    def blitme(self):
        """在指定位置绘制飞船"""
        # print(self.rect.x,self.rect.y)
        self.screen.blit(self.image, self.rect)

    def update(self):
        if self.moving_right and self.rect.right < self.screen_rect.right:
            self.x += self.settings.ship_speed
        elif self.moving_left and self.rect.left > 0:
            self.x -= self.settings.ship_speed
        self.rect.x = self.x  # 更新rect值

setting.py

class Settings:
    """存储游戏《外星人入侵》中所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200  # 屏幕宽度,单位像素
        self.screen_height = 800  # 屏幕高度,单位像素
        self.bg_color = (230, 230, 230)  # 背景颜色

        # 飞船设置
        self.ship_speed = 1.5

alien_invasion.py

import sys
import pygame

from settings import Settings
from ship import Ship


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)  # 全屏
        self.settings.screen_width = self.screen.get_rect().width  # 更新settings中配置
        self.settings.screen_height = self.screen.get_rect().height
        self.ship = Ship(self)
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            self.ship.update()
            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏
            elif event.type == pygame.KEYDOWN:  # 玩家每次按键都被注册为一个KEYDOWN事件
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)

    def _check_keydown_events(self, event):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_q:
            sys.exit()


    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

12.5 射击

我们的用户目前已经能操控飞船左右移动了,接下来就可以设计飞船发射子弹的功能了。

12.5.1 涉及到的函数

  1. pygame.Rect(x,y,width,height) 创建一个矩形,参数1~4分别表示矩形左上角的x坐标、y坐标以及矩形的宽度和高度。

  2. pygame.draw.rect(surface, color, rect) 使用color的颜色填充rect对象占据的surface(可以是屏幕)部分。

    # 二种写法效果相同,都会在屏幕的rect区域填充color
    self.screen.fill(self.button_color, self.rect)   
    pygame.draw.rect(self.screen, self.button_color, self.rect)
    
  3. pygame.sprite.Group() 创建 pygame.sprite.Group 类的一个实例。是一个编组(group),类似于列表,但提供了有助于开发游戏的额外功能。

  4. 对编组调用update() 方法时,编组会自动对其中的每个精灵(sprite)调用update()

    我们可编写继承 Sprite 类的子类,重写update() 方法,这样就可以将游戏中相关元素编组,进而通过update同时操作编组中的所有元素。

    引入Sprite 类:from pygame.sprite import Sprite

  5. 对编组调用 add(sprite) 方法,可将某精灵添加到编组中。此方法类似于append(),不过是专门为Pygame编组编写的。

  6. 对编组调用 remove(sprite) 方法,可删除编组中的某精灵。

  7. 对编组调用 sprites() 方法,返回一个列表,其中包含编组中的所有精灵。

12.5.2 分析

我们希望用户按下空格就能发射子弹,对能发射的子弹的最大数目要做出一定限制。

各个技术点如下:

  1. 创建子弹类——因为用户能发射多枚子弹,且子弹发出后要能不断射出(即子弹的位置每时每刻都要改变),所以我们可以考虑编写继承自 Sprite 类的子弹类 Bullet,这样通过实例化pygame.sprite.Group 类,我们就能得到一个子弹编组,可通过 add()remove()方法添加、删除子弹。也能通过 update()方法统一对子弹的变化进行管理。
    1. 用户按下空格后,若当前子弹编组中子弹的数目 < 能发射子弹的最大数目,则add()
    2. 子弹抵达屏幕顶端后会消失,我们需要将这些无效的子弹自编组中删除,否则会持续消耗内存及CPU处理能力。这需遍历编组中的所有子弹,通过判断子弹的 rect.buttom 位置是否<=0,判断是否remove()
    3. 子弹需要不断射出。则可在 Bullet 类中重写 update()方法,改变子弹的位置。每次重绘屏幕前对子弹编组调用update() 方法,则编组会自动对其中的每颗子弹调用update()
  2. 子弹不是图片,是创建的矩形,则要调用pygame.Rect(),要将此矩形绘制在屏幕上,要调用pygame.draw.rect()

则大体流程是:

  1. 在 Settings 类中设定子弹的各个设置,包括子弹颜色、子弹宽高、发射子弹的最大数目
  2. 编写子弹类,子弹类要创建代表子弹的矩形属性,继承Sprite类,重写update()方法,编写在屏幕上绘制子弹的方法。
  3. 在 AlienInvasion类的 __init__()方法中创建子弹编组,run_game()中需要监听用户按下空格事件创建子弹,不断更新子弹位置,最后绘制屏幕。

12.5.3 代码

setting.py

class Settings:
    """存储游戏《外星人入侵》中所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200  # 屏幕宽度,单位像素
        self.screen_height = 800  # 屏幕高度,单位像素
        self.bg_color = (230, 230, 230)  # 背景颜色

        # 飞船设置
        self.ship_speed = 1.5

        # 子弹设置
        self.bullet_speed = 1.0
        self.bullet_with = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3  # 最大子弹数

bullet.py

import pygame
from pygame.sprite import Sprite


class Bullet(Sprite):
    """管理飞船所发射子弹的类"""

    def __init__(self, ai_game):
        """在飞船当前位置创建一个子弹对象"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.color = self.settings.bullet_color

        # 在(0,0)处创建一个表示子弹的矩形,再设置正确的位置
        self.rect = pygame.Rect(0, 0, self.settings.bullet_with, self.settings.bullet_height)
        self.rect.midtop = ai_game.ship.rect.midtop

        # 存储用小数表示的子弹位置
        self.y = float(self.rect.y)

    def update(self):
        """向上移动子弹"""
        # 更新表示子弹位置的小数值
        self.y -= self.settings.bullet_speed
        self.rect.y = self.y

    def draw_bullet(self):
        """在屏幕上绘制子弹"""
        pygame.draw.rect(self.screen, self.color, self.rect)

alien_invasion.py

import sys
import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        # self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)  # 全屏
        # self.settings.screen_width = self.screen.get_rect().width  # 更新settings中的配置
        # self.settings.screen_height = self.screen.get_rect().height
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()  # 创建用于存储子弹的编组
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            self.ship.update()
            self._update_bullets()

            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏
            elif event.type == pygame.KEYDOWN:  # 玩家每次按键都被注册为一个KEYDOWN事件
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)

    def _check_keydown_events(self, event):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()
        elif event.key == pygame.K_q:
            sys.exit()

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)  # !! 别忘了传入 ai_game
            self.bullets.add(new_bullet)

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹"""
        self.bullets.update()  # 对编组调用update()时,编组自动对其中的每个精灵调用 update()

        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        # print(len(self.bullets))

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

使用for循环遍历列表(或Python编组)时,Python 要求列表的长度在整个循环中需保持不变。

所以不能在for循环遍历的列表/编组中删除元素,若要删除,可遍历列表/编组的副本。

如上Eg: for bullet in self.bullets.copy():

第十三章 外星人来了

开发大型项目时,要在进入每个开发阶段之前回顾下开发计划,搞清楚接下来要通过编写代码完成哪些任务。

本章节涉及:

  1. 研究既有代码,确定实现新功能前是否要重构。
  2. 在屏幕左上角添加一个外星人,并指定合适的边距。
  3. 根据第一个外星人的边距和屏幕尺寸计算屏幕上可容纳多少个外星人。编写一个循环来创建一系列外星人,使其填满屏幕的上半部分。
  4. 让外星人群向两边和下方移动,直到外星人被全部击落、有外星人撞到飞船或有外星人抵达屏幕底端。
    • 如果整群外星人都被击落,将再创建一群外星人。
    • 如果有外星人撞到了飞船或抵达屏幕底端,将销毁飞船并创建一群外星人。
  5. 限制玩家可用的飞船数量。配给的飞船用完之后,游戏将结束。

我们将在实现功能的同时,完善这个计划。

在项目添加新功能前,还应审核既有代码。对混乱/低效的代码进行清理,有必要时重构代码。

13.1 创建一群外星人

在images文件夹中添加外星人位图 alien.bmp,我们最终希望达到的效果如图所示:

image-20210317023900293

12.1.1 涉及到的函数

  1. 对编组调用 draw(surface) 方法,将编组中的每个元素绘制到surface上,具体位置由元素的属性rect指定。

13.1.2 分析

  1. 编写外星人类

  2. 计算外星人在屏幕上的位置,我们需要预留第一排外星人到屏幕的距离,以及最后排外星人到飞船的距离,以及左右两列外星人到屏幕的距离。

    image-20210317025817486

13.1.3 代码

alien.py

import pygame
from pygame.sprite import Sprite


class Alien(Sprite):
    """表示单个外星人的类"""

    def __init__(self, ai_game):
        """初始化外星人并设置其初始位置"""
        super().__init__()
        self.screen = ai_game.screen

        # 加载外星人图像并设置其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)
        self.y = float(self.rect.y)

alien_invasion.py

import sys
import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet
from alien import Alien


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        # self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)  # 全屏
        # self.settings.screen_width = self.screen.get_rect().width  # 更新settings中的配置
        # self.settings.screen_height = self.screen.get_rect().height
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()  # 创建用于存储子弹的编组
        self.aliens = pygame.sprite.Group()  # 创建用于存储外星人的编组
        self._create_fleet()
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            self.ship.update()
            self._update_bullets()

            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏
            elif event.type == pygame.KEYDOWN:  # 玩家每次按键都被注册为一个KEYDOWN事件
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)

    def _check_keydown_events(self, event):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()
        elif event.key == pygame.K_q:
            sys.exit()

    def _create_fleet(self):
        """创建外星人群"""
        # 创建第一个外星人并计算一行可容纳多少个外星人
        alien = Alien(self)
        alien_width = alien.rect.width
        alien_height = alien.rect.height
        available_space_x = self.settings.screen_width - alien_width
        available_space_y = self.settings.screen_height - 3 * alien_height - self.ship.rect.height
        num_aliens_x = available_space_x // (2 * alien_width)
        num_aliens_rows = available_space_y // (2 * alien_height)

        # 创建外星人群
        for row_number in range(num_aliens_rows):
            for alien_number in range(num_aliens_x):
                self._create_alien(alien_number, row_number, alien_width, alien_height)

    def _create_alien(self, alien_number, row_number, alien_width, alien_height):
        """创建一个外星人群"""
        alien = Alien(self)
        alien.x = alien_width + alien_number * alien_width * 2
        alien.y = alien_height + row_number * alien_height * 2
        alien.rect.x = alien.x
        alien.rect.y = alien.y
        self.aliens.add(alien)

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)  # !! 别忘了传入 ai_game
            self.bullets.add(new_bullet)

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹"""
        self.bullets.update()  # 对编组调用update()时,编组自动对其中的每个精灵调用 update()

        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        # print(len(self.bullets))

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)
        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

13.2 让外星人群移动

添加完外星人群后,我们想让外星人群在屏幕上向右移动,撞到屏幕边缘后向下移动一定量,再沿相反方向移动(即向下移动的同时兼顾左右移动)。我们将不断移动所有外星人,直到外星人被全部消灭,或有外星人撞上飞船或抵达屏幕底端。

13.2.1 分析

  1. 我们需要外星人移动,则需在 Alien类中重写 update()方法,然后在 run_game() 的循环中调用。
  2. 我们需要外星人群碰到屏幕边沿时更换移动方向,则可在 Settings 类 中设置外星人群的移动方向 self.fleet_direction = 1(1表示向右移动,-1表示向左移动),一旦有外星人碰到边缘即 self.rect.right >= self.screen.get_rect().right or self.rect.left <= 0 ,则 self.fleet_direction *= -1
  3. 我们需要外星人群碰到屏幕边缘后向下移动,则可在 Settings 类中设置外星人群向下移动的速度 self.fleet_drop_speed = 10,一旦有外星人碰到边缘则将外星人群整体 y 坐标增加 self.fleet_drop_speed

13.2.2 代码

setting.py

class Settings:
    """存储游戏《外星人入侵》中所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200  # 屏幕宽度,单位像素
        self.screen_height = 800  # 屏幕高度,单位像素
        self.bg_color = (230, 230, 230)  # 背景颜色

        # 飞船设置
        self.ship_speed = 1.5

        # 子弹设置
        self.bullet_speed = 1.0
        self.bullet_with = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3  # 最大子弹数

        # 外星人设置
        self.alien_speed = 1.0
        self.fleet_drop_speed = 10  # 行下移动的速度
        self.fleet_direction = 1  # 1表示往右移动,-1表示往左移动

alien.py

import pygame
from pygame.sprite import Sprite


class Alien(Sprite):
    """表示单个外星人的类"""

    def __init__(self, ai_game):
        """初始化外星人并设置其初始位置"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.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)
        self.y = float(self.rect.y)

    def check_edge(self):
        """检查外星人是否位于屏幕边缘,是则返回True"""
        if self.rect.right >= self.screen.get_rect().right or self.rect.left <= 0:
            return True

    def update(self):
        """向左或向右移动外星人"""
        self.x += self.settings.alien_speed * self.settings.fleet_direction
        self.rect.x = self.x

alien_invasion.py

import sys
import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet
from alien import Alien


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()  # 创建用于存储子弹的编组
        self.aliens = pygame.sprite.Group()  # 创建用于存储外星人的编组
        self._create_fleet()
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            self.ship.update()
            self._update_bullets()
            self._update_aliens()

            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏
            elif event.type == pygame.KEYDOWN:  # 玩家每次按键都被注册为一个KEYDOWN事件
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)

    def _check_keydown_events(self, event):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()
        elif event.key == pygame.K_q:
            sys.exit()

    def _create_fleet(self):
        """创建外星人群"""
        # 创建第一个外星人并计算一行可容纳多少个外星人
        alien = Alien(self)
        alien_width = alien.rect.width
        alien_height = alien.rect.height
        available_space_x = self.settings.screen_width - alien_width
        available_space_y = self.settings.screen_height - 3 * alien_height - self.ship.rect.height
        num_aliens_x = available_space_x // (2 * alien_width)
        num_aliens_rows = available_space_y // (2 * alien_height)

        # 创建外星人群
        for row_number in range(num_aliens_rows):
            for alien_number in range(num_aliens_x):
                self._create_alien(alien_number, row_number, alien_width, alien_height)

    def _create_alien(self, alien_number, row_number, alien_width, alien_height):
        """创建一个外星人群"""
        alien = Alien(self)
        alien.x = alien_width + alien_number * alien_width * 2
        alien.y = alien_height + row_number * alien_height * 2
        alien.rect.x = alien.x
        alien.rect.y = alien.y
        self.aliens.add(alien)

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)  # !! 别忘了传入 ai_game
            self.bullets.add(new_bullet)

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹"""
        self.bullets.update()  # 对编组调用update()时,编组自动对其中的每个精灵调用 update()

        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        # print(len(self.bullets))

    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新外星人群中所有外星人的位置"""
        self._check_fleet_edge()
        self.aliens.update()  # 移动

    def _check_fleet_edge(self):
        """有外星人到达边缘时采取相应的措施"""
        for alien in self.aliens.sprites():
            if alien.check_edge():
                self._change_fleet_direction() #下移并改变方向
                break

    def _change_fleet_direction(self):
        """将整群外星人下移,并改变方向"""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)
        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

13.3 射杀外星人

飞船可以发射子弹,我们也已经创建了外星人群,接下来就要实现子弹碰撞外星人后的射杀效果。碰撞即指游戏元素重叠在一起。

当外星人全部被射杀,则删除现有子弹并创建一群新的外星人。

13.3.1 涉及到的函数

  1. pygame.sprite.groupcollide(编组1, 编组2, boolean, boolean):将编组1中每个元素的 rect 同编组2中每个元素的 rect 进行比较,返回一个字典,其中包含 rect 发生重叠的键值对,键是编组1中的元素,值是编组2中的元素列表。参数3、4分别指当二者重叠时是否要删除编组1及编组2中的对应元素。
  2. 对编组调用 empty() 方法,清空编组中的元素。

13.3.2 分析

  1. 在 _update_bullets() 方法中,检查子弹与外星人是否发生碰撞,若有,则删除此外星人和子弹。此外还要检查外星人是否都被射杀,若是则清空子弹 bullets.empty(),再创建一群新的外星人 _create_fleet()。

13.3.3 代码

alien_invasion.py

import sys
import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet
from alien import Alien


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()  # 创建用于存储子弹的编组
        self.aliens = pygame.sprite.Group()  # 创建用于存储外星人的编组
        self._create_fleet()
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            self.ship.update()
            self._update_bullets()
            self._update_aliens()

            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏
            elif event.type == pygame.KEYDOWN:  # 玩家每次按键都被注册为一个KEYDOWN事件
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)

    def _check_keydown_events(self, event):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()
        elif event.key == pygame.K_q:
            sys.exit()

    def _create_fleet(self):
        """创建外星人群"""
        # 创建第一个外星人并计算一行可容纳多少个外星人
        alien = Alien(self)
        alien_width = alien.rect.width
        alien_height = alien.rect.height
        available_space_x = self.settings.screen_width - alien_width
        available_space_y = self.settings.screen_height - 3 * alien_height - self.ship.rect.height
        num_aliens_x = available_space_x // (2 * alien_width)
        num_aliens_rows = available_space_y // (2 * alien_height)

        # 创建外星人群
        for row_number in range(num_aliens_rows):
            for alien_number in range(num_aliens_x):
                self._create_alien(alien_number, row_number, alien_width, alien_height)

    def _create_alien(self, alien_number, row_number, alien_width, alien_height):
        """创建一个外星人群"""
        alien = Alien(self)
        alien.x = alien_width + alien_number * alien_width * 2
        alien.y = alien_height + row_number * alien_height * 2
        alien.rect.x = alien.x
        alien.rect.y = alien.y
        self.aliens.add(alien)

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)  # !! 别忘了传入 ai_game
            self.bullets.add(new_bullet)

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹"""
        self.bullets.update()  # 对编组调用update()时,编组自动对其中的每个精灵调用 update()

        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        # print(len(self.bullets))

        self._check_bullet_alien_collisions()  # 射杀外星人

    def _check_bullet_alien_collisions(self):
        """检查子弹和为行人的碰撞"""
        # 检查是否有子弹击中了外星人,若有则删除相应的子弹和外星人
        collisions = pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)

        # 若外星人群被消灭,则删除现有的子弹并新建一群外星人
        if not self.aliens:
            self.bullets.empty()
            self._create_fleet()

    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新外星人群中所有外星人的位置"""
        self._check_fleet_edge()
        self.aliens.update()

    def _check_fleet_edge(self):
        """有外星人到达边缘时采取相应的措施"""
        for alien in self.aliens.sprites():
            if alien.check_edge():
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        """将整群外星人下移,并改变方向"""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)
        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

13.4 结束游戏

我们设定结束游戏的条件是:

玩家没有在足够短的时间内将外星人消灭,导致有外星人撞到飞船或抵达屏幕底端,飞船将被摧毁。玩家可用的总飞船数目是被限定的,在玩家用光所有飞船后,游戏结束。

13.4.1 涉及到的函数

  1. pygame.sprite.spritecollideany(sprite, 编组): 检查编组是否有成员与精灵(sprite)发生碰撞,会遍历编组,并返回与精灵发生碰撞的第一个编组成员。若没有发生碰撞,则返回 None。

13.4.2 分析

  1. 我们编写一个统计类,用于跟踪游戏统计信息,其中包含玩家能使用的飞船数目。

  2. 在 _update_aliens() 方法中检测外星人是否与飞船发生碰撞,若是,则将玩家能使用的飞船数目-1,清空余下的外星人和子弹,创建一群新的外星人并将飞船放到屏幕底部中央,暂停半秒钟。

  3. 在 _update_aliens() 方法中检测是否有外星人到达屏幕底端,若有则按照步骤2发生碰撞时一样处理。

  4. 若玩家能使用的飞船数目为0,则游戏结束。故我们可在统计类中设置游戏状态参数 game_active,在 run_game() 方法中通过此参数判断是否游戏已经结束。

    ⚠️ run_game() 方法循环中,任何情况都需要调用 _check_event() 方法,即使游戏处于非活动状态,例如我们仍需要直到玩家是否按下Q键退出游戏窗口,是否选择重新开始游戏(此时需重绘屏幕)。

13.4.3 代码

game_stats.py

class GameStats:
    """跟踪游戏的统计信息"""

    def __init__(self, ai_game):
        """初始化统计信息"""
        self.settings = ai_game.settings
        self.reset_stats()
        self.game_active = True  # 游戏刚启动时处于活动状态

    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.settings.ship_limit

ship.py

import pygame


class Ship:
    """管理飞船的类"""

    def __init__(self, ai_game):
        """初始化飞船并设置其初始位置"""
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()  # get_rect()访问屏幕的属性rect

        # 加载飞船图片并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对每艘新飞船,都将其放在屏幕底部的中央
        self.rect.midbottom = self.screen_rect.midbottom
        self.x = float(self.rect.x)  # x属性存储小数值,因为rect的x等属性只能存储整数值

        # 移动标志
        self.moving_right = False
        self.moving_left = False

    def blitme(self):
        """在指定位置绘制飞船"""
        # print(self.rect.x,self.rect.y)
        self.screen.blit(self.image, self.rect)

    def update(self):
        if self.moving_right and self.rect.right < self.screen_rect.right:
            self.x += self.settings.ship_speed
        elif self.moving_left and self.rect.left > 0:
            self.x -= self.settings.ship_speed
        self.rect.x = self.x

    def center_ship(self):
        """让飞船在屏幕底部中央"""
        self.rect.midbottom = self.screen_rect.midbottom
        self.x = float(self.rect.x)

alien_invasion.py

import sys
from time import sleep
import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet
from alien import Alien
from game_stats import GameStats


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.stats = GameStats(self)  # 创建存储游戏统计信息的实例
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()  # 创建用于存储子弹的编组
        self.aliens = pygame.sprite.Group()  # 创建用于存储外星人的编组
        self._create_fleet()
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            if self.stats.game_active:
                self.ship.update()
                self._update_bullets()
                self._update_aliens()

            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏
            elif event.type == pygame.KEYDOWN:  # 玩家每次按键都被注册为一个KEYDOWN事件
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)

    def _check_keydown_events(self, event):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()
        elif event.key == pygame.K_q:
            sys.exit()

    def _create_fleet(self):
        """创建外星人群"""
        # 创建第一个外星人并计算一行可容纳多少个外星人
        alien = Alien(self)
        alien_width = alien.rect.width
        alien_height = alien.rect.height
        available_space_x = self.settings.screen_width - alien_width
        available_space_y = self.settings.screen_height - 3 * alien_height - self.ship.rect.height
        num_aliens_x = available_space_x // (2 * alien_width)
        num_aliens_rows = available_space_y // (2 * alien_height)

        # 创建外星人群
        for row_number in range(num_aliens_rows):
            for alien_number in range(num_aliens_x):
                self._create_alien(alien_number, row_number, alien_width, alien_height)

    def _create_alien(self, alien_number, row_number, alien_width, alien_height):
        """创建一个外星人群"""
        alien = Alien(self)
        alien.x = alien_width + alien_number * alien_width * 2
        alien.y = alien_height + row_number * alien_height * 2
        alien.rect.x = alien.x
        alien.rect.y = alien.y
        self.aliens.add(alien)

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)  # !! 别忘了传入 ai_game
            self.bullets.add(new_bullet)

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹"""
        self.bullets.update()  # 对编组调用update()时,编组自动对其中的每个精灵调用 update()

        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        # print(len(self.bullets))

        self._check_bullet_alien_collisions()  # 射杀外星人

    def _check_bullet_alien_collisions(self):
        """检查子弹和为行人的碰撞"""
        # 检查是否有子弹击中了外星人,若有则删除相应的子弹和外星人
        collisions = pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)

        # 若外星人群被消灭,则删除现有的子弹并新建一群外星人
        if not self.aliens:
            self.bullets.empty()
            self._create_fleet()

    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新外星人群中所有外星人的位置"""
        self._check_fleet_edge()
        self.aliens.update()

        # 检测外星人和飞船之间的碰撞
        if pygame.sprite.spritecollideany(self.ship, self.aliens):
            self._ship_hit()

        # 检查是否有外星人到达屏幕底端
        self._check_aliens_bottom()

    def _check_fleet_edge(self):
        """有外星人到达边缘时采取相应的措施"""
        for alien in self.aliens.sprites():
            if alien.check_edge():
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        """将整群外星人下移,并改变方向"""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1

    def _ship_hit(self):
        """飞船被外星人撞到"""

        if self.stats.ships_left > 0:
            # 将ship_left - 1
            self.stats.ships_left -= 1
            
            # 清空剩余的外星人和子弹
            self.aliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放在屏幕底部中央
            self._create_fleet()
            self.ship.center_ship()

            # 暂停半秒钟
            sleep(0.5)
        else:
            self.stats.game_active = False

    def _check_aliens_bottom(self):
        """检查是否有外星人到达了屏幕底端"""
        for alien in self.aliens.sprites():
            if alien.rect.bottom >= self.screen.get_rect().bottom:
                self._ship_hit()  # 像飞船被撞到一样处理
                break

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)
        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

第十四章 计分

14.1 添加 Play 按钮

我们目前已实现:

  1. 运行项目后游戏自动开始,玩家可操纵飞船左右移动并发射子弹射击外星人,外星人全部被射杀后会重建一群外星人。

  2. 若外星人碰撞到飞船 or 到达屏幕底部则当前飞船销毁。

  3. 飞船销毁后本轮游戏重新开始,飞船销毁次数达到上限时游戏结束,当前界面停止。

本章节我们需添加一个 Play 按钮,运行项目后先显示 Play 按钮 ,玩家点击按钮后游戏才开始,游戏结束后再次显示 Play 按钮。

14.1.1 涉及到的函数

  1. pygame.font.SysFont(参数1,参数2):将文本渲染为指定字体并返回。参数1为 None 时 Pygame使用默认字体,参数2指定文本的字号。
  2. 对字体调用render(string, boolean, color, bg_color):将存储在 string 中的文本转换为图像并返回。参数2指定是否开启关闭反锯齿功能(能让文本的边缘更平滑),参数3表示字体颜色,参数4表示背景颜色,若没有指定背景颜色则默认使用透明背景。
  3. pygame.MOUSEBUTTONDOWN:单击游戏窗口的任何地方都将检测到此事件。
  4. pygame.mouse.get_pos():返回值为一个元组,其中包含玩家单击时鼠标的 x 坐标和 y 坐标。
  5. 对 rect 调用collidepoint(参数)方法:检查鼠标单击位置是否在 rect 内。参数为元组,其中包含玩家单击时鼠标的 x 坐标和 y 坐标。
  6. pygame.mouse.set_visible(boolean):是否在游戏窗口内隐藏鼠标光标。

14.1.2 分析

  1. 因为游戏不再是一运行项目就开始,故 GameStats 类中 stats.game_active 初始化为 False。

  2. 创建一个按钮类 Button,其中定义表示按钮的矩形框,和按钮上要显示的文字。

    Pygame处理文本的方式是:将要显示的字符渲染为图像。 故我们需要渲染文本字体后将其转换为图片,再绘制在屏幕上。

  3. 在更新屏幕方法 _update_screen() 中,当我们检测到标志 stats.game_active 为 False 时就显示按钮。

  4. 在响应用户事件的方法 _check_events() 中,我们需检测用户是否按下按钮,若用户单击鼠标的位置在按钮的 rect 区域内,且此时处于非游戏状态 stats.game_active = False,则开始游戏。

14.1.3 代码

game_status.py

class GameStats:
    """跟踪游戏的统计信息"""

    def __init__(self, ai_game):
        """初始化统计信息"""
        self.settings = ai_game.settings
        self.reset_stats()
        self.game_active = False  # 游戏刚启动时处于非活动状态

    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.settings.ship_limit

button.py

import pygame.font


class Button:
    def __init__(self, ai_game, msg):
        """初始化按钮属性"""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()

        # 设置按钮的尺寸和其他属性
        self.width, self.height = 200, 50
        self.button_color = (0, 0, 255)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)  # None让Pygame使用默认字体,48指定字体字号

        # 创建按钮的rect对象,并使其居中
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center

        # 按钮的标签只需创建一次
        self._prep_msg(msg)

    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

    def draw_button(self):
        """绘制一个用颜色填充的按钮,再绘制文本"""
        self.screen.fill(self.button_color, self.rect)   
        # 或 pygame.draw.rect(self.screen, self.button_color, self.rect)
        
        self.screen.blit(self.msg_image, self.msg_image_rect)

alien.invasion.py

import sys
from time import sleep
import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet
from alien import Alien
from game_stats import GameStats
from button import Button


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.stats = GameStats(self)  # 创建存储游戏统计信息的实例
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()  # 创建用于存储子弹的编组
        self.aliens = pygame.sprite.Group()  # 创建用于存储外星人的编组
        self.play_button = Button(self, "Play")  # 创建 Play 按钮
        self._create_fleet()
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            if self.stats.game_active:
                self.ship.update()
                self._update_bullets()
                self._update_aliens()

            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏
            elif event.type == pygame.KEYDOWN:  # 玩家每次按键都被注册为一个KEYDOWN事件
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)
            elif event.type == pygame.MOUSEBUTTONDOWN:  # 玩家点击 Play 按钮
                mouse_pos = pygame.mouse.get_pos()
                self._check_play_button(mouse_pos)

    def _check_keydown_events(self, event):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()
        elif event.key == pygame.K_q:
            sys.exit()

    def _create_fleet(self):
        """创建外星人群"""
        # 创建第一个外星人并计算一行可容纳多少个外星人
        alien = Alien(self)
        alien_width = alien.rect.width
        alien_height = alien.rect.height
        available_space_x = self.settings.screen_width - alien_width
        available_space_y = self.settings.screen_height - 3 * alien_height - self.ship.rect.height
        num_aliens_x = available_space_x // (2 * alien_width)
        num_aliens_rows = available_space_y // (2 * alien_height)

        # 创建外星人群
        for row_number in range(num_aliens_rows):
            for alien_number in range(num_aliens_x):
                self._create_alien(alien_number, row_number, alien_width, alien_height)

    def _create_alien(self, alien_number, row_number, alien_width, alien_height):
        """创建一个外星人群"""
        alien = Alien(self)
        alien.x = alien_width + alien_number * alien_width * 2
        alien.y = alien_height + row_number * alien_height * 2
        alien.rect.x = alien.x
        alien.rect.y = alien.y
        self.aliens.add(alien)

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)  # !! 别忘了传入 ai_game
            self.bullets.add(new_bullet)

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹"""
        self.bullets.update()  # 对编组调用update()时,编组自动对其中的每个精灵调用 update()

        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        # print(len(self.bullets))

        self._check_bullet_alien_collisions()  # 射杀外星人

    def _check_bullet_alien_collisions(self):
        """检查子弹和为行人的碰撞"""
        # 检查是否有子弹击中了外星人,若有则删除相应的子弹和外星人
        collisions = pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)

        # 若外星人群被消灭,则删除现有的子弹并新建一群外星人
        if not self.aliens:
            self.bullets.empty()
            self._create_fleet()

    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新外星人群中所有外星人的位置"""
        self._check_fleet_edge()
        self.aliens.update()

        # 检测外星人和飞船之间的碰撞
        if pygame.sprite.spritecollideany(self.ship, self.aliens):
            self._ship_hit()

        # 检查是否有外星人到达屏幕底端
        self._check_aliens_bottom()

    def _check_fleet_edge(self):
        """有外星人到达边缘时采取相应的措施"""
        for alien in self.aliens.sprites():
            if alien.check_edge():
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        """将整群外星人下移,并改变方向"""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1

    def _ship_hit(self):
        """飞船被外星人撞到"""

        if self.stats.ships_left > 0:
            # 将ship_left - 1
            self.stats.ships_left -= 1

            # 清空剩余的外星人和子弹
            self.aliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放在屏幕底部中央
            self._create_fleet()
            self.ship.center_ship()

            # 暂停半秒钟
            sleep(0.5)
        else:
            self.stats.game_active = False  # 游戏结束
            pygame.mouse.set_visible(True)  # 显示隐藏的光标

    def _check_aliens_bottom(self):
        """检查是否有外星人到达了屏幕底端"""
        for alien in self.aliens.sprites():
            if alien.rect.bottom >= self.screen.get_rect().bottom:
                self._ship_hit()  # 像飞船被撞到一样处理
                break

    def _check_play_button(self, mouse_pos):
        """在玩家单击 Play 按钮时开始新游戏"""
        button_checked = self.play_button.rect.collidepoint(mouse_pos)
        if button_checked and not self.stats.game_active:  # 防止用户在游戏过程中点击了按钮所在区域时,游戏重新开始
            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)

            self.stats.game_active = True
            # 重置游戏统计信息
            self.stats.reset_stats()

            # 清空余下的外星人和子弹
            self.aliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放在屏幕底部中央
            self._create_fleet()
            self.ship.center_ship()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

        # 如果游戏处于非活动状态,就绘制Play按钮
        if not self.stats.game_active:
            self.play_button.draw_button()

        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

14.2 提高等级

我们的游戏已经有个雏形了,玩家可点击 Play 按钮进行游戏,游戏结束后再次显示 Play 按钮,玩家的飞船数目有限制,但每轮难度并没有变化。

现在我们再为游戏增加点趣味性:每当玩家将屏幕上的外星人消灭完后,都将加快游戏的节奏,让游戏玩起来更难。

14.2.1 分析

  1. 将游戏设置划分为静态和动态两组。每当玩家开始新游戏时,就只需重置动态部分的设置。

    这和 GameStats 类有静态和动态地设置,是同一个道理。

  2. 在设置类中添加提高速度的方法。

  3. 每当外星人群被消灭完时,就提高新一轮的速度;每当玩家开始新游戏时,就重置动态部分的设置。

14.2.2 代码

settings.py

class Settings:
    """存储游戏《外星人入侵》中所有设置的类"""

    def __init__(self):
        """初始化游戏的静态设置"""
        # 屏幕设置
        self.screen_width = 1200  # 屏幕宽度,单位像素
        self.screen_height = 800  # 屏幕高度,单位像素
        self.bg_color = (230, 230, 230)  # 背景颜色

        # 飞船设置
        self.ship_limit = 3  # 可用总飞船数目

        # 子弹设置
        self.bullet_with = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3  # 最大子弹数

        # 外星人设置
        self.fleet_drop_speed = 10  # 行下移动的速度

        # 加快游戏节奏的速度
        self.speedup_scale = 1.1

        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        """初始化随游戏进行而变化的设置"""
        self.ship_speed = 1.5
        self.bullet_speed = 1.5
        self.alien_speed = 1.0
        self.fleet_direction = 1  # 1表示往右移动,-1表示往左移动

    def increase_speed(self):
        """提高速度设置"""
        self.ship_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.alien_speed *= self.speedup_scale

alien_invasion.py(标注 # !!!!!!!!!!!!!!!!!!!!! 为本次改变的代码)

import sys
from time import sleep
import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet
from alien import Alien
from game_stats import GameStats
from button import Button


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.stats = GameStats(self)  # 创建存储游戏统计信息的实例
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()  # 创建用于存储子弹的编组
        self.aliens = pygame.sprite.Group()  # 创建用于存储外星人的编组
        self.play_button = Button(self, "Play")  # 创建 Play 按钮
        self._create_fleet()
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            if self.stats.game_active:
                self.ship.update()
                self._update_bullets()
                self._update_aliens()

            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏
            elif event.type == pygame.KEYDOWN:  # 玩家每次按键都被注册为一个KEYDOWN事件
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)
            elif event.type == pygame.MOUSEBUTTONDOWN:  # 玩家点击 Play 按钮
                mouse_pos = pygame.mouse.get_pos()
                self._check_play_button(mouse_pos)

    def _check_keydown_events(self, event):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()
        elif event.key == pygame.K_q:
            sys.exit()

    def _create_fleet(self):
        """创建外星人群"""
        # 创建第一个外星人并计算一行可容纳多少个外星人
        alien = Alien(self)
        alien_width = alien.rect.width
        alien_height = alien.rect.height
        available_space_x = self.settings.screen_width - alien_width
        available_space_y = self.settings.screen_height - 3 * alien_height - self.ship.rect.height
        num_aliens_x = available_space_x // (2 * alien_width)
        num_aliens_rows = available_space_y // (2 * alien_height)

        # 创建外星人群
        for row_number in range(num_aliens_rows):
            for alien_number in range(num_aliens_x):
                self._create_alien(alien_number, row_number, alien_width, alien_height)

    def _create_alien(self, alien_number, row_number, alien_width, alien_height):
        """创建一个外星人群"""
        alien = Alien(self)
        alien.x = alien_width + alien_number * alien_width * 2
        alien.y = alien_height + row_number * alien_height * 2
        alien.rect.x = alien.x
        alien.rect.y = alien.y
        self.aliens.add(alien)

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)  # !! 别忘了传入 ai_game
            self.bullets.add(new_bullet)

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹"""
        self.bullets.update()  # 对编组调用update()时,编组自动对其中的每个精灵调用 update()

        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        # print(len(self.bullets))

        self._check_bullet_alien_collisions()  # 射杀外星人

    def _check_bullet_alien_collisions(self):
        """检查子弹和为行人的碰撞"""
        # 检查是否有子弹击中了外星人,若有则删除相应的子弹和外星人
        collisions = pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)

        # 若外星人群被消灭,则删除现有的子弹并新建一群外星人,并加快游戏的节奏
        if not self.aliens:
            self.bullets.empty()
            self._create_fleet()
            self.settings.increase_speed()  # !!!!!!!!!!!!!!!!!!!!!

    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新外星人群中所有外星人的位置"""
        self._check_fleet_edge()
        self.aliens.update()

        # 检测外星人和飞船之间的碰撞
        if pygame.sprite.spritecollideany(self.ship, self.aliens):
            self._ship_hit()

        # 检查是否有外星人到达屏幕底端
        self._check_aliens_bottom()

    def _check_fleet_edge(self):
        """有外星人到达边缘时采取相应的措施"""
        for alien in self.aliens.sprites():
            if alien.check_edge():
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        """将整群外星人下移,并改变方向"""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1

    def _ship_hit(self):
        """飞船被外星人撞到"""

        if self.stats.ships_left > 0:
            # 将ship_left - 1
            self.stats.ships_left -= 1
            
            # 清空剩余的外星人和子弹
            self.aliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放在屏幕底部中央
            self._create_fleet()
            self.ship.center_ship()

            # 暂停半秒钟
            sleep(0.5)
        else:
            self.stats.game_active = False  # 游戏结束
            pygame.mouse.set_visible(True)  # 显示隐藏的光标

    def _check_aliens_bottom(self):
        """检查是否有外星人到达了屏幕底端"""
        for alien in self.aliens.sprites():
            if alien.rect.bottom >= self.screen.get_rect().bottom:
                self._ship_hit()  # 像飞船被撞到一样处理
                break

    def _check_play_button(self, mouse_pos):
        """在玩家单击 Play 按钮时开始新游戏"""
        button_checked = self.play_button.rect.collidepoint(mouse_pos)
        if button_checked and not self.stats.game_active:  # 防止用户在游戏过程中点击了按钮所在区域时,游戏重新开始
            # 重置游戏设置
            self.settings.initialize_dynamic_settings()  # !!!!!!!!!!!!!!!!!!!!!

            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)

            self.stats.game_active = True
            # 重置游戏统计信息
            self.stats.reset_stats()

            # 清空余下的外星人和子弹
            self.aliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放在屏幕底部中央
            self._create_fleet()
            self.ship.center_ship()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

        # 如果游戏处于非活动状态,就绘制Play按钮
        if not self.stats.game_active:
            self.play_button.draw_button()

        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

14.3 计分

下面我们想要实现一个记分系统,以实时跟踪玩家的得分,并显示最高得分、等级和余下的飞船数。

最终效果:

14.3.1 涉及到的函数

  1. round(float, [int]) :将参数1的小数精确到小数点后 x 位,x 由参数2指定。默认精确到小数点后1位。

    rounded_score = round(score, -1) # 能将 score 舍入到最近的10的整数倍

  2. 将数值转换为字符串时在其中添加逗号标识的千位分隔符。

    score_str = "{:,}".format(score)
    

14.3.2 分析

  1. 我们在设置类 Settings 中设置好射杀外星人的分数,和每轮外星人分数的提高速度
  2. 在游戏统计信息类 GameStats 中设置好初始分数、最高分数、初始等级。
  3. 创建显示得分信息的类 ScoreBoard,用它来显示当前分数、最高分数、等级以及剩余的飞船数目。
  4. 因为显示剩余飞船时要显示多艘飞船,所以更改 Ship 类,使它继承自 Sprite类。
  5. 更改 AlienInvasion类,使它在按下Play按钮时重置当前分数、等级以及剩余的飞船数目;更新屏幕时显示得分、等级和飞船条数;射击外星人时增加当前分数,并检查是否更新最高分;飞船被撞击时更新剩余飞船数目;外星人群被消灭时更新当前等级;

14.3.3 代码

settings.py

class Settings:
    """存储游戏《外星人入侵》中所有设置的类"""

    def __init__(self):
        """初始化游戏的静态设置"""
        # 屏幕设置
        self.screen_width = 1200  # 屏幕宽度,单位像素
        self.screen_height = 800  # 屏幕高度,单位像素
        self.bg_color = (230, 230, 230)  # 背景颜色

        # 飞船设置
        self.ship_limit = 3  # 可用总飞船数目

        # 子弹设置
        self.bullet_with = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 3  # 最大子弹数

        # 外星人设置
        self.fleet_drop_speed = 10  # 行下移动的速度

        self.speedup_scale = 1.1  # 加快游戏节奏的速度
        self.score_scale = 1.5  # 外星人分数的提高速度

        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        """初始化随游戏进行而变化的设置"""
        self.ship_speed = 1.5
        self.bullet_speed = 1.5
        self.alien_speed = 1.0
        self.fleet_direction = 1  # 1表示往右移动,-1表示往左移动
        self.alien_points = 50  # 一个外星人记分

    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)

game_stats.py

class GameStats:
    """跟踪游戏的统计信息"""

    def __init__(self, ai_game):
        """初始化统计信息"""
        self.settings = ai_game.settings
        self.reset_stats()
        self.game_active = False  # 游戏刚启动时处于非活动状态
        self.high_score = 0  # 任何情况下都不应重置最高得分

    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.settings.ship_limit
        self.score = 0
        self.level = 1

scoreboard.py

import pygame.font
from pygame.sprite import Group
from ship import Ship


class ScoreBoard:
    """显示得分信息的类"""

    def __init__(self, ai_game):
        """初始化显示得分涉及的属性"""
        self.ai_game = ai_game
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

        # 显示得分信息时使用的字体设置
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 24)

        # 准备初始得分、等级和飞船图像
        self.prep_score()
        self.prep_high_score()
        self.prep_level()
        self.prep_ships()

    def prep_score(self):
        """将得分转换为一副渲染的图像"""
        rounded_score = round(self.stats.score, -1)  # 舍入到10的整数倍
        score_str = "current score: " + "{:,}".format(rounded_score)  # 将数值转换为字符串时,在其中插入逗号(千位分隔符)
        self.score_image = self.font.render(score_str, True, self.text_color, self.settings.bg_color)

        # 在屏幕右上角显示得分
        self.score_rect = self.score_image.get_rect()
        self.score_rect.right = self.screen_rect.right - 10
        self.score_rect.top = 10

    def prep_high_score(self):
        """将最高得分转换为渲染的图像"""
        rounded_high_score = round(self.stats.high_score, -1)
        high_score_str = "highest score: " + "{:,}".format(rounded_high_score)
        self.high_score_image = self.font.render(high_score_str, True, self.text_color, self.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.score_rect.top

    def prep_level(self):
        level_str = "level: " + str(self.stats.level)
        self.level_image = self.font.render(level_str, True, self.text_color, self.settings.bg_color)

        # 将等级放在得分下方
        self.level_rect = self.level_image.get_rect()
        self.level_rect.top = self.score_rect.bottom + 5
        self.level_rect.right = self.score_rect.right

    def prep_ships(self):
        """显示还余下多少飞船"""
        self.ships = Group()
        for ship_number in range(self.stats.ships_left):
            ship = Ship(self.ai_game)
            ship.rect.x = 10 + ship_number * ship.rect.width
            ship.rect.y = 10
            self.ships.add(ship)

    def show_score_level_ships(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)

    def check_high_score(self):
        """检查是否诞生了新的最高得分"""
        if self.stats.score > self.stats.high_score:
            self.stats.high_score = self.stats.score
            self.prep_high_score()

ship.py

import pygame
from pygame.sprite import Sprite

class Ship(Sprite):
    """管理飞船的类"""

    def __init__(self, ai_game):  # 参数:引用self和指向当前AlienInvasion实例的引用,这让Ship能访问AlienInvasion中定义的所有游戏资源
        """初始化飞船并设置其初始位置"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()  # get_rect()访问屏幕的属性rect

        # 加载飞船图片并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()

        # 对每艘新飞船,都将其放在屏幕底部的中央
        self.rect.midbottom = self.screen_rect.midbottom
        self.x = float(self.rect.x)  # x属性存储小数值,因为rect的x等属性只能存储整数值

        # 移动标志
        self.moving_right = False
        self.moving_left = False

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

    def update(self):
        if self.moving_right and self.rect.right < self.screen_rect.right:
            self.x += self.settings.ship_speed
        elif self.moving_left and self.rect.left > 0:
            self.x -= self.settings.ship_speed
        self.rect.x = self.x

    def center_ship(self):
        """让飞船在屏幕底部中央"""
        self.rect.midbottom = self.screen_rect.midbottom
        self.x = float(self.rect.x)

alien_invasion.py

import sys
from time import sleep
import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet
from alien import Alien
from game_stats import GameStats
from button import Button
from scoreboard import ScoreBoard


class AlienInvasion:
    """初始化游戏资源和行为的类"""

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()  # 初始化背景设置,让 Pygame 能正常工作
        self.settings = Settings()  # 创建Settings实例并用它来访问设置
        # 创建一个显示窗口。参数是元组,单位为像素,返回对象是surface(屏幕的一部分,用于显示游戏元素,这里表示整个游戏窗口)
        self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
        self.stats = GameStats(self)  # 创建存储游戏统计信息的实例
        self.scoreboard = ScoreBoard(self)  # 创建记分牌
        self.ship = Ship(self)
        self.bullets = pygame.sprite.Group()  # 创建用于存储子弹的编组
        self.aliens = pygame.sprite.Group()  # 创建用于存储外星人的编组
        self.play_button = Button(self, "Play")  # 创建 Play 按钮
        self._create_fleet()
        pygame.display.set_caption("Alien Invasion")

    def run_game(self):
        """开始游戏的主循环"""
        while True:  # 通过辅助方法将事务管理与游戏的其他方面(如更新屏幕)分离——重构,使循环变得简单
            self._check_events()
            if self.stats.game_active:
                self.ship.update()
                self._update_bullets()
                self._update_aliens()

            # 每次循环时都重绘屏幕
            self._update_screen()

    def _check_events(self):
        """响应按键和鼠标事件"""
        for event in pygame.event.get():  # 得到一个监听键盘和鼠标事件列表(上一次被调用后发生的所有事件)
            if event.type == pygame.QUIT:  # 玩家单击游戏窗口的关闭按钮时,将检测到 pygame.QUIT事件
                sys.exit()  # 退出游戏
            elif event.type == pygame.KEYDOWN:  # 玩家每次按键都被注册为一个KEYDOWN事件
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)
            elif event.type == pygame.MOUSEBUTTONDOWN:  # 玩家点击 Play 按钮
                mouse_pos = pygame.mouse.get_pos()
                self._check_play_button(mouse_pos)

    def _check_keydown_events(self, event):
        """响应按键"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()
        elif event.key == pygame.K_q:
            sys.exit()

    def _create_fleet(self):
        """创建外星人群"""
        # 创建第一个外星人并计算一行可容纳多少个外星人
        alien = Alien(self)
        alien_width = alien.rect.width
        alien_height = alien.rect.height
        available_space_x = self.settings.screen_width - alien_width
        available_space_y = self.settings.screen_height - 3 * alien_height - self.ship.rect.height
        num_aliens_x = available_space_x // (2 * alien_width)
        num_aliens_rows = available_space_y // (2 * alien_height)

        # 创建外星人群
        for row_number in range(num_aliens_rows):
            for alien_number in range(num_aliens_x):
                self._create_alien(alien_number, row_number, alien_width, alien_height)

    def _create_alien(self, alien_number, row_number, alien_width, alien_height):
        """创建一个外星人群"""
        alien = Alien(self)
        alien.x = alien_width + alien_number * alien_width * 2
        alien.y = alien_height + 10 + row_number * alien_height * 2
        alien.rect.x = alien.x
        alien.rect.y = alien.y
        self.aliens.add(alien)

    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)  # !! 别忘了传入 ai_game
            self.bullets.add(new_bullet)

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹"""
        self.bullets.update()  # 对编组调用update()时,编组自动对其中的每个精灵调用 update()

        # 删除消失的子弹
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        # print(len(self.bullets))

        self._check_bullet_alien_collisions()  # 射杀外星人

    def _check_bullet_alien_collisions(self):
        """检查子弹和为行人的碰撞"""
        # 检查是否有子弹击中了外星人,若有则删除相应的子弹和外星人
        collisions = pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)
        if collisions:  # 记分
            for aliens in collisions.values():  # 保证消灭的每个外星人都计入得分
                self.stats.score += self.settings.alien_points * len(aliens)
            self.scoreboard.prep_score()
            self.scoreboard.check_high_score()

        # 若外星人群被消灭,则删除现有的子弹并新建一群外星人,并加快游戏的节奏,提升玩家等级
        if not self.aliens:
            self.bullets.empty()
            self._create_fleet()
            self.settings.increase_speed()
            self.stats.level += 1
            self.scoreboard.prep_level()

    def _update_aliens(self):
        """检查是否有外星人位于屏幕边缘,并更新外星人群中所有外星人的位置"""
        self._check_fleet_edge()
        self.aliens.update()

        # 检测外星人和飞船之间的碰撞
        if pygame.sprite.spritecollideany(self.ship, self.aliens):
            self._ship_hit()

        # 检查是否有外星人到达屏幕底端
        self._check_aliens_bottom()

    def _check_fleet_edge(self):
        """有外星人到达边缘时采取相应的措施"""
        for alien in self.aliens.sprites():
            if alien.check_edge():
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        """将整群外星人下移,并改变方向"""
        for alien in self.aliens.sprites():
            alien.rect.y += self.settings.fleet_drop_speed
        self.settings.fleet_direction *= -1

    def _ship_hit(self):
        """飞船被外星人撞到"""

        if self.stats.ships_left > 0:
            # 将ship_left - 1
            self.stats.ships_left -= 1
            # 更新记分牌
            self.scoreboard.prep_ships()

            # 清空剩余的外星人和子弹
            self.aliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放在屏幕底部中央
            self._create_fleet()
            self.ship.center_ship()

            # 暂停半秒钟
            sleep(0.5)
        else:
            self.stats.game_active = False  # 游戏结束
            pygame.mouse.set_visible(True)  # 显示隐藏的光标

    def _check_aliens_bottom(self):
        """检查是否有外星人到达了屏幕底端"""
        for alien in self.aliens.sprites():
            if alien.rect.bottom >= self.screen.get_rect().bottom:
                self._ship_hit()  # 像飞船被撞到一样处理
                break

    def _check_play_button(self, mouse_pos):
        """在玩家单击 Play 按钮时开始新游戏"""
        button_checked = self.play_button.rect.collidepoint(mouse_pos)
        if button_checked and not self.stats.game_active:  # 防止用户在游戏过程中点击了按钮所在区域时,游戏重新开始
            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)

            # 重置游戏设置
            self.settings.initialize_dynamic_settings()

            # 重置游戏统计信息
            self.stats.reset_stats()
            self.stats.game_active = True
            self.scoreboard.prep_score()  # 分数要改变
            self.scoreboard.prep_level()  # 等级要改变
            self.scoreboard.prep_ships()  # 余下飞船数要改变

            # 清空余下的外星人和子弹
            self.aliens.empty()
            self.bullets.empty()

            # 创建一群新的外星人,并将飞船放在屏幕底部中央
            self._create_fleet()
            self.ship.center_ship()

    def _check_keyup_events(self, event):
        """响应松开"""
        if event.key == pygame.K_RIGHT:
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        self.screen.fill(self.settings.bg_color)  # 颜色,fill()方法用于处理surface,只接受一个实参——一种颜色
        self.ship.blitme()  # 绘制飞船
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()
        self.aliens.draw(self.screen)

        # 如果游戏处于非活动状态,就绘制Play按钮
        if not self.stats.game_active:
            self.play_button.draw_button()

        self.scoreboard.show_score_level_ships()  # 显示得分、等级和飞船条数
        pygame.display.flip()  # 让最近绘制的屏幕可见


# 检查特殊变量 __name__,此变量为程序执行时设置。若此文件作为主程序执行,则此变量被设置为'__main__';若此文件被测试框架导入,则变量将不是'__main__'
if __name__ == '__main__':
    # 创建游戏实例并运行游戏
    ai = AlienInvasion()
    ai.run_game()

本文作者:Joey-Wang

本文链接:https://www.cnblogs.com/joey-wang/p/14553157.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Joey-Wang  阅读(1251)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开