目录

  • 安装Pygame
  • 创建Pygame窗口以及响应用户输入
  • 设置背景色
  • 创建设置类
  • 添加飞船图像
  • 创建ship类
  • 重构:模块game_functions
  • 函数check_events()
  • 函数update_screen()
  • 驾驶飞船
  • 响应按键
  • 允许不断移动
  • 左右移动
  • 调整飞船的速度
  • 限制飞船的活动范围
  • 重构check_events()
  • 射击
  • 添加子弹设置
  • 创建bullet类
  • 将子弹存储到编组中
  • 开火
  • 删除已经消失的子弹
  • 限制子弹的数量
  • 创建函数update_bullets()
  • 创建函数fire_bullet()
  • 外星人
  • 创建Alien类
  • 创建Alien实例
  • 让外星人出现在屏幕上
  • 创建一群外星人
  • 确定一行可容纳多少个外星人
  • 创建多行外星人
  • 重构create_fleet()
  • 添加行
  • 让外星人群移动
  • 向右移动的外星人
  • 创建表示外星人移动方向的设置
  • 检查外星人是否撞到屏幕边缘
  • 向下移动外星人群并改变移动方向
  • 射杀外星人
  • 检测外星人与子弹的碰撞
  • 生成新的外星人群
  • 重构update_bullets()
  • 结束游戏
  • 检测外星人和飞船碰撞
  • 响应外星人和飞船碰撞
  • 有外星人到达屏幕底部
  • 游戏结束
  • 确定应运行游戏的那些部分
  • 后记


安装Pygame

终端输入:
pip install Pygame
#开始游戏项目

创建Pygame窗口以及响应用户输入

首先:创建一个空的Pygame窗口。

import sys
import pygame


def run_game():
    pygame.init()
    screen = pygame.display.set_mode((1200, 800))
    pygame.display.set_caption("Alien Invasion")
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        pygame.display.flip()


run_game()

理解:

  1. pygame.init()初始化背景设置,让Pygame能够正确地工作。
  2. Pygame.display.set_mode()来创建一个名为screen的显示窗口。每经过一次循环都将自动重绘这个surface(游戏中的元素,外星人,飞船等。)
  3. while循环包含一个事件循环以及管理屏幕更新的代码。事件是用户玩游戏时的操作,如按键或移动鼠标。为让程序响应事件,我们编写一个事件循环,用于监听事件,并根据发生的事件执行相应的任务。
  4. Pygame.QUIT,用于检测当用户单击关闭按钮时,调用sys.exit()来退出程序。
  5. pygame.display.flip(),命令pygame让最近绘制的屏幕可见。在这里,它在每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使只有新屏幕可见。在我们移动游戏元素时,pygame.display.flip()将不断更新屏幕,以显示元素的新位置,并在原来的位置隐藏元素,从而营造平滑移动的效果。

设置背景色

alien_invasion.py

import sys
import pygame


def run_game():
    pygame.init()
    screen = pygame.display.set_mode((1200, 800))
    pygame.display.set_caption("Alien Invasion")
    bg_color = (230,230,230)
    # 开始游戏主循环
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        screen.fill(bg_color)
    # 让最近绘制的屏幕可见
        pygame.display.flip()


run_game()

添加了一个screen.fill(bg_color),用于填充颜色

创建设置类

每次给游戏添加新功能时,通常也将引入一些新设置。下面编写一个名为settings的模块,其中包含一个名为settings的类,用于将所有设置存储在一个地方,以免在代码中到处添加设置。
这样我们就能传递一个设置对象,而不是众多不同的设置。
另外,这让函数调用更简单,且在项目增大时修改游戏的外观更容易:要修改游戏,只需要修改settings.py中的一些值。

settings.py

class Settings():
    def __init__(self):
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230, 230, 230)

修改原来的alien_invasion.py文件为:

alien_invasion.py

import sys
import pygame
from settings import Settings


def run_game():
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    # 开始游戏主循环
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        screen.fill(ai_settings.bg_color)
    # 让最近绘制的屏幕可见
        pygame.display.flip()


run_game()

添加飞船图像

在网站http://pixabay.com/中下载飞船图像,使用.bmp结尾的图像,(这样的图片不用下载图像库便可以使用)。
大多图片为.jpg,.png,.gif格式。但可以使用Photoshop,GIMP和paint等工具将其转换为位图。
在项目的文件夹下创建一个images的文件夹,并将图片ship.bmp放进去。

创建ship类

选择用于表示飞船的图像后,需要将其显示到屏幕上。我们将创建一个名为ship的模块,其中包含ship类,它负责管理飞船的大部分行为。

ship.py

import pygame
class Ship():
    def __init__(self, screen):
        self.screen = screen
        self.image = pygame.image.load('images/ship.png')
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom

    def blitme(self):

        self.screen.blit(self.image, self.rect)

重构:模块game_functions

在大型项目中,经常需要在添加新代码前重构既有代码。重构旨在简化既有代码的结构,使其更容易扩展。
在本节中,我们将创建一个名为game_functions的新模块,它将存储大量让游戏运行的函数。通过创建模块game_functions,可避免alien_invasion.py太长,使其逻辑混乱。

函数check_events()

我们首先把管理事件的代码移到一个名为check_events()的函数中,以简化run_game()并隔离事件管理循环。通过隔离事件循环,可将事件管理与游戏的其他方面(如更新屏幕)分离。
将check_events()放在一个名为game_functions的模块中。

import sys
import pygame


def check_events():
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

更改alien_invasions.py为:

import sys
import pygame
from settings import Settings
from ship import Ship
import game_functions as gf


def run_game():
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    pygame.display.set_caption('Alien Invasion')
    ship = Ship(screen)
    # 开始游戏主循环
    while True:
        gf.check_events()
        screen.fill(ai_settings.bg_color)
        ship.blitme()
    # 让最近绘制的屏幕可见
        pygame.display.flip()


run_game()

把循环部分更改了即可。

函数update_screen()

为了进一步简化run_game(),下面将更新屏幕的代码移到一个名为update_screen()的函数中,并将这个函数放在模块game_functions.py中:

def update_screen(ai_settings, screen, ship):
    screen.fill(ai_settings.bg_color)
    ship.blitme()
    pygame.display.flip()

再将alien_invasion.py中的部分替换即可:

while True:
        gf.check_events()
        gf.update_screen(ai_settings, screen, ship)

驾驶飞船

下面让玩家可以左右移动飞船。为此,我们将编写代码,在用户按左或右箭头时做出响应。
我们先专注于向右移动。

响应按键

每当用户按键时,都将在pygame中注册一个事件。事件是通过方法pygame.event.get()获取的,因此在函数check_events()中,我们需要指定要检查那些类型的事件。
每次按键都被注册为一个KEYDOWN事件。
检测到KEYDOWN事件时,我们需要检查按下的键是否为特定的键。例如,如果按下的时右箭头,就增大飞船的rect.centrx值。
在game_functions.py中添加以下函数:

def check_events(ship):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN: # ①
            if event.key == pygame.K_PIGHT: # ②
                ship.rect.centerx += 1

解释:①处用于在pygame检测到KEYDOWN事件时作出响应。
②处在检测到按下为右箭头时将飞船向右移动。
修改对应的alien_invasion.py模块:传参ship

while True:
        gf.check_events(ship)
        gf.update_screen(ai_settings, screen, ship)

允许不断移动

玩家按住右箭头不放时,我们希望飞船不断地向右移动,直到玩家松开为止。
思路:

  1. 检测pygame.KEYUP事件,以便玩家松开右箭头时我们能够知道这一点;然后,我们将结合使用KEYDOWN和KEYUP事件,以及一个名为moving_right的标志来实现持续移动。
  2. 飞船不动时,标志moving_right将为False。玩家按下右箭头时,我们将这个标志设为True,而玩家松开时,我们将这个标志重新设置为Falsh

飞船的属性由ship类控制,因此我们给这个类添加一个名为moving_right的属性和一个名为update()的方法。方法update()检查标志moving_right的状态,如果这个标志为True,就调整飞船的位置。

import pygame
class Ship():
	--snip--
	def __init__(self, screen):
		self.moving_right = False

    def update(self):
        if self.moving_right:
            self.rect.centerx += 1

接下来更改game_functions.py中的check_events()函数,让玩家按下右箭头时将moving_right设置为True,并在玩家松开时将moving_right设置为false:

def check_events(ship):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = True
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = False

最后,在alien_invasion.py中的while循环,以便每次执行循环时都要调用飞船的方法update()。

while True:
        gf.check_events(ship)
        ship.update()
        gf.update_screen(ai_settings, screen, ship)

左右移动

飞船能够不断地向右移动后,添加向左移动地逻辑很容易。我们将再次修改ship类和函数check_events().

ship.py

import pygame
class Ship():
    def __init__(self, screen):
        --snip--
        self.moving_right = False
        self.moving_left = False

    def update(self):
        if self.moving_right:
            self.rect.centerx += 1
        elif self.moving_left:
            self.rect.centerx -= 1
	--snip--
game_functions.py # 中的check_events()修改。

def check_events(ship):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = True
            elif event.key == pygame.K_LEFT:
                ship.moving_left = True
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = False
            if event.key == pygame.K_LEFT:
                ship.moving_left = False

调整飞船的速度

接下来我们将进一步优化飞船的移动方式:调整飞船的速度;限制飞船的移动距离,以免它移到屏幕外面去。
每次执行while循环时,飞船最多移动1像素,但我们可以在Settings类中添加属性ship_speed_factor,用于控制飞船的速度。我们将根据这个属性决定飞船在每次循环时最多移动多少距离。

class Settings():
    def __init__(self):
        --snip--
        self.ship_speed_factor = 1.5

通过将速度设置为小数值,可在后面加快游戏的节奏时更细致地控制飞船的速度。然而,rect的centerx等属性只能存储整数值,因此我们需要对ship类作修改:

import pygame
class Ship():
    def __init__(self, ai_settings, screen):
        --snip--
        self.center = float(self.rect.centerx)
        self.moving_right = False
        self.moving_left = False
        
	 def update(self):
        if self.moving_right:
           	self.center += self.ai_settings.ship_speed_factor
        elif self.moving_left:
            self.center -= self.ai_settings.ship_speed_factor
     self.rect.centerx = self.center

	def blitme(self):
		--snip--

最后更改alien_invasion.py

def run_game():
	--snip--
	ship = Ship(ai_settings, screen)
	--snip--

限制飞船的活动范围

当玩家按住箭头的时间足够长,飞船将移动到屏幕外面,消失得无影无踪。下面来修复这种问题。为此,我们将修改ship类的方法update():

def run_game():
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    ship = Ship(ai_settings, screen)

重构check_events()

随着游戏的开发,函数check_events()将越来越长,我们将其部分代码放在两个函数中:一个处理KEYDOWN事件,一个处理KEYUP事件。

import sys
import pygame


def check_keydown_events(event, ship):
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True


def check_keyup_events(event, ship):
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    elif event.key == pygame.K_LEFT:
        ship.moving_left = False


def check_events(ship):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, ship)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event, ship)


def update_screen(ai_settings, screen, ship):
    screen.fill(ai_settings.bg_color)
    ship.blitme()
    pygame.display.flip()

射击

下面来添加射击设置。我们将编写玩家按空格键时发射子弹(小矩形)的代码。子弹将在屏幕中向上穿行,抵达屏幕边缘后消失。

添加子弹设置

class Settings():
    def __init__(self):
        --snip--
        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60

创建bullet类

创建存储Bullet类的文件bullet.py。

import pygame
from pygame.sprite import Sprite


class Bullet(Sprite):
    def __init__(self, ai_settings, screen, ship):
        super(Bullet, self).__init__()
        self.screen = screen
        self.rect = pygame.Rect(0, 0, ai_settings.bullet_width, ai_settings.bullet_height)
        self.rect.centerx = ship.rect.centerx
        self.rect.top = ship.rect.top
        self.y = float(self.rect.y)
        self.color = ai_settings.bullet_color
        self.speed_factor = ai_settings.bullet_speed_factor

    def update(self):
        self.y -= self.speed_factor
        self.rect.y = self.y

    def draw_bullet(self):
        pygame.draw.rect(self.screen, self.color, self.rect)

将子弹存储到编组中

定义Bullet类和必要的设置后,就可以编写代码了,在玩家每次按空格键时都发射出一发子弹。首先,我们将在alien_invasion.py中创建一个编组(group),用于存储所有有效的子弹,以便能够管理发射出去的子弹。
这个编组将是pygame.sprite.Group类的一个实例;pygame.sprite.Group类,类似于列表,但提供了有助于开发游戏的额外功能。我们将在主循环中,使用这个编组在屏幕上绘制子弹,以及更新子弹的位置。

import sys
import pygame
from settings import Settings
from ship import Ship
import game_functions as gf
from pygame.sprite import Group


def run_game():
    --snip--
    bullets = Group()
    # 开始游戏主循环
    while True:
        gf.check_events(ship)
        ship.update()
        bullets.update()
        gf.update_screen(ai_settings, screen, ship)


run_game()

开火

在game_functions.py中,我们需要修改check_keydown_events(),以便在玩家按空格键时发射一颗子弹。我们无需修改check_keyup_events(),因为玩家松开空格键时什么都不会发生。我们还需要修改update_screen(),确保在调用flip()前在屏幕上重新绘制每颗子弹:

import sys
import pygame
from bullet import Bullet


def check_keydown_events(event,ai_settings , screen, ship, bullets):
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        new_bullet = Bullet(ai_settings, screen, ship)
        bullets.add(new_bullet)

	--snip--

def check_events(ai_settings, screen, ship, bullets):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
       --snip--
        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, ai_settings, screen, ship, bullets)


def update_screen(ai_settings, screen, ship, bullets):
    screen.fill(ai_settings.bg_color)
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()
    pygame.display.flip()

删除已经消失的子弹

当前,子弹抵达屏幕顶端后消失,这仅仅是因为Pygame无法在屏幕外面绘制它们。这些子弹实际上依旧存在。这是个问题,因为它们会继续消耗内存。
为此,我们要检测这样的条件,即表示子弹的rect的bottom属性为零,它表明子弹已穿过屏幕顶端。

# alien_invasion.py

 while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()
        for bullet in bullets.copy():
            if bullet.rect.bottom <= 0:
                bullets.remove(bullet)
        print(len(bullets))
        gf.update_screen(ai_settings, screen, ship, bullets)

在for循环中,不应从列表或编组中删除条目,因此必须遍历编组中的副本。我们使用了方法copy()来设置for循环,这让我们能够在循环中修改bullets。

限制子弹的数量

我们可以对可同时出现在屏幕上的子弹数量进行限制,以鼓励玩家有目的的射击。
首先,在settings.py中存储允许的最大子弹数量。

class Settings():
    def __init__(self):
        --snip--
        # 允许的子弹数量
        self.bullets_allowed = 3

在创建新子弹前检查未消失得子弹数量是否小于该设置。

# game_functions.py

def check_keydown_events(event,ai_settings , screen, ship, bullets):
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        if len(bullets) < ai_settings.bullets_allowed: # 判断是否满足子弹数量的限制
            new_bullet = Bullet(ai_settings, screen, ship)
            bullets.add(new_bullet)

创建函数update_bullets()

编写并检查子弹管理代码后,可将其移到模块game_functions中,让主程序文件alien_invasion.py尽可能的简单。
创建一个名为update_bullets()的函数:

# game_functions.py 底部增加

def update_bullets(bullets):
    bullets.update()
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullets)

更改alien_invasion.py中的while循环:

# game_invasion.py

    while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()
        gf.update_bullets(bullets)
        gf.update_screen(ai_settings, screen, ship, bullets)

创建函数fire_bullet()

将发射子弹的代码移到一个独立的函数中,这样,在check_keydown_events()中只需要使用一行代码来发射子弹,让elif代码块简单。

# game_functions.py

import sys
import pygame
from bullet import Bullet


def check_keydown_events(event, ai_settings, screen, ship, bullets):
    --snip--
    elif event.key == pygame.K_SPACE:
        fire_bullet(ai_settings, screen, ship, bullets)


def fire_bullet(ai_settings, screen, ship, bullets):
    if len(bullets) < ai_settings.bullets_allowed:
        new_bullet = Bullet(ai_settings, screen, ship)
        bullets.add(new_bullet)

外星人

接下来,我们将在游戏中添加外星人。首先,我们在屏幕边缘附近添加一个外星人,然后生成一群外星人向两边和下面移动,并删除被子弹击中的外星人。最后,我们将显示玩家拥有的飞船属性,并在玩家的飞船用完后结束游戏。
在给项目添加新功能前,还应审核既有代码。每进一个阶段,通常项目都会更复杂,因此最好度混乱或低效的代码进行审计。
由于每次测试时,都要点击关闭来关闭游戏,很麻烦,下面添加一个结束游戏的快捷键Q:

# game_functions.py

def check_keydown_events(event, ai_settings, screen, ship, bullets):
   --snip--
    elif event.key == pygame.K_q:
        sys.exit()

创建Alien类

# alien.py

import pygame
from pygame.sprite import Sprite


class Alien(Sprite):
    def __init__(self, ai_settings, screen):
        super(Alien, self).__init__()
        self.screen = screen
        self.ai_settings = ai_settings
        self.image = pygame.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)

    def blitme(self):
        self.screen.blit(self.image, self.rect)

创建Alien实例

在alien_invasion.py中创建一个Alien实例:

# alien_invasion.py

import sys
import pygame
from settings import Settings
from ship import Ship
import game_functions as gf
from pygame.sprite import Group
from alien import Alien


def run_game():
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")
    ship = Ship(ai_settings, screen)
    bullets = Group()
    alien = Alien(ai_settings, screen)

让外星人出现在屏幕上

在update_screen()中调用方法blitme()。

# game_functions.py

def update_screen(ai_settings, screen, ship, alien, bullets):
    screen.fill(ai_settings.bg_color)
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()
    alien.blitme() # 显示外星人
    pygame.display.flip()

创建一群外星人

首先确认一行可以容纳多少个外星人。

确定一行可容纳多少个外星人

为确定一行可容纳多少外星人,我们可以看看可用的水平空间有多大。并在屏幕左右各留出一个外星人的距离;最后用剩下的距离除以一个外星人的距离。
公式:
available_space_x=ai_settings.screen_width-(2* alien_width)
number_aliens_x=available_space_x / (2* alien_width)

创建多行外星人

为创建一行外星人,首先在alien_invasion.py中创建一个名为aliens的空编组,用于存储全部外星人,再调用game_functions.py中的创建外星人群的函数。

import sys
import pygame
from settings import Settings
from ship import Ship
import game_functions as gf
from pygame.sprite import Group
from alien import Alien


def run_game():
    --snip--
    bullets = Group()
    aliens = Group()
    # alien = Alien(ai_settings, screen)
    gf.create_fleet(ai_settings, screen, aliens)
    # 开始游戏主循环
    --snip--

修改update_screen():

# game_functions.py

def update_screen(ai_settings, screen, ship, aliens, bullets):
    screen.fill(ai_settings.bg_color)
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()
    aliens.draw(screen)    # 添加
    # alien.blitme()
    pygame.display.flip()

增加新函数:create_fleet(),放在game_functions.py

def create_fleet(ai_settings, screen, aliens):
    alien = Alien(ai_settings, screen)
    alien_width = alien.rect.width
    available_space_x = ai_settings.screen_width - 2 * alien_width
    number_aliens_x = int(available_space_x / (2 * alien_width))
    for alien_number in range(number_aliens_x):
        alien = Alien(ai_settings, screen)
        alien.x = alien_width + 2 * alien_width * alien_number
        alien.rect.x = alien.x
        aliens.add(alien)

重构create_fleet()

下面是create_fleet()和两个新函数,get_number_aliens_x()和create_alien():

# game_function.py

def get_number_aliens_x(ai_settings, alien_width):
    available_space_x = ai_settings.screen_width - 2 * alien_width
    number_aliens_x = int(available_space_x / (2 * alien_width))
    return number_aliens_x


def create_alien(ai_settings, screen, aliens, alien_number):
    alien = Alien(ai_settings, screen)
    alien_width = alien.rect.width
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    aliens.add(alien)


def create_fleet(ai_settings, screen, aliens):
    alien = Alien(ai_settings, screen)
    number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)
    for alien_number in range(number_aliens_x):
        create_alien(ai_settings, screen, aliens, alien_number)

添加行

# game_functions.py

def get_number_rows(ai_settings, ship_height, alien_height):
    available_space_y = (ai_settings.screen_height - (3 * alien_height) - ship_height)
    number_rows = int(available_space_y / (2 * alien_height))
    return number_rows


def create_alien(ai_settings, screen, aliens, alien_number, row_number):
    --snip--
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
    aliens.add(alien)


def create_fleet(ai_settings, screen, ship, aliens):
    alien = Alien(ai_settings, screen)
    number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)
    number_rows = get_number_rows(ai_settings, ship.rect.height, alien.rect.height)
    for row_number in range(number_rows):
        for alien_number in range(number_aliens_x):
            create_alien(ai_settings, screen, aliens, alien_number, row_number)

让外星人群移动

下面来让外星人移动,在屏幕上向右移动,撞到屏幕边缘后向下移动一定的距离,再沿反方向移动。

向右移动的外星人

为移动外星人,我们将使用方法alien.py中的方法update(),且对外星人群中的每个外星人都调用它。
首先在settings.py中添加控制外星人的速度

# settings.py

class Settings():
    def __init__(self):
        --snip--
        # 外星人设置
        self.alien_speed_factor = 1

alien.py中添加update()函数:

alien.py

    def update(self):
        self.x += self.ai_settings.alien_speed_factor
        self.rect.x = self.x

更新while循环中的更新子弹和飞船的方法

alien_invasion.py

    while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()
        gf.update_bullets(bullets)
        gf.update_aliens(aliens)
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

最后在game.functons.py末尾添加新函数:update_aliens()

game_functions.py

def update_aliens(aliens):
    aliens.update()

创建表示外星人移动方向的设置

下面来创建让外星人撞到屏幕右边缘后向下移动,再向左移动的设置。

settings.py

class Settings():
    def __init__(self):
        --snip--
        # 外星人设置
        self.alien_speed_factor = 1
        self.fleet_drop_speed = 10
        self.fleet_direction = 1

检查外星人是否撞到屏幕边缘

现在需要编写一个方法来检查是否有外星人撞到屏幕边缘,还需要修改update(),以让每个外星人都沿正确的方法移动。

alien.py

    def check_edges(self):
        screen_rect = self.screen.get_rect()
        if self.rect.right >= screen_rect.right:
            return True
        elif self.rect.left <= 0:
            return True

    def update(self):
        self.x += (self.ai_settings.alien_speed_factor * self.ai_settings.fleet_direction)
        self.rect.x = self.x

向下移动外星人群并改变移动方向

game_functions.py

def update_aliens(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

更改alien_invasion.py

alien_invasion.py

    while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()
        gf.update_bullets(bullets)
        gf.update_aliens(ai_settings, aliens)
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

射杀外星人

我们创建了飞船和外星人群,但子弹射击中外星人时,将穿过外星人,因为我们还没有检查碰撞。我们将使用sprite.groupcollide()检测编组成员之间的碰撞。

检测外星人与子弹的碰撞

方法sprite.groupcollide()将每颗子弹的rect同每个外星人的rect进行比较,并返回一个字典,其中包含发生了碰撞的子弹和外星人。
在函数update_bullets()中,使用下面的代码来检测碰撞:

# game_functions.py

def update_bullets(aliens,bullets):
    bullets.update()
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullets)
    print(len(bullets))
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)

update_bullets()的调用传递了实参,aliens:

alien_functions.py

    while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()
        gf.update_bullets(aliens, bullets)
        gf.update_aliens(ai_settings, aliens)
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

生成新的外星人群

这个游戏的一个重要特点就是外星人无穷无尽,一个外星人被消灭后,又会出现一群新的外星人。
首先就要检查编组aliens是否为空。如果为空,就调用create_fleet()。

game_functions.py

def update_bullets(ai_settings, screen, ship, aliens, bullets):
    --snip--
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    if len(aliens) == 0:
        bullets.empty()
        create_fleet(ai_settings, screen, ship, aliens)

更改alien_invasion.py

while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()
        gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
        gf.update_aliens(ai_settings, aliens)
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

重构update_bullets()

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets):
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    if len(aliens) == 0:
        bullets.empty()
        create_fleet(ai_settings, screen, ship, aliens)


def update_bullets(ai_settings, screen, ship, aliens, bullets):
    bullets.update()
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullets)
    print(len(bullets))
    check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets)

结束游戏

检测外星人和飞船碰撞

game_functions.py

def update_aliens(ai_settings, ship, aliens):
    check_fleet_edges(ai_settings, aliens)
    aliens.update()
    if pygame.sprite.spritecollideany(ship, aliens):
        print("ship hit !!!")

将ship传给update_aliens():

# alien_invasion.py

    while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        bullets.update()
        gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
        gf.update_aliens(ai_settings, ship, aliens)   # 加入ship
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

响应外星人和飞船碰撞

通过跟踪游戏的统计信息来记录飞船被撞了多少次。(有助于计分)
编写一个用于跟踪游戏统计信息的新类:GameStarts放在game_starts.py中

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

飞船数存储在settings.py中:

class Settings():
    def __init__(self):
        --snip--
        # 飞船设置
        self.ship_speed_factor = 1.5
        self.ship_limit = 3

在alien_invasion.py中创建一个GameStats实例:

--snip--
from alien import Alien
from game_stats import GameStats


def run_game():
    --snip--
    pygame.display.set_caption("Alien Invasion")
    stats = GameStats(ai_settings)
    --snip--
    while True:
        --snip--
        gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
        gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)
        --snip--

run_game()

最后将实现这些功能的大部分代码放到函数ship.hit()中:

# game_functions.py

def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
    stats.ships_left -= 1
    aliens.empty()
    bullets.empty()
    create_fleet(ai_settings, screen, ship, aliens)
    ship.center_ship()
    sleep(0.5)

def update_aliens(ai_settings, stats, screen, ship, aliens, bullets):
    check_fleet_edges(ai_settings, aliens)
    aliens.update()
    if pygame.sprite.spritecollideany(ship, aliens):
        ship_hit(ai_settings, stats, screen, ship, aliens, bullets)

将center_ship()方法,添加到ship.py末尾:

def center_ship(self):
        self.center = self.screen_rect.centerx

有外星人到达屏幕底部

如果有外星人到达屏幕底端,我们想有外星人撞到飞船那样做出反应。
创建一个新函数,check_aliens_bottom():

# game_functions.py


def update_aliens(ai_settings, stats, screen, ship, aliens, bullets):
    --snip--
    check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets)

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

游戏结束

ships_left会不断变成更小的负数。下面在GameStats中添加一个作为标志的属性game_active,以便玩家在用完飞船后结束游戏。

# game_stats.py

class GameStats():
    def __init__(self, ai_settings):
        --snip--
        self.game_active = True

在玩家所有飞船用完后,将game_active设置为False:

# game_functions.py

def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
    if stats.ships_left > 0:
        stats.ships_left -= 1
        sleep(0.5)
    else:
        stats.game_active = False
    --snip--

确定应运行游戏的那些部分

# alien_invasion.py

    while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        if stats.game_active:
            ship.update()
            gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
            gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)
        bullets.update()
        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

后记

如果有机会,下次再完善计分和开始结束让玩家可以持续玩这个游戏的功能。