淘先锋技术网

首页 1 2 3 4 5 6 7

背景部分


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

# invasion.py

import sys
import pygame

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    screen = pygame.display.set_mode((1200, 700))
    pygame.display.set_caption("Thunder")

    # 开始游戏的主循环
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

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


run_game()
  1. 首先我们导入模块pygamesys。sys用于退出游戏
  2. 游戏以函数run_game()开头。
  3. pygame.init()用于初始化游戏背景。
  4. pygame.display.set_mode()用于创建一个名为screen的显示窗口。实参(1200,700)是一个元组,指定游戏窗口的尺寸。
  5. 对象screen是一个surface。在Pygame中,surface是屏幕的一部分,用于显示游戏元素(比如外星人、飞船),游戏中每个元素都是一个surface。激活游戏的动画循环后,每经过一次循环都将重新绘制这个surface。
  6. 为访问Pygame侦听到的时间,我们使用方法pygame.event.get()。所有的键盘和鼠标事件都将促使for循环运行。比如玩家点击窗口的关闭按钮时,将检测到pygame.QUIT事件,我们就调用sys.exit()来退出游戏。
  7. pygame.display.flip()命令Pygame让最近绘制的屏幕可见。它在每次执行while循环时都会绘制一个空屏幕,并擦去旧屏幕。

绘制背景色

# 在while循环中加入如下语句:
    screen.fill((230,230,230))

Pygame中,颜色是以RGB值表示的。

创建设置类

#settings.py

class Settings():
    """存储游戏所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200
        self.screen_length = 700
        self.bg_color = (230, 230, 230)

将所有游戏的设置存储在这个类中, 则invasion.py可修改:

import sys
import pygame

from settings import Settings


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    sett = Settings()
    screen = pygame.display.set_mode(
        (sett.screen_length, sett.screen_width)
    )
    pygame.display.set_caption("Thunder")

    # 开始游戏的主循环
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        screen.fill(sett.bg_color)

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


run_game()


飞船部分


添加飞船图像

就选用书配套的素材吧

创建Ship类

import pygame


class Ship():

    def __init__(self, screen):
        self.screen = screen

        self.image = pygame.image.load("ship.bmp")
        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)

在屏幕上绘制飞船

在invasion.py中创建飞船对象,并调用其方法blitme():

import sys
import pygame

from settings import Settings
from ship import Ship


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    sett = Settings()
    screen = pygame.display.set_mode(
        (sett.screen_width, sett.screen_length)
    )
    pygame.display.set_caption("Thunder")
    ship = Ship(screen)


    # 开始游戏的主循环
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        screen.fill(sett.bg_color)
        ship.blitme()

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

run_game()

运行后结果:
在这里插入图片描述

重构:game_function模块


函数check_events()

我们把管理事件的代码移到一个名为check_events()的函数里,以简化run_game()

#game_function.py

import sys
import pygame


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

函数update_screen()

为进一步简化run_game(),将更新屏幕的代码移到一个名为update_screen()的函数里,并将函数定义放在game_function中

def update_screen(sett, screen, ship):
    screen.fill(sett.bg_color)
    ship.blitme()
    # 让最近绘制的屏幕可见
    pygame.display.flip()




飞船移动部分


响应按键

每当用户按键时,都在Pygame里注册一个事件。事件都是通过方法pygame.event.get()获取的,因此在函数check_events()中,我们需要制定检查哪些类型的事件。
每次按键都被注册一个KEYDOWN事件。检测到该事件后,我们需要检查是否按下了特定的键,执行特定的操作。比如按下右键后,要让飞船向右移动。

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.rect.centerx += 1       

我们在参数列表里加入了ship,因为需要能够访问到飞船内部的属性。

允许不断移动

玩家按住→键是希望飞船不停移动,直到松开为止。
我们可以让游戏检测pygame.KEYUP事件,然后结合KEYUP和KEYDOWN事件实现持续移动。

# ship.py
        def __init__(self, screen):
        ...
        self.right_move = False
	
	def update(self):
        if self.right_move:
            self.rect.centerx += 1

在飞船的类内初始化时多添了一个属性移动标志变量right_move
多添了一个方法update(),用于检查该标志变量,实现飞船属性更新:这个变量为True时,飞船就会向右移动。
而这个标志变量会因KEYDOWN变为True,因KEYUP变为False,以此来实现持续移动

同时,要在invasion.py的while循环里调用update()方法:

# invasion.py   
    while True:
        gf.check_events(ship)
        ship.update()
        gf.update_screen(sett, screen, ship)

左右移动

只需照着向右移动就能做出向左移动

调整飞行速度

每次执行while循环,飞船最多移动1像素。但可以在settings模块里加入属性ship_speed,用于控制飞船的速度。

# settings.py
class Settings():
    """存储游戏所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200
        self.screen_length = 700
        self.bg_color = (230, 230, 230)
        self.ship_speed = 0.7

同时在ship.py中修改:

class Ship():

    def __init__(self, sett, screen):
        ...
        self.center = float(self.rect.centerx)
        ...
        
    	...

    def update(self):
        if self.right_move:
            self.center += sett.ship_speed
        if self.left_move:
            self.center -= sett.ship_speed

        self.rect.centerx = self.center
  • 在__init__()的形参中加入了setting类的sett,让飞船的方法update()可以获取其速度设置。
  • rect只存储整数,所以我们新建一个属性center,用flota()rect.centerx转化成小数存储到center中。更新center之后,再根据它来更新控制飞船位置的rect.centerx(虽然centerx只存储self.center的整数部分,但对于显示飞船而言问题不大。)

限制飞船活动范围

为了防止飞船飞出屏幕外,我们在飞船位置变更前添加if语句判断飞船是否将飞出框外。

# ship.py
    def update(self):
        if self.right_move and self.rect.right < self.screen_rect.right:
            self.center += self.sett.ship_speed
        if self.left_move and self.rect.left > 0:
            self.center -= self.sett.ship_speed

如果rect的左/右边缘没有触及屏幕左/右边缘,才可以移动。

重构check_event()

# game_function.py
def check_keydown(event, ship):
    if event.key == pygame.K_RIGHT:
        ship.right_move = True
    elif event.key == pygame.K_LEFT:
        ship.left_move = True


def check_keyup(event, ship):
    if event.key == pygame.K_RIGHT:
        ship.right_move = False
    elif event.key == pygame.K_LEFT:
        ship.left_move = 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(event, ship)

        elif event.type == pygame.KEYUP:
            check_keyup(event, ship)


子弹部分

添加子弹设置

在setting.py中添加新类Bullet所需的值:

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        ...
        # 子弹设置
        self.bullet_speed = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)

创建Bullet类

# bullet.py
import pygame
from pygame.sprite import Sprite


class Bullet(Sprite):
    """一个对飞船的子弹管理的类"""

    def __init__(self, sett, screen, ship):
        """在飞船处创建一个子弹对象"""
    	super(Bullet, self).__init__()
    	self.screen = screen

    	self.rect = pygame.Rect(0, 0, sett.bullet_width, sett.bullet_height)
    	self.rect.centerx = ship.rect.centerx
    	self.rect.top = ship.rect.top

    	self.y = float(self.rect.y)

    	self.color = sett.bullet_color
    	self.speed_factor = sett.bullet_speed

子弹并非基于图像,因此我们必须使用pygame.Rect()类从空白开始创建一个矩形。创建这个类的实例时,必须提供矩形左上角的x坐标和y坐标,还有宽度和高度。我们先在(0,0)处创建一个矩形,并在接下来放在正确的位置,这个位置取决于飞船的位置。

接下来编写update()draw_bullet方法

    def update(self):
        """向上移动子弹"""
        
    	self.y -= self.speed
    	self.rect.y = self.y
     
    def draw_bullet(self):
        pygame.draw.rect(self.screen, self.color, self.rect)

将子弹存到编组中

在玩家每次按下空格时都射出一发子弹。首先我们在invasion.py中创建一个编组(Group)用于存储所有子弹,以便能够管理发射出去的子弹。
这个编组是pygame.sprite.Group类的一个实例;Group类 类似于列表,但提供了有助于游戏开发的功能。在主循环中,我们使用这个编组在屏幕上绘制子弹,更新每一个子弹的位置。

import sys
import pygame
import game_function as gf

from pygame.sprite import Group
from settings import Settings
from ship import Ship


def run_game():
    # 初始化游戏并创建一个屏幕对象
    ...
    bullets = Group()
    # 开始游戏的主循环
    while True:
        gf.check_events(sett, screen, ship, bullets)
        ship.update()
        bullets.update()
        gf.update_screen(sett, screen, ship, bullets)


run_game()

我们将bullets作为实参传递给了check_events()和update_screen()。在check_event()中我们要用空格处理bullets;在update_screen中则要更新绘制到屏幕上的bullets。
当你对编组调用update()时,编组将自动对每一个"精灵"调用update(),即对每一个子弹。

开火

因为只有在按下空格键时飞船才会开火,所以我们只需修改check_keydown_events()而不用修改keyup

# game_function.py
from bullet import  Bullet

def check_keydown(event, sett, screen, ship, bullets):
    if event.key == pygame.K_RIGHT:
        ship.right_move = True
    elif event.key == pygame.K_LEFT:
        ship.left_move = True
    elif event.key == pygame.K_SPACE:
        new_bullet = Bullet(sett, screen, ship)
        bullets.add(new_bullet)

def check_events(sett, screen, ship, bullets):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            check_keydown(event, sett, screen, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup(event, ship)

def update_screen(sett, screen, ship, bullets):
    screen.fill(sett.bg_color)
    ship.blitme()
    for bullet in bullets:
        bullet.draw_bullet()
    # 让最近绘制的屏幕可见
    pygame.display.flip()

删除已经消失的子弹

我们需要将已经飞出屏幕的子弹删除,减少内存负担。
为此,我们需要在每次更新子弹位置后,检测rect的bottom属性小于0的子弹,并删除它们。

# invasion.py
    while True:
        gf.check_events(sett, 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(sett, screen, ship, bullets)
  • 在for循环中,不应从列表或编组中删除条目,因此必须是遍历编组的副本,故需要调用方法copy(),返回一个编组的副本。
  • 输出编组的长度,即有效子弹的数量,是为了显示子弹的数量,核实已消失的子弹确实被删除了。

子弹效果如图:
在这里插入图片描述

限制子弹数量

多数同类型游戏里面都会有对子弹数量的限制,鼓励玩家有目标地射击。
我们在此限制子弹最大数量为4.

首先在Setting类里设置允许的最大子弹数:

	#setting.py
	
   class Settings():
   """存储游戏所有设置的类"""

   def __init__(self):
       """初始化游戏的设置"""
       # 屏幕设置
       ...
       # 子弹设置
       ...
       self.bullet_allowed = 4	

在check_keydown_event()中检测到空格前,添加if语句判断子弹数量(群组长度)是否已经超过最大限制。

# game_function.py

def check_keydown(event, sett, screen, ship, bullets):
    if event.key == pygame.K_RIGHT:
        ship.right_move = True
    elif event.key == pygame.K_LEFT:
        ship.left_move = True
    elif event.key == pygame.K_SPACE:
        if len(bullets) < sett.bullet_allowed:
            new_bullet = Bullet(sett, screen, ship)
            bullets.add(new_bullet)

重构bullet函数

我们可以把子弹更新函数和删除子弹的代码写进一个函数update_bullet()里:

# game_function.py

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

故主循环里的代码可简化:

# invasion.py

    while True:
        gf.check_events(sett, screen, ship, bullets)
        ship.update()

        gf.update_bullet(bullets)
        gf.update_screen(sett, screen, ship, bullets)

同时,把检查子弹数量是否超额的代码已经添加新子弹的代码整合进一个fire_bullet()函数里:

# game_function.py

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

def check_keydown(event, sett, screen, ship, bullets):
    if event.key == pygame.K_RIGHT:
        ship.right_move = True
    elif event.key == pygame.K_LEFT:
        ship.left_move = True
    elif event.key == pygame.K_SPACE:
        fire_bullet(sett, screen, ship, bullets)

外星人部分


创建Alien类

# alien.py

import pygame
from pygame.sprite import Sprite


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

    def __init__(self, sett, screen):
        super().__init__()
        self.screen = screen
        self.setting = sett

        # 加载外星人图像,设置rect属性
        self.image = pygame.image.load('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)

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

创建Alien实例

# invasion.py

def run_game():
    # 初始化游戏并创建一个屏幕对象
    ...
    alien = Alien(sett, screen)
    ...
    # 开始游戏的主循环
    while True:
        ...
        gf.update_screen(sett, screen, ship, alien, bullets)
		# update_screen里调用 alien.blitme()

run_game()

在这里插入图片描述

创建一群外星人

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

我们要根据屏幕水平宽度确定一行可容纳多少外星人。我们要在屏幕两边留下边距,把它设置为外星人图像的宽度。所以放置外星人的水平空间为:

available_space_x = sett.screen_width - (2 * alien_width)


外星人之间还得留下空间,设置为一个外星人的宽度。因此一行可容纳的外星人数量:

number_aliens_x = available_space_x / (2 * alien_width)

创建多行外星人

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

# invasion.py

...
ship = Ship(sett, screen)
    bullets = Group()
    aliens = Group()
	aliens = Group()

    gf.create_fleet(sett, screen, aliens)
    # 开始游戏的主循环
    while True:
        ...
        gf.update_screen(sett, screen, ship, aliens, bullets)
# game_function.py

def create_fleet(sett, screen, aliens):
    """创建外星人群"""
    # 创建一个外星人,并计算一行可容纳多少外星人
    alien = Alien(sett, screen)
    alien_width = alien.rect.width
    available_space_x = sett.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(sett, screen)
        alien.x = alien_width + 2 * alien_width * alien_number
        alien.rect.x = alien.x
        aliens.add(alien)


def update_screen(sett, screen, ship, aliens, bullets):
    ...
    ...
    aliens.draw(screen)
    ...
    # 让最近绘制的屏幕可见
    ...

效果如图:
在这里插入图片描述

重构create_fleet()

为create_fleet()新添两个函数create_alien()get_number_aliens_x()

# game_function.py

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


def create_alien(sett, screen, aliens, alien_width, alien_number):
    # 创建一个外星人并加入群组
    alien = Alien(sett, screen)
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    aliens.add(alien)


def create_fleet(sett, screen, aliens):
    """创建外星人群"""
    # 创建一个外星人,并计算一行可容纳多少外星人
    alien = Alien(sett, screen)
    alien_width = alien.rect.width
    number_aliens_x = get_number_aliens_x(sett, alien_width)

    # 创建第一行外星人
    for alien_number in range(number_aliens_x):
        create_alien(sett, screen, aliens, alien_width, alien_number)

添加行

要创建外星人群,需要计算屏幕可容纳多少行,并对创建一行外星人的循环重复相应的次数。为计算可容纳的行数,我们将屏幕高度减去第一行的外星人的上边距(外星人高度)、飞船的高度以及最初外星人高度加上外星人边距:

available_space_y = sett.screen_height - 3*alien_height - ship_height

这样可以给飞船上方留出一定空白区域。

每行下方都要留出一定的空白区域,并将其设置为外星人的高度。为计算可容纳的行数,我们将可用垂直空间除以外星人高度的两倍:

number_rows = available_space_y /(2 * alien_height)

# game_funtion.py

def get_nuber_rows(sett, alien_height, ship_height):
    available_space_y = sett.screen_length - 3 * alien_height - ship_height
    number_rows = int(available_space_y / (2 * alien_height))
    return number_rows


def create_alien(sett, screen, aliens, alien_width, alien_number, row_number):
    # 创建一个外星人并加入群组
    ...
    alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
    ...


def create_fleet(sett, screen, ship, aliens):
    """创建外星人群"""
    # 创建一个外星人,并计算一行可容纳多少外星人
    ...
    number_rows = get_nuber_rows(sett, alien_height, ship.rect.height)
    # 创建第一行外星人
    for row_number in range(number_rows):
        for alien_number in range(number_aliens_x):
            create_alien(sett, screen, aliens, alien_width, alien_number, row_number)

在这里插入图片描述

移动外星人

让外星人向右移动

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

# setting.py

class Settings():
    """存储游戏所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        ...
        # 子弹设置
        ...
        self.alien_speed = 1

然后在Alien类里实现update():

# alien.py

    def update(self):
        self.x += self.setting.alien_speed
        self.rect.x = self.x

接着在game_function.py里编写update_aliens()

# game_function.py

def update_aliens(aliens):
    aliens.update()

aliens编组将自动对每一个外星人调用update()。

在主循环里调用update_aliens(aliens):

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

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

让外星人在撞到屏幕右边缘后会向下移动,再向左移动,代码如下:

# setting.py

self.alien_speed = 1
self.alien_drop_speed = 10
# 下降速度
self.fleet_direction = 1
# 1表示向右,-1表示向左, 可以直接作为速度的系数用于坐标运算

检查外星人是否撞到边缘

检查外星人是否撞到边缘,为类Alien编写方法check_edges()

# 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 <= screen_rect.left:
         return True

同时修改update():

    def update(self):
        self.x += self.setting.alien_speed * self.setting.fleet_direction
        self.rect.x = self.x

向下移动并改变移动方向

一个外星人到达屏幕边缘时,需要将整群外星人下移并转向。所以我们需要对game_function.py做大修改,因为我们需要检查每一个外星人是不是已经到了边缘。为此我们编写check_fleet_edge()change_fleet_dir()

# game_function.py

def change_fleet_dir(sett, aliens):
    """将整群外星人下移"""
    for alien in aliens:
        alien.rect.y += sett.alien_drop_speed
    sett.fleet_direction *= -1


def check_fleet_edges(sett, aliens):
    """有一个外星人到达边缘"""
    for alien in aliens.sprites():
        if alien.check_edges():
            change_fleet_dir(sett, aliens)
            break


def update_aliens(sett, aliens):
    check_fleet_edges(sett, aliens)
    aliens.update()

同时修改主循环中update_aliens()的参数:


射杀外星人

检测子弹与外星人的碰撞

子弹击中外星人时,我们要让外星人消失。为此我们需要在更新子弹位置后判断其是否碰撞。

我们用sprite.groupcollide()方法来检测两个群组的成员是否有碰撞。
它将每颗子弹的rect同每个外星人的rect进行比较,并返回一个字典,其中包含发生碰撞的子弹和外星人。在这个字典中,每个键都是一颗子弹,而对应的值都是被击中的外星人。(这个字典在之后计分要用到)

# game_function.py

def update_bullets(aliens, bullets):
    ...
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)

这行代码先遍历每颗子弹再遍历每个外星人,每当有两者rect重叠,它就在返回的字典中添加一对键值对。最后两个实参告诉pygame删除发生碰撞的子弹和外星人(第一个true表示子弹会被删除,如果改为false则子弹碰撞时不会被删除,而是一直飞到屏幕外)

接着要在invasion.py中的update_bullets()参数中添加aliens。

在这里插入图片描述

生成新的外星人群

当一个外星人群被消灭后,应该再出现另一群外星人。
我们先检查编组aliens是否为空,如果为空,就调用create_fleet()。我们将在update_bullets()中进行这个检查,因为外星人都是在这里被消灭的

# game_function.py

def update_bullets(sett, screen, ship, aliens, bullets):
   ...
   if len(aliens) == 0:
   create_fleet(sett, screen, ship, aliens)

同时要修改invasion.py中update_bullets的参数。

重构update_bullets()

# game_function.py

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


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

结束游戏

需要添加失败事件:外星人撞到飞船,或者有外星人降到屏幕底端,飞船将会被摧毁,玩家用光生命树后游戏结束。

检测飞船与外星人碰撞

# game_function.py

def update_aliens(sett, ship, aliens):
    check_fleet_edges(sett, aliens)
    aliens.update()

    if pygame.sprite.spritecollideany(ship, aliens):
        print("shit!")

方法spritecollideany()接受两个实参:一个精灵和一个编组。它检查编组是否有其他成员与精灵发生了碰撞,并在找到与精灵发生碰撞的成员后停止遍历,返回True. 如果没有碰撞则返回None。

响应外星人与飞船碰撞

飞船与外星人碰撞后:飞船生命-1、全屏外星人和子弹清空并暂停一段时间后出现新的外星人群。

寻找编写一个用于跟踪游戏统计信息的新类–Gamestats, 并将其保存为文件stats. py :

# stats.py

class Gamestats():

    def __init__(self, sett):
        self.sett = sett
        self.reset_stats()
        self.active = False

    def reset_stats(self):
        self.life = 1

同时在invasion.py中创建一个名为stats的实例

sett = Settings()
stats = Gamestats(sett)

接着编写飞船碰撞时的响应:

# game_function.py

def ship_hit(sett, stats, screen, ship, aliens, bullets):
    stats.life -= 1

    aliens.empty()
    bullets.empty()

    create_fleet(sett, screen, ship, aliens)
    ship.center = screen.get_rect().centerx
    # 将飞船调整至中心位置

    sleep(0.5)


def update_aliens(sett, stats, screen, ship, aliens, bullets):
    check_fleet_edges(sett, aliens)
    aliens.update()

    if pygame.sprite.spritecollideany(ship, aliens):
        ship_hit(sett, stats, screen, ship, aliens, bullets)

同时要在invasion.py中修改update_aliens()参数列表

有外星人到达底部

为此我们写一个函数check_alien_bottom()

# game_function.py

def alien_bottom(sett, stats, screen, ship, aliens, bullets):
    screen_rect = screen.get_rect()
    for alien in aliens:
        if alien.rect.bottom >= screen_rect.bottom:
            ship_hit(sett, stats, screen, ship, aliens, bullets)
     
def update_aliens(sett, stats, screen, ship, aliens, bullets):
    check_fleet_edges(sett, aliens)
    aliens.update()

    if pygame.sprite.spritecollideany(ship, aliens) or alien_bottom(sett, stats, screen, ship, aliens, bullets):
        ship_hit(sett, stats, screen, ship, aliens, bullets)

游戏结束

当life减为0后,游戏结束。我们在GameStats里添加一个作为标志的属性active,以便在玩家的飞船用完后结束游戏:

# stats.py
class Gamestats():

    def __init__(self, sett):
        self.sett = sett
        self.life = 3
        self.active = True

    def reset_stats(self):
        self.life = 3
        self.active = True

当玩家的生命减为0时,该变量变为false

# game_function.py

def ship_hit(sett, stats, screen, ship, aliens, bullets):

    stats.life -= 1
    if stats.life>0:
        aliens.empty()
        bullets.empty()

        create_fleet(sett, screen, ship, aliens)
        ship.center = screen.get_rect().centerx

        sleep(0.5)

    else:
        stats.active = False

添加PLAY按钮

添加PLAY按钮,让程序开始时处于非活动状态,则要修改stats.py中的代码

class Gamestats():

    def __init__(self, sett):
        self.sett = sett
        self.life = 3
        self.active = False

    def reset_stats(self):
        self.life = 3

创建Button类

由于pygame没有内置创建按钮的方法,所以我们创建一个Button类

# button.py

import pygame.font


class Button():

    def __init__(self, sett, screen, msg):
        self.screen = screen
        self.screen_rect = screen.get_rect()

        self.width, self.height = 200, 50
        self.button_color = (0, 255, 0)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)
        # 指定字体字号来渲染文字

        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):
        """将字符串渲染成图像"""
        # 第二个布尔参数是反锯齿开关
        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)
        self.screen.blit(self.msg_image, self.msg_image_rect)

在屏幕上绘制按钮

只需要一个Play按钮,故我们直接在invasion.py中创建

...
from button import Button

def run_game():
    # 初始化游戏并创建一个屏幕对象
    ...
    pygame.display.set_caption("Thunder")

    play_button = Button(sett, screen, 'PLAY')
    ...
    # 开始游戏的主循环
    while True:
        ...
        gf.update_screen(sett, screen, stats, ship, aliens, bullets, play_button)


run_game()

接着修改game_function.py的update_screen,以便在游戏处于非活动状态时显示按钮

def update_screen(sett, screen, stats, ship, aliens, bullets, button):
    ...
    if not stats.active:
        button.draw_button()
	# 让最近绘制的屏幕可见
    pygame.display.flip()

一定要把draw放在flip前面,这样才能让绘制完所有其他元素之后再绘制按钮,然后切换到新屏幕。

在这里插入图片描述

开始游戏

在按下按钮时开始新游戏,需要对鼠标事件进行监视。
在game_function.py中添加如下代码:

def check_play(stats, button, mouseX, mouseY):
    if button.rect.collidepoint(mouseX, mouseY):
        stats.active = True


def check_events(sett, screen, stats, button, ship, bullets):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            check_keydown(event, sett, screen, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup(event, ship)
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_X, mouse_Y = pygame.mouse.get_pos()
            check_play(stats, button, mouse_X, mouse_Y)

重置游戏

游戏结束后,会再显示PLAY按钮。每次单击它都应该重置整个游戏,重置统计信息,删除现有的外星人和子弹,创建新的外星人,让飞船居中。

def check_play(sett, screen, stats, button, ship, aliens, bullets, mouseX, mouseY):
    if button.rect.collidepoint(mouseX, mouseY):
        stats.reset_stats()
        stats.active = True

        aliens.empty()
        bullets.empty()

        create_fleet(sett, screen, ship, aliens)
        ship.center = screen.get_rect().centerx


def check_events(sett, screen, stats, button, ship, aliens, bullets):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            check_keydown(event, sett, screen, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup(event, ship)
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_X, mouse_Y = pygame.mouse.get_pos()
            check_play(sett, screen, stats, button, ship, aliens, bullets, mouse_X, mouse_Y)

将Play按钮切换到非活动状态

有一个问题是,即使在游戏活动状态,按钮图形不会显示,但是点击其原来的位置依然会重置游戏。所以要在监视鼠标事件时添加一个if条件:

def check_play(sett, screen, stats, button, ship, aliens, bullets, mouseX, mouseY):
    if button.rect.collidepoint(mouseX, mouseY) and not stats.active:
        ...

隐藏光标

在点击Play后的游戏活动状态,鼠标光标应该被隐藏

def check_play(sett, screen, stats, button, ship, aliens, bullets, mouseX, mouseY):
    if button.rect.collidepoint(mouseX, mouseY) and not stats.active:
        pygame.mouse.set_visible(False)
        stats.reset_stats()
        stats.active = True

        aliens.empty()
        bullets.empty()

        create_fleet(sett, screen, ship, aliens)
        ship.center = screen.get_rect().centerx

并在游戏结束后重现它:

def ship_hit(sett, stats, screen, ship, aliens, bullets):

    stats.life -= 1
    if stats.life > 0:
        ...
    else:
        pygame.mouse.set_visible(True)
        stats.active = False

提高难度

随着游戏的进行,游戏的难度应当得到提升。

修改速度设置

我们要通过提高游戏整体速度来提升难度,所以飞船、子弹、外星人的速度是在变的。为此我们可以将settting里的设置属性分为静态和动态两部分。

class Settings():
    """存储游戏所有设置的类"""
    
    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200
        self.screen_length = 700
        self.bg_color = (230, 230, 230)
        # 子弹设置
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 4
        self.speed_scale = 1.1
        # 速度提升率
        self.init_dynamic()
        
    def init_dynamic(self):
        self.alien_speed = 1
        self.alien_drop_speed = 10
        # 下降速度
        self.fleet_direction = 1
        # 1表示向右,-1表示向左, 可以直接作为速度的系数用于坐标运算
        self.bullet_speed = 2
        self.ship_speed = 1.5

接着编写提升速度的方法increase_speed()

    def increase_speed(self):
        self.alien_speed *= self.speed_scale
        self.bullet_speed *= self.speed_scale
        self.ship_speed *= self.speed_scale

并在每消灭一群外星人时调用一次这个方法:

def check_bullet_collision(sett, screen, ship, bullets, aliens):
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    if len(aliens) == 0:
        create_fleet(sett, screen, ship, aliens)
        sett.increase_speed()

重置速度

每次开始新游戏时,速度都要重置一次

def check_play(sett, screen, stats, button, ship, aliens, bullets, mouseX, mouseY):
    if button.rect.collidepoint(mouseX, mouseY) and not stats.active:
        ...
        sett.init_dynamic()


记分

在stats类里添加一个属性记录得分

class Gamestats():

    def __init__(self, sett):
        self.sett = sett
        self.reset_stats()
        self.active = False

    def reset_stats(self):
        self.life = 1
        self.score = 0

显示得分

为了在屏幕上显示得分,我们首先创建一个新类scoreboard:

import pygame.font

class ScoreBoard():

    def __init__(self, sett, screen, stats):
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.sett = sett
        self.stats = stats

        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)

        self.prep_score()

    def prep_score(self):
        score_str = str(self.stats.score)
        self.score_image = self.font.render(score_str, True, self.text_color, self.sett.bg_color)

        self.score_rect = self.score_image.get_rect()
        self.score_rect.right = self.screen_rect.right - 20
        self.score_rect.top = 20

	def show_score(self):
		self.screen.blit(self.score_image, self.score_rect)

创建记分牌实例

...
from scoreboard import ScoreBoard


def run_game():
    # 初始化游戏并创建一个屏幕对象
    ...
    sb = ScoreBoard(sett, screen, stats)

    ...
    # 开始游戏的主循环
    while True:
        ...
        gf.update_screen(sett, screen, stats, sb, ship, aliens, bullets, play_button)


run_game()

同时要在update_screen()中调用show_score()

得分

击杀外星人后要增加分数。只需要检查子弹击中外星人时返回的字典(collision)即可.
我们现在setting中设置一个外星人的得分。

    def __init__(self):
        """初始化游戏的设置"""
       	...
        self.alien_score = 50

接着在check_bullet_collision()中检查字典。(这个字典的键是一颗子弹,值是被这颗子弹击中的外星人列表)

def check_bullet_collision(sett, screen, stats, sb, ship, bullets, aliens):
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    ...
    if collisions:
    	for aliens in collisions.key():
        	stats.score += sett.alien_score * len(aliens)
        	sb.prep_score()

主循环中要修改update_bullets()的参数

提高点数

随着游戏难度提升,一个外星人的得分应当提高。
所以在setting中增加一个得分提升的幅度属性

在游戏难度提升时,即速度提高时,修改setting的属性alien_score
(因为alien_score会变动,所以要把这个属性分类为动态,使其在动态初始化方法中被赋初值)

class Settings():
    """存储游戏所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        ...
        self.init_dynamic()


    def init_dynamic(self):
        ...
        self.alien_score = 50

    def increase_speed(self):
        ...
        self.alien_score = int(self.alien_score * self.score_scale)

将得分圆整

大部分游戏都会将游戏得分显示为10的整倍数。我们可以把得分圆整。

 # scoreboard.py
    
    def prep_score(self):
        rounded_score = int(round(self.stats.score, -1))
        score_str = "{:,}".format(rounded_score)
        ...

round()的第二个参数为精确到的小数位。
第二个参数为负数,则round()将圆整到最近的10、100、1000等整倍数。

"{:,}".format(rounded_score)为一个字符串格式设置指令,它让Python将数值转换成字符串时在其中插入逗号。

最高分

我们在stats中增加一个属性最高分,并将其展示在屏幕顶端中央。
但是为了让数据保存,这个最高分存储在外部文件中,所以每次都需要从外部文件读入:

class Gamestats():

    def __init__(self, sett):
        self.sett = sett
        self.reset_stats()
        with open("highScore.txt", 'r') as hs:
            self.high_score = int(hs.read())

每当一场游戏结束后,都要更新最高分:

def ship_hit(sett, stats, screen, ship, aliens, bullets):

    stats.life -= 1
    if stats.life > 0:
        ...

    else:
        pygame.mouse.set_visible(True)
        stats.active = False
        if stats.score > stats.high_score:
            stats.high_score = stats.score

每次关闭前都要在外部文件更新最高分:

def check_events(sett, screen, stats, button, ship, aliens, bullets):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            with open("highScore.txt", 'w') as hs:
                hs.write(str(stats.high_score))
            sys.exit()
        ...

接着要在最顶端显示最高分:

# scoreboard.py

class ScoreBoard():

    def __init__(self, sett, screen, stats):
        ...

        self.prep_score()
        self.prep_high()

    def prep_score(self):
        ...

    def prep_high(self):
        high_score_str = "{:,}". format(self.stats.high_score)
        print(high_score_str)
        self.high_score_image = self.font.render(high_score_str, True, self.text_color, self.sett.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.screen_rect.top
        
    def show_score(self):
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)

同时,在更新过最高分之后调用prep_high()

def ship_hit(sett, stats, screen, ship, sb, aliens, bullets):

    stats.life -= 1
    if stats.life > 0:
        ...
    else:
        pygame.mouse.set_visible(True)
        stats.active = False
        if stats.score > stats.high_score:
            stats.high_score = stats.score
            sb.prep_high()

用到ship_hit()的地方都要修改参数

在这里插入图片描述

奖励子弹

我们新增一个机制,当玩家分数达到一定程度后,我们将接下来的3发子弹的宽度提高100倍,提高消灭外星人的效率。

首先,现在stats.py中设置奖励标准:

class Gamestats():

    def __init__(self, sett):
        ...
    def reset_stats(self):
        ...
        self.award_level = 1 # 奖励等级
        self.bullet_award = False # 奖励状态
        self.award_b = 0 # 已用奖励子弹数量
        self.award_score = 1500 # 奖励分数标准

因为外星人的分数会随着游戏难度增加而增加,所以奖励分数标准应该在每一次奖励后增加。所以我们在setting.py中增加一个属性award_score_scale

class Settings():
    """存储游戏所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200
        self.screen_length = 700
        self.bg_color = (230, 230, 230)
        # 子弹设置
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 4
        self.speed_scale = 1.1
        # 速度提升率
        self.score_scale = 1.5
        self.award_score_scale = 1.4
        # 分数提升率

接着,在game_funciton.py中编写判断函数:

def award(sett, stats):
    if stats.score >= stats.award_level * stats.award_score:
        stats. bullet_award = True
        sett.bullet_width = 300
        stats.award_level += 1
        stats.award_b = 0
        stats.award_score *= sett.award_score_scale

每此奖励完之后,奖励等级(award_level)要提升,奖励分数标准(award_score)要提升,已用奖励子弹数(award_b)清零。

然后要让奖励状态在三发子弹后变回False。因为子弹是在按下空格后发射,所以我们可以在检测空格事件的函数中实现:

def award_check(sett, stats):
    if stats.bullet_award:
        if stats.award_b == 3:
            stats.bullet_award = False
            sett.bullet_width = 3
        stats.award_b += 1


def check_keydown(event, sett, screen, stats, ship, bullets):
    if event.key == pygame.K_RIGHT:
        ship.right_move = True
    elif event.key == pygame.K_LEFT:
        ship.left_move = True
    elif event.key == pygame.K_SPACE:
        if stats.bullet_award:
            award_check(sett, stats)
            fire_bullet(sett, screen, ship, bullets)

因为奖励状态是随着得分转变的,所以我们在得分的函数里调用award(),即check_bullet_collision():

def check_bullet_collision(sett, screen, stats, sb, ship, bullets, aliens):
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    if len(aliens) == 0:
        create_fleet(sett, screen, ship, aliens)
        sett.increase_speed()
    if collisions:
        for aliens in collisions.values():
            stats.score += sett.alien_score * len(aliens)
            sb.prep_score()
            award(sett, stats)

在这里插入图片描述

左上角显示剩余生命

最后,我们来显示玩家还剩多少艘飞船,但用的是图形而不是数字。
首先,需要让Ship继承Sprite,以便创建飞船编组:

import pygame
from pygame.sprite import Sprite


class Ship(Sprite):

    def __init__(self, sett, screen):
        ...
        super().__init__()

接着在scoreboard. py 中,创建一个可供显示的飞船编组。

    def show_score(self):
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)
        self.ships.draw(self.screen)

    def prep_ship(self):
        """显示剩余飞船"""
        self.ships = Group()
        for ship_num in range(self.stats.life):
            ship = Ship(self.sett, self.screen)
            ship.rect.x = 10 + ship_num * ship.rect.width
            ship.rect.y = 10
            self.ships.add(ship)

要在游戏开始时显示这个剩余生命,所以我们在开始新游戏时调用prep_ships()。这个将在check_play()中进行:

def check_play(sett, screen, stats, button, ship, sb, aliens, bullets, mouseX, mouseY):
    if button.rect.collidepoint(mouseX, mouseY) and not stats.active:
        ...
        sb.prep_score()
        sb.prep_ship()

同时,当损失生命值时,左上角的生命牌需要更新,要在ship_hit()中还要调用prep_ship()

def ship_hit(sett, stats, screen, ship, sb, aliens, bullets):

    stats.life -= 1
    if stats.life > 0:
        ...

    else:
        ....

    sb.prep_ship()
  • 别忘了对对相关函数的参数列表修改
    在这里插入图片描述

最后的重构

  • 将清屏和重新创建舰队的代码编写为一个函数clear_recreate()
  • 将点击PLAY按钮后的分数板和动态设置重置的函数整合为restart()
  • 将scoreboard .py中的__init__()调用的prep方法整合

最终代码:

# ship.py

import pygame
from pygame.sprite import Sprite


class Ship(Sprite):

    def __init__(self, sett, screen):
        self.screen = screen
        self.sett = sett
        self.image = pygame.image.load("ship.bmp")
        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
        self.center = float(self.rect.centerx)

        self.right_move = False
        self.left_move = False
        super().__init__()

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

    def update(self):
        if self.right_move and self.rect.right < self.screen_rect.right:
            self.center += self.sett.ship_speed
        if self.left_move and self.rect.left > 0:
            self.center -= self.sett.ship_speed

        self.rect.centerx = self.center

# alien.py

import pygame
from pygame.sprite import Sprite


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

    def __init__(self, sett, screen):
        super().__init__()
        self.screen = screen
        self.setting = sett

        # 加载外星人图像,设置rect属性
        self.image = pygame.image.load('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)

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

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

    def update(self):
        self.x += self.setting.alien_speed * self.setting.fleet_direction
        self.rect.x = self.x

# bullet.py

import pygame
from pygame.sprite import Sprite


class Bullet(Sprite):
    """一个对飞船的子弹管理的类"""
    def __init__(self, sett, screen, ship):

        super().__init__()
        self.screen = screen

        self.rect = pygame.Rect(0, 0, sett.bullet_width, sett.bullet_height)
        self.rect.centerx = ship.rect.centerx
        self.rect.top = ship.rect.top

        self.y = float(self.rect.y)

        self.color = sett.bullet_color
        self.speed = sett.bullet_speed

    def update(self):
        """向上移动子弹"""

        self.y -= self.speed
        self.rect.y = self.y

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

# setting.py

class Settings():
    """存储游戏所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        # 屏幕设置
        self.screen_width = 1200
        self.screen_length = 700
        self.bg_color = (230, 230, 230)
        # 子弹设置
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullet_allowed = 4
        self.speed_scale = 1.1
        # 速度提升率
        self.score_scale = 1.5
        self.award_score_scale = 1.4
        # 分数提升率
        self.awared_width = 300
        # 奖励宽度
        self.init_dynamic()

    def init_dynamic(self):
        self.alien_speed = 1
        self.alien_drop_speed = 10
        # 下降速度
        self.fleet_direction = 1
        # 1表示向右,-1表示向左, 可以直接作为速度的系数用于坐标运算
        self.bullet_speed = 2
        self.ship_speed = 1.5
        self.alien_score = 50

    def increase_speed(self):
        self.alien_speed *= self.speed_scale
        self.bullet_speed *= self.speed_scale
        self.ship_speed *= self.speed_scale
        self.alien_score = int(self.alien_score * self.score_scale)


# stats.py

class Gamestats():

    def __init__(self, sett):
        self.sett = sett
        self.reset_stats()
        with open("highScore.txt", 'r') as hs:
            self.high_score = int(hs.read())

    def reset_stats(self):
        self.life = 3
        self.score = 0
        self.active = False
        self.award_level = 1
        self.bullet_award = False
        self.award_b = 0
        self.award_score = 1500

# button.py

import pygame.font


class Button():

    def __init__(self, sett, screen, msg):
        self.screen = screen
        self.screen_rect = screen.get_rect()

        self.width, self.height = 200, 50
        self.button_color = (0, 255, 0)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)
        # 指定字体字号来渲染文字

        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):
        """将字符串渲染成图像"""
        # 第二个布尔参数是反锯齿开关
        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)
        self.screen.blit(self.msg_image, self.msg_image_rect)
# scoreboard.py

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


class ScoreBoard():

    def __init__(self, sett, screen, stats):
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.sett = sett
        self.stats = stats

        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)

        self.prep_image()

    def prep_image(self):
        self.prep_score()
        self.prep_high()
        self.prep_ship()

    def prep_score(self):
        rounded_score = int(round(self.stats.score, -1))
        score_str = "{:,}".format(rounded_score)
        self.score_image = self.font.render(score_str, True, self.text_color, self.sett.bg_color)

        self.score_rect = self.score_image.get_rect()
        self.score_rect.right = self.screen_rect.right - 20
        self.score_rect.top = 20

    def prep_high(self):
        high_score_str = "{:,}". format(self.stats.high_score)
        self.high_score_image = self.font.render(high_score_str, True, self.text_color, self.sett.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.screen_rect.top

    def show_score(self):
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)
        self.ships.draw(self.screen)

    def prep_ship(self):
        """显示剩余飞船"""
        self.ships = Group()
        for ship_num in range(self.stats.life):
            ship = Ship(self.sett, self.screen)
            ship.rect.x = 10 + ship_num * ship.rect.width
            ship.rect.y = 10
            self.ships.add(ship)

# game_function.py

import sys
import pygame
from bullet import Bullet
from alien import Alien
from time import sleep


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


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


def get_nuber_rows(sett, alien_height, ship_height):
    available_space_y = sett.screen_length - 3 * alien_height - ship_height
    number_rows = int(available_space_y / (2 * alien_height))
    return number_rows


def create_alien(sett, screen, aliens, alien_width, alien_number, row_number):
    # 创建一个外星人并加入群组
    alien = Alien(sett, screen)
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
    alien.rect.x = alien.x
    aliens.add(alien)


def create_fleet(sett, screen, ship, aliens):
    """创建外星人群"""
    # 创建一个外星人,并计算一行可容纳多少外星人
    alien = Alien(sett, screen)
    alien_width = alien.rect.width
    alien_height = alien.rect.height
    number_aliens_x = get_number_aliens_x(sett, alien_width)
    number_rows = get_nuber_rows(sett, alien_height, ship.rect.height)
    # 创建第一行外星人
    for row_number in range(number_rows):
        for alien_number in range(number_aliens_x):
            create_alien(sett, screen, aliens, alien_width, alien_number, row_number)


def award_check(sett, stats):
    if stats.bullet_award:
        if stats.award_b == 3:
            stats.bullet_award = False
            sett.bullet_width = 3
        stats.award_b += 1


def check_keydown(event, sett, screen, stats, ship, bullets):
    if event.key == pygame.K_RIGHT:
        ship.right_move = True
    elif event.key == pygame.K_LEFT:
        ship.left_move = True
    elif event.key == pygame.K_SPACE:
        if stats.bullet_award:
            award_check(sett, stats)
        fire_bullet(sett, screen, ship, bullets)


def check_keyup(event, ship):
    if event.key == pygame.K_RIGHT:
        ship.right_move = False
    elif event.key == pygame.K_LEFT:
        ship.left_move = False


def clear_recreate(sett, screen, ship, aliens, bullets):
    """清除屏幕重新开始"""
    aliens.empty()
    bullets.empty()

    create_fleet(sett, screen, ship, aliens)
    ship.center = screen.get_rect().centerx


def restart(sett, sb):
    sett.init_dynamic()
    sb.prep_score()
    sb.prep_ship()


def check_play(sett, screen, stats, button, ship, sb, aliens, bullets, mouseX, mouseY):
    if button.rect.collidepoint(mouseX, mouseY) and not stats.active:
        pygame.mouse.set_visible(False)
        stats.reset_stats()
        stats.active = True

        clear_recreate(sett, screen, ship, aliens, bullets)
        restart(sett, sb)


def check_events(sett, screen, stats, button, ship, sb, aliens, bullets):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            with open("highScore.txt", 'w') as hs:
                hs.write(str(stats.high_score))
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            check_keydown(event, sett, screen, stats, ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup(event, ship)
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_X, mouse_Y = pygame.mouse.get_pos()
            check_play(sett, screen, stats, button, ship, sb, aliens, bullets, mouse_X, mouse_Y)


def check_bullet_collision(sett, screen, stats, sb, ship, bullets, aliens):
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    if len(aliens) == 0:
        create_fleet(sett, screen, ship, aliens)
        sett.increase_speed()
    if collisions:
        for aliens in collisions.values():
            stats.score += sett.alien_score * len(aliens)
            sb.prep_score()
            award(sett, stats)


def update_bullet(sett, screen, stats, sb, ship, bullets, aliens):
    bullets.update()
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)
    check_bullet_collision(sett, screen, stats, sb, ship, bullets, aliens)


def update_screen(sett, screen, stats, sb, ship, aliens, bullets, button):
    screen.fill(sett.bg_color)
    ship.blitme()
    sb.show_score()
    aliens.draw(screen)
    for bullet in bullets:
        bullet.draw_bullet()

    if not stats.active:
        button.draw_button()
    # 让最近绘制的屏幕可见
    pygame.display.flip()


def change_fleet_dir(sett, aliens):
    """将整群外星人下移"""
    for alien in aliens:
        alien.rect.y += sett.alien_drop_speed
    sett.fleet_direction *= -1


def check_fleet_edges(sett, aliens):
    """有一个外星人到达边缘"""
    for alien in aliens.sprites():
        if alien.check_edges():
            change_fleet_dir(sett, aliens)
            break


def alien_bottom(sett, stats, screen, ship, sb, aliens, bullets):
    screen_rect = screen.get_rect()
    for alien in aliens:
        if alien.rect.bottom >= screen_rect.bottom:
            ship_hit(sett, stats, screen, ship, sb, aliens, bullets)


def ship_hit(sett, stats, screen, ship, sb, aliens, bullets):

    stats.life -= 1
    if stats.life > 0:
        clear_restart(sett, screen, ship, aliens, bullets)
        sleep(0.5)

    else:
        pygame.mouse.set_visible(True)
        stats.active = False
        if stats.score > stats.high_score:
            stats.high_score = stats.score
            sb.prep_high()

    sb.prep_ship()


def update_aliens(sett, stats, screen, ship, sb, aliens, bullets):
    check_fleet_edges(sett, aliens)
    aliens.update()

    if pygame.sprite.spritecollideany(ship, aliens) or alien_bottom(sett, stats, screen, ship, sb, aliens, bullets):
        ship_hit(sett, stats, screen, ship, sb, aliens, bullets)


def award(sett, stats):
    if stats.score >= stats.award_level * stats.award_score:
        stats. bullet_award = True
        sett.bullet_width = sett.awared_width
        stats.award_level += 1
        stats.award_b = 0
        stats.award_score *= sett.award_score_scale

# invasion.py

import pygame
import game_function as gf

from pygame.sprite import Group
from settings import Settings
from ship import Ship
from stats import Gamestats
from button import Button
from scoreboard import ScoreBoard


def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    sett = Settings()
    stats = Gamestats(sett)
    screen = pygame.display.set_mode(
        (sett.screen_width, sett.screen_length)
    )
    pygame.display.set_caption("Thunder")

    play_button = Button(sett, screen, 'PLAY')
    ship = Ship(sett, screen)
    bullets = Group()
    aliens = Group()
    sb = ScoreBoard(sett, screen, stats)

    gf.create_fleet(sett, screen, ship, aliens)
    # 开始游戏的主循环
    while True:
        gf.check_events(sett, screen, stats, play_button, ship, sb, aliens, bullets)
        ship.update()
        gf.update_bullet(sett, screen, stats, sb, ship, bullets, aliens)
        gf.update_aliens(sett, stats, screen, ship, sb, aliens, bullets)
        gf.update_screen(sett, screen, stats, sb, ship, aliens, bullets, play_button)


run_game()