Python練手項目(1)Pygame製作遊戲

背景部分


創建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()的參數:

gf.update_aliens(sett, 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中增加一個得分提升的幅度屬性

self.score_scale = 1.5

在遊戲難度提升時,即速度提高時,修改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()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章