13.1 项目回顾
本章涉及以下内容:
(1)研究既有代码,确定实现新功能前是否要重构
(2)在屏幕左上角添加一个外星人,并指定合适的边距
(3)根据第一个外星人的边距和屏幕尺寸计算屏幕上可容纳多少个外星人。编写一个循环来创建一系列外星人,使其填满屏幕的上半部分。
(4)让外星人群向两边和下方移动,直到外星人被全部击落、有外星人撞到飞船或有外星人抵达屏幕底端。如果郑群外星人都被击落,将再创建一群外星人。如果有外星人撞到了飞船或抵达屏幕底端,将销毁飞船并再创建一群外星人。
(5)限制玩家可用的飞船数量。当配给的飞船用完之后,游戏结束。
13.2 创建第一个外星人
跟飞船ship一样,创建一个外星人的Alien类,象征外星人的图片可以自己找(放张新垣结衣的大头照也是可以的哟)。

13.2.1 创建Alien类
import pygame
from pygame.sprite import Sprite
class Alien(Sprite):
    def super()__init__(self,screen):
        """初始化外星人并设置其起始位置"""
        super(Alien,self).__init__()
        self.screen = screen

        #加载外星人图像并设置其rect属性
        self.image = pygame.image.load("images/alien.png")
        self.rect = self.image.get_rect()

        #每个外星人最初都在屏幕左上角附近
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        #存储外星人的精确水平位置
        self.x = float(self.rect.x)

说明一下,当我看完视频讲解的时候,再回来做这个内容,发现自己错的地方比较多。而重新跟着视频做的第一版反而较为正确。所以我决定,本期的项目大致跟着第一版走。那么,很显然,其他人就看不懂了。

建议,有书的自己捉摸着做(目前我就是根据第一版的经验看着第二版的书做,基本还能做下来。)。

没书的,或者基础不牢的,请去看学习视频(视频地址在12.6章中)

13.3 创建一群外星人
13.3.2 创建一行外星人
def create_fleet(ai_settings, aliens, screen):
    """创建外星人群"""
    # 创建一个外星人并计算一行可容纳多少个外星人
    # 外星人的间距为外星人的宽度
    alien = Alien(screen)
    alien_width= alien.rect.width
    # 屏幕宽度减去2个外星人的宽度等于真实放置外星人的宽度(1个外星人的宽度用于放置左右边距)
    available_space_x = ai_settings.screen_width - (alien_width * 1)
    # 上面的真实宽度除以外星人的宽度等于一排能放多少个外星人
    number_aliens_x = available_space_x // alien_width
    # 创建第一行外星人
    for alien_number in range(number_aliens_x):
        # 创建一个外星人并将其加入当前行
        alien = Alien(screen)
        alien.x = alien_width + 1 * alien_width * alien_number
        alien.rect.x = alien.x
        aliens.add(alien)

这里比较复杂的就是alien.x = alien_width + 1 * alien_width * alien_number。这里其实是不停的创建外星人(上限为number_aliens_x的值)。每创建一个,跟在前面一个外星人的后面,也就是alien.x = alien_width + 1 * alien_width * alien_number。

比如当前number_aliens_x为0,那么第一个外星人的x轴位置就是第一个外星人的宽度。如果number_aliens_x为1,那么第二个外星人就是在第一个外星人后面再增加一个外星人的宽度作为x轴位置。

最后把每个创建的外星人都添加到aliens这个编组内。

13.3.3 重构create_fleet()
def create_fleet(ai_settings, aliens, screen):
    """创建外星人群"""
    # 创建一个外星人并计算一行可容纳多少个外星人
    # 外星人的间距为外星人的宽度
    alien = Alien(screen)
    alien_width= alien.rect.width
    # 屏幕宽度减去2个外星人的宽度等于真实放置外星人的宽度(两个外星人的宽度用于放置左右边距)
    available_space_x = ai_settings.screen_width - (alien_width * 1)
    # 上面的真实宽度除以外星人的宽度等于一排能放多少个外星人(一个外星人需要本体和空余一个外星人的空白区域作为外星人之间的间距)
    number_aliens_x = available_space_x // alien_width
    # 创建第一行外星人
    for alien_number in range(number_aliens_x):
        create_alien(alien_number,aliens, screen)

def create_alien(alien_number, aliens, screen):
        # 创建一个外星人并将其加入当前行
        alien = Alien(screen)
        alien_width = alien.rect.width
        alien.x = alien_width + 1 * alien_width * alien_number
        alien.rect.x = alien.x
        aliens.add(alien)

13.3.4 添加行
计算屏幕可容纳多少行,并将创建一行外星人的循环重复执行响应的次数。为计算可容纳的行数,要先计算可用的垂直空间:用屏幕高度减去第一行外星人的上边距(外星人高度)、飞船的高度以及外星人群最初与飞船之间的距离(外星人高度的两倍):
available_space_y = screen_height - Ship_height - (alien_height * 3)

每行下方都要流出一个空白区域,不放将其设置为外星人的高度。为计算可容纳的行数,将可用的垂直空间除以外星人高度的两倍。我们使用整除,因为行数只能是整除。

number_rows = available_space_y // 2* alien_height

知道可容纳多少行后,就可以重复执行创建一行外星人的代码了:
def create_fleet(ai_settings, aliens, screen, ship):
    """创建外星人群"""
    # 创建一个外星人并计算一行可容纳多少个外星人
    # 外星人的间距为外星人的宽度
    alien = Alien(screen)
①    alien_width, alien_height = alien.rect.size
    # 屏幕宽度减去2个外星人的宽度等于真实放置外星人的宽度(两个外星人的宽度用于放置左右边距)
    available_space_x = ai_settings.screen_width - (alien_width * 1)
    number_aliens_x = available_space_x // alien_width

    # 计算屏幕可容纳多少行外星人
    ship_height = ship.rect.height
    available_space_y = ai_settings.screen_height - ship_height - alien_height
    number_rows = available_space_y // alien_height

    # 创建外星人群
③    for row_number in range(number_rows):
        for alien_number in range(number_aliens_x):
            create_alien(alien_number, aliens, screen, row_number)


def create_alien(alien_number, aliens, screen, row_number):
    # 创建一个外星人并将其加入当前行
    alien = Alien(screen)
    alien_width, alien_height = alien.rect.size
    alien.x = alien_width + alien_width * alien_number
    alien.rect.x = alien.x
④    alien.rect.y = 50 + alien.rect.height * row_number
    aliens.add(alien)

需要知道外星人的高度和宽度,于是在①处使用了属性size。该属性是个元组,包含rect对象的宽度和高度。为计算ping缪可容纳多少航外星人,在计算available_space_x的代码后面添加了计算available_space_y的代码。

为创建多行外星人,使用了两个嵌套在一起的循环:一个外部循环和一个内部循环(见③)。内部循环创建一行外星人,而外部循环从零数到要创建的外星人行数:python将重复执行创建单行外星人的代码,重复次数为number_rows。

为嵌套循环,编写了一个新的for循环,并缩进了要重复执行的代码。现在调用create_alien()时,传递了一个表示行号的实参,将每行都沿屏幕向下放置。

在create_alien()的定义中,需要一个用于存储行号的形参。在create_alien()中,修改外星人的Y坐标(见④)并在第一行外星人上方流出与外星人等高的空白区域。相邻外星人行的y坐标相差外星人高度的两倍。

这里我根据自己屏幕及素材的实际情况,修改了部分代码。比如素材距离屏幕上边距我改成了50。因为我的素材太高, 如果按照素材的高来算,空白就会太多。

13.4 让外星人移动
下面来让外星人群在屏幕上向右移动,撞到屏幕边缘后下移一定的量,再沿相反的方向移动。我们将不断移动所有的外星人,知道外星人被全部消灭,或者有外星人撞上飞船或抵达屏幕底端。
    def update(self, ai_settings):
        """向右移动外星人"""
        self.x += ai_settings.alien_speed
        self.rect.x = self.x
1.4.2 创建表示外星人移动方向的设置
下面来创建让外星人撞到屏幕右边缘后向下移动、再向上移动的设置。
import pygame

class Text_settings():
    """所有设置"""

    def __init__(self):
        self.bg_color = (230, 230, 230)
        self.screen_width = 1200
        self.screen_height = 800
        self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
        self.ship_speed_factor = 2
        self.bullet_speed_factor = 2
        self.bullet_width = 15
        self.bullet_height = 3
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 100
        self.alien_speed = float(1.5)
        self.feleet_drop_speed = 10
        #fleet_direction 为1 表示向右移,为-1表示向左移
        self.fleet_direcion = 1

设置self.feleet_drop_speed指定有外星人撞到屏幕边缘时,外星人裙向下移动的速度。将这个速度与水平速度分开是有好处的,便于分别调整这两个速度。

要实现设置fleet_direcion,可将其设置为文本值,如'left'或'right',但这样就必须编写if-elif来检查外星人裙的移动方向。鉴于只有两个可能的方向,我们使用值1 和-1来表示,并在外星人群改变方向时在这两个值之间切换。(向右移时需要增大每个外星人的x坐标,而向左移时需要减少每个人的x坐标,因此使用数字来表示方向非常合理)
13.4.3 检查外星人是否撞到屏幕边缘
现在需要编写一个方法来检查外星人是否撞到了屏幕边缘,还需修改update()让每个外星人都沿着正确的方向行动。这些代码位于Alien类中:
    def check_edges(self):
        """如果外星人位于屏幕边缘,就返回True"""
        screen_rect = self.screen.get_rect()
        if self.rect.right >= screen_rect.right or self.rect.left <= 0:
            return True

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

可对任意外星人调用新方法check_edges(),看其是否位于屏幕左边缘或右边缘。如果外星人的rect的属性right大于或等于屏幕rect的属性,就说明外星人位于屏幕右边缘;如果外星人的rect的left属性小于或等于0,就说明外星人位于屏幕左边缘。

我们修改方法update(),将移动量设置为外星人速度和fleet_direction的乘积,让外星人向左或向右移动。如果fleet_direction为1,就将外星人的当前x坐标增大alien_speed,从而将外星人向右移;如果fleet_direction为-1,就将外星人的当前x坐标减去alien_speed,从而将外星人向左移。
13.4.4 向下移动外星人群并改变移动方向
有外星人到达屏幕边缘时,需要将整群外星下移,并改变它们的移动方向。为此,需要在AlienInvasion中添加一些代码,因为要在这里检查是否有外星人到达了左边缘或右边缘。我们编写方法check_fleet_edges()和change_fleet_direcion(),并且修改update_aliens()。这些新方法将放在create_alien()后面,但其实放在AlienInvasion类中的什么位置都无关紧要:

def update_alien(ai_settings,aliens):
    """检查是否有外星人位于屏幕边缘,并更新整群外星人的位置"""
    check_fleet_edges(ai_settings,aliens)
    aliens.update()

def check_fleet_edges(ai_settings, aliens):
    """当外星人到达边缘时采取相应的措施"""
    for alien in aliens.sprites():
        if alien.check_edges():
            change_fleet_direction(ai_settings, aliens)
            break


def change_fleet_direction(ai_settings, aliens):
    """将整群外星人下移,并改变它们的方向"""
    for alien in aliens.sprites():
        alien.rect.y += ai_settings.fleet_drop_speed
    ai_settings.fleet_direction *= -1
13.5 射杀外星人

13.5.1 检测子弹与外星人的碰撞
函数sprite.groupcollide()将一个编组中每个元素的rect同另一个编组中的每个元素的rect进行比较。在这里,是将每颗子弹的rect与每个外星人的rect进行比较,并返回一个字典,其中包含发生了碰撞的子弹和外星人。在这个字典中,每个键都是一颗子弹,而关联的值是被该自担集中的外星人。

在方法update_bullets()末尾,添加如下检查子弹和外星人碰撞的代码:
def update_bullet(bullets,aliens):
    # 检查子弹位置,并删除多余子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 20:
            bullets.remove(bullet)

    # 检查是否有子弹击中了外星人
    # 如果是,就删除相应的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets,aliens,True,True)

这些新增的代码将bullets中所有的子弹斗鱼aliens中所有的外星人进行比较。看他们是否重叠在一起。每当有子弹和外星人的rect重叠时,groupcollide()就在它返回的字典中添加一个键值对。两个实参True让pygame删除发生碰撞的子弹和外星人。
13.5.2 为测试创建大子弹
将bullet_width设置为300至3000,看看将所有外星人全部射杀有多快!
13.5.3 生成新的外星人群
一群外星人被消灭后再显示一群外星人,首先需要检查编组aliens是否为空。如果是,就调用create_fleet()。我们将在update_bullets()末尾执行这项任务,因为外星人都是在这里被消灭的:
def update_bullet(ai_settings,screen,ship,aliens,bullets):
    # 检查子弹位置,并删除多余子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 20:
            bullets.remove(bullet)

    # 检查是否有子弹击中了外星人
    # 如果是,就删除相应的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets,aliens,False,True)

    if len(aliens) == 0:
        # 删除现有的子弹并新建一群外星人
        bullets.empty()
        create_fleet(ai_settings,screen,ship,aliens)

13.5.4 提高子弹的速度
在settings中直接修改self.bullet_speed_factor 就好了。
13.5.5 重构update_bullets()
下面来重构,使其不再执行那么多任务,为此,将处理子弹和外星人碰撞的代码移到一个独立的方法中:
def update_bullet(ai_settings,screen,ship,aliens,bullets):
    # 检查子弹位置,并删除多余子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 20:
            bullets.remove(bullet)
    check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets)

def check_bullet_alien_collisions(ai_settings,screen,ship,aliens,bullets):
    """响应子弹和外星人碰撞"""
    # 检查是否有子弹击中了外星人
    # 删除发生碰撞的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, False, True)
    if len(aliens) == 0:
        # 删除现有的子弹并新建一群外星人
        bullets.empty()
        create_fleet(ai_settings,screen,ship,aliens)
13.6 结束游戏
玩家根本不会输,游戏就没意思,于是要设定玩家会输的游戏规则。

13.6.1 检测外星人和飞船碰撞
在update_alien 下增加
    if pygame.sprite.spritecollideany(ship,aliens):
        print("胜败乃兵家常事,大侠请从新来过")

13.6.2 响应外星人和飞船相撞
创建stats类:
class GameStats():
    """跟踪游戏的统计信息"""
    def __init__(self,ai_settings):
        self.ai_settings = ai_settings
        self.reset_stats()

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

每当玩家开始新游戏时,需要充值一些统计信息。为此在reset_stats()中初始化大部分统计信息。

将实现统计功能的大部分代码放到新方法ship_hit()中。
def ship_hit(ai_settings,stats,screen,ship,aliens,bullets):
    """响应被外星人撞到的飞船"""
    # 将ship_left 减1
    stats.ships_left -=1

    # 清空外星人列表和子弹列表
    aliens.empty()
    bullets.empty()

    # 创建一群新的外星人,并将飞船放到屏幕底端中央
    create_fleet(ai_settings,screen,ship,aliens)
    ship.center_ship()

    #暂停
    sleep(0.5)

13.6.3 外星人到达屏幕底端
def check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets):
    """外星人到达底端"""
    screen_rect = screen.get_rect()
    for alien in aliens.sprites():
        if alien.rect.bottom >= screen_rect.bottom:
            # 像飞船被撞到一样处理
            ship_hit(ai_settings, stats, screen, ship, aliens, bullets)
            break

13.6.4 游戏结束

下面再GameStats中添加一个标志作为属性game_active,一遍在玩家的飞船用完后结束游戏。
class GameStats():
    """跟踪游戏的统计信息"""

    def __init__(self, ai_settings):
        self.ai_settings = ai_settings
        self.reset_stats()
        self.game_active = True

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

def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
    """响应被外星人撞到的飞船"""
    if stats.ships_left > 0:
        # 将ship_left 减1
        stats.ships_left -= 1

        # 清空外星人列表和子弹列表
        aliens.empty()
        bullets.empty()

        # 创建一群新的外星人,并将飞船放到屏幕低端中央
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

        # 暂停
        sleep(0.5)

    else:
        print("给你机会你不中用啊!")
        stats.game_active = False
        sys.exit()

13.7 确定应运行游戏的那些部分:
我们需要确定要关闭哪些部分的程序:
    while True:
        test_game_function.check_events(ai_settings, screen, ship, bullets)
        if stats.game_active:
            Ship.update(ship)
            bullets.update()
            test_game_function.update_alien(
                ai_settings, stats, screen, ship, aliens, bullets)
            test_game_function.update_bullet(
                ai_settings, screen, ship, aliens, bullets)
            test_game_function.check_aliens_bottom(
                ai_settings, stats, screen, ship, aliens, bullets)
            # 每次循环重新绘制屏幕
        test_game_function.update_screen(
            ai_settings, screen, ship, bullets, aliens)

本章后面几节已经写的已经不全了,因为目前是凌晨3点,再不睡觉我估计自己会猝死。所以就先凑活吧,等外星人项目完结。我会把所有代码贴上来。

胭惜雨

2021年02月21日

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据