2020年9月26日星期六

013 Python编程 從入門到實踐, 第二部分 專案 第 13 章 外星人

申明本站飛宇網 https://feiyetopro.blogspot.com/自網路收集整理之書籍文章影音僅供預覽交流學習研究,其[書籍、文章、影音]情節內容, 評論屬其個人行為, 與本網站無關。版權歸原作者和出版社所有,請在下載 24 小時內刪除,不得用作商業用途;如果您喜歡其作品,請支持訂閱購買[正版]謝謝! 



13 章 外星人

在本章中,我們將在遊戲《外星人入侵》中添加外星人。首先,我們在螢幕上邊緣附近添加一個外星人,然後生成一群外星人。我們讓這群外星人向兩邊和下面移動,並刪除被子彈擊中的外星人。最後,我們將顯示玩家擁有的飛船數量,並在玩家的飛船用完後結束遊戲。

通過閱讀本章,你將更深入地瞭解Pygame和大型專案的管理。你還將學習如何檢測遊戲物件之間的碰撞,如子彈和外星人之間的碰撞。檢測碰撞有助於你定義遊戲元素之間的交互:可以將角色限定在迷宮牆壁之內或在兩個角色之間傳球。我們將時不時地查看遊戲開發計畫,以確保程式設計工作不偏離軌道。

著手編寫在螢幕上添加一群外星人的代碼前,先來回顧一下這個項目,並更新開發計畫。

13.1 回顧項目

開發較大的項目時,進入每個開發階段前回顧一下開發計畫,搞清楚接下來要通過編寫代碼來完成哪些任務都是不錯的主意。本章涉及以下內容。

·    研究既有代碼,確定實現新功能前是否要進行重構。

·    在螢幕左上角添加一個外星人,並指定合適的邊距。

·    根據第一個外星人的邊距和螢幕尺寸計算螢幕上可容納多少個外星人。我們將編寫一個迴圈來創建一系列外星人,這些外星人填滿了螢幕的上半部分。

·    讓外星人群向兩邊和下方移動,直到外星人被全部擊落,有外星人撞到飛船,或有外星人抵達螢幕底端。如果整群外星人都被擊落,我們將再創建一群外星人。如果有外星人撞到了飛船或抵達螢幕底端,我們將銷毀飛船並再創建一群外星人。

·    限制玩家可用的飛船數量,配給的飛船用完後,遊戲結束。

我們將在實現功能的同時完善這個計畫,但就目前而言,該計畫已足夠詳盡。

在給專案添加新功能前,還應審核既有代碼。每進入一個新階段,通常專案都會更複雜,因此最好對混亂或低效的代碼進行清理。

我們在開發的同時一直不斷地重構,因此當前需要做的清理工作不多,但每次為測試新功能而運行這個遊戲時,都必須使用滑鼠來關閉它,這太討厭了。下面來添加一個結束遊戲的快速鍵Q

game_functions.py

 

def check_keydown_events(event, ai_settings, screen, ship, bullets):

    --snip--

    elif event.key == pygame.K_q:

        sys.exit()

 

 

 

 

 

 

 

 

check_keydown_events() 中,我們添加了一個代碼塊,以便在玩家按Q時結束遊戲。這樣的修改很安全,因為Q鍵離方向鍵和空白鍵很遠,玩家不小心按Q鍵而導致遊戲結束的可能性不大。現在測試時可按Q關閉遊戲,而無需使用滑鼠來關閉視窗了。

13.2 創建第一個外星人

在螢幕上放置外星人與放置飛船類似。每個外星人的行為都由Alien 類控制,我們將像創建Ship 類那樣創建這個類。出於簡化考慮,我們也使用點陣圖來表示外星人。你可以自己尋找表示外星人的圖像,也可使用圖13-1所示的圖像,可在本書配套資源(https://www.nostarch.com/pythoncrashcourse/ )中找到。這幅圖像的背景為灰色,與螢幕背景色一致。請務必將你選擇的影像檔保存到資料夾images中。


13-1 用來創建外星人群的外星人圖像

13.2.1 創建Alien 

下麵來編寫Alien 類:

alien.py

 

  import pygame

  from pygame.sprite import Sprite

 

  class Alien(Sprite):

      """表示單個外星人的類"""

 

      def __init__(self, ai_settings, screen):

          """初始化外星人並設置其起始位置"""

          super(Alien, self).__init__()

          self.screen = screen

          self.ai_settings = ai_settings

 

          # 載入外星人圖像,並設置其rect屬性

          self.image = pygame.image.load('images/alien.bmp')

          self.rect = self.image.get_rect()

 

          # 每個外星人最初都在螢幕左上角附近

         self.rect.x = self.rect.width

          self.rect.y = self.rect.height

 

          # 存儲外星人的準確位置

          self.x = float(self.rect.x)

 

      def blitme(self):

          """在指定位置繪製外星人"""

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

 

 

 

 

 

 

 

 

除位置不同外,這個類的大部分代碼都與Ship 類相似。每個外星人最初都位於螢幕左上角附近,我們將每個外星人的左邊距都設置為外星人的寬度,並將上邊距設置為外星人的高度(見

13.2.2 創建Alien 實例

下麵在alien_invasion.py中創建一個Alien 實例:

alien_invasion.py

 

--snip--

from ship import Ship

from alien import Alien

import game_functions as gf

 

def run_game():

    --snip--

    # 創建一個外星人

    alien = Alien(ai_settings, screen)

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, ship, bullets)

        ship.update()

        gf.update_bullets(bullets)

        gf.update_screen(ai_settings, screen, ship, alien, bullets)

 

run_game()

 

 

 

 

 

 

 

 

在這裡,我們導入了新創建的Alien 類,並在進入主while 迴圈前創建了一個Alien 實例。我們沒有修改外星人的位置,因此該while 迴圈沒有任何新東西,但我們修改了對update_screen() 的調用,傳遞了一個外星人實例。

13.2.3 讓外星人出現在螢幕上

為讓外星人出現在螢幕上,我們在update_screen() 中調用其方法blitme() 

game_functions.py

 

def update_screen(ai_settings, screen, ship, alien, bullets):

    --snip--

 

    # 在飛船和外星人後面重繪所有的子彈

    for bullet in bullets:

        bullet.draw_bullet()

    ship.blitme()

    alien.blitme()

 

    # 讓最近繪製的螢幕可見

    pygame.display.flip()

 

 

 

 

 

 

 

 

我們先繪製飛船和子彈,再繪製外星人,讓外星人在螢幕上位於最前面。圖13-2顯示了螢幕上的第一個外星人。


13-2 第一個外星人現身

第一個外星人正確地現身後,下面來編寫繪製一群外星人的代碼。

13.3 創建一群外星人

要繪製一群外星人,需要確定一行能容納多少個外星人以及要繪製多少行外星人。我們將首先計算外星人之間的水準間距,並創建一行外星人,再確定可用的垂直空間,並創建整群外星人。

13.3.1 確定一行可容納多少個外星人

為確定一行可容納多少個外星人,我們來看看可用的水準空間有多大。螢幕寬度存儲在ai_settings.screen_width 中,但需要在螢幕兩邊都留下一定的邊距,把它設置為外星人的寬度。由於有兩個邊距,因此可用於放置外星人的水準空間為螢幕寬度減去外星人寬度的兩倍:

 

available_space_x = ai_settings.screen_width – (2 * alien_width)

 

 

 

 

 

 

 

 

我們還需要在外星人之間留出一定的空間,即外星人寬度。因此,顯示一個外星人所需的水準空間為外星人寬度的兩倍:一個寬度用於放置外星人,另一個寬度為外星人右邊的空白區域。為確定一行可容納多少個外星人,我們將可用空間除以外星人寬度的兩倍:

 

number_aliens_x = available_space_x / (2 * alien_width)

 

 

 

 

 

 

 

 

我們將在創建外星人群時使用這些公式。

注意  令人欣慰的是,在程式中執行計算時,一開始你無需確定公式是正確的,而可以嘗試直接運行程式,看看結果是否符合預期。即便是在最糟糕的情況下,也只是螢幕上顯示的外星人太多或太少。你可以根據在螢幕上看到的情況調整計算公式。

13.3.2 創建多行外星人

為創建一行外星人,首先在alien_invasion.py中創建一個名為aliens 的空編組,用於存儲全部外星人,再調用game_functions.py中創建外星人群的函數:

alien_invasion.py

 

  import pygame

  from pygame.sprite import Group

  from settings import Settings

  from ship import Ship

  import game_functions as gf

 

  def run_game():

      --snip--

      # 創建一艘飛船、一個子彈編組和一個外星人編組

      ship = Ship(ai_settings, screen)

      bullets = Group()

     aliens = Group()

 

      # 創建外星人群

     gf.create_fleet(ai_settings, screen, aliens)

 

      # 開始遊戲主迴圈

      while True:

          --snip—

         gf.update_screen(ai_settings, screen, ship, aliens, bullets)

 

  run_game()

 

 

 

 

 

 

 

 

由於我們不再在alien_invasion.py中直接創建外星人,因此無需在這個檔中導入Alien 類。

處創建了一個空編組,用於存儲所有的外星人。接下來,調用稍後將編寫的函數create_fleet() (見),並將ai_settings 、物件screen 和空編組aliens 傳遞給它。然後,修改對update_screen() 的調用,讓它能夠訪問外星人編組(見

我們還需要修改update_screen() 

game_functions.py

 

def update_screen(ai_settings, screen, ship, aliens, bullets):

    --snip--

    ship.blitme()

    aliens.draw(screen)

 

    # 讓最近繪製的螢幕可見

    pygame.display.flip()

 

 

 

 

 

 

 

 

對編組調用draw() 時,Pygame自動繪製編組的每個元素,繪製位置由元素的屬性rect 決定。在這裡,aliens.draw(screen) 在螢幕上繪製編組中的每個外星人。

13.3.3 創建外星人群

現在可以創建外星人群了。下面是新函數create_fleet() ,我們將它放在game_functions.py的末尾。我們還需要導入Alien 類,因此務必在檔game_functions.py開頭添加相應的import 語句:

game_functions.py

 

  --snip--

  from bullet import Bullet

  from alien import Alien

  --snip--

 

  def create_fleet(ai_settings, screen, aliens):

      """創建外星人群"""

      # 創建一個外星人,並計算一行可容納多少個外星人

      # 外星人間距為外星人寬度

     alien = Alien(ai_settings, screen)

     alien_width = alien.rect.width

     available_space_x = ai_settings.screen_width - 2 * alien_width

     number_aliens_x = int(available_space_x / (2 * alien_width))

 

      # 創建第一行外星人

     for alien_number in range(number_aliens_x):

          # 創建一個外星人並將其加入當前行

         alien = Alien(ai_settings, screen)

          alien.x = alien_width + 2 * alien_width * alien_number

          alien.rect.x = alien.x

          aliens.add(alien)

 

 

 

 

 

 

 

 

這些代碼大都在前面詳細介紹過。為放置外星人,我們需要知道外星人的寬度和高度,因此在執行計算前,我們先創建一個外星人(見)。這個外星人不是外星人群的成員,因此沒有將它加入到編組aliens 中。在處,我們從外星人的rect 屬性中獲取外星人寬度,並將這個值存儲到alien_width 中,以免反復訪問屬性rect 。在處,我們計算可用於放置外星人的水準空間,以及其中可容納多少個外星人。

相比於前面介紹的工作,這裡唯一的不同是使用了int() 來確保計算得到的外星人數量為整數(見),因為我們不希望某個外星人只顯示一部分,而且函數range() 也需要一個整數。函數int() 將小數部分丟棄,相當於向下圓整(這大有裨益,因為我們寧願每行都多出一點點空間,也不希望每行的外星人之間過於擁擠)。

接下來,我們編寫了一個迴圈,它從零數到要創建的外星人數(見)。在這個迴圈的主體中,我們創建一個新的外星人,並通過設置x 座標將其加入當前行(見)。將每個外星人都往右推一個外星人的寬度。接下來,我們將外星人寬度乘以2,得到每個外星人佔據的空間(其中包括其右邊的空白區域),再據此計算當前外星人在當前行的位置。最後,我們將每個新創建的外星人都添加到編組aliens 中。

如果你現在運行這個遊戲,將看到第一行外星人,如圖13-3所示。


13-3 第一行外星人

這行外星人在螢幕上稍微偏向了左邊,這實際上是有好處的,因為我們將讓外星人群往右移,觸及螢幕邊緣後稍微往下移,然後往左移,以此類推。就像經典遊戲《太空入侵者》,相比於只往下移,這種移動方式更有趣。我們將讓外形人群不斷這樣移動,直到所有外星人都被擊落或有外星人撞上飛船或抵達螢幕底端。

注意  根據你選擇的螢幕寬度,在你的系統中,第一行外星人的位置可能稍有不同。

13.3.4 重構create_fleet()

倘若我們創建了外星人群,也許應該讓create_fleet() 保持原樣,但鑒於創建外星人的工作還未完成,我們稍微清理一下這個函數。下面是create_fleet() 和兩個新函數,get_number_aliens_x() create_alien() 

game_functions.py

 

def get_number_aliens_x(ai_settings, alien_width):

      """計算每行可容納多少個外星人"""

      available_space_x = ai_settings.screen_width - 2 * alien_width

      number_aliens_x = int(available_space_x / (2 * alien_width))

      return number_aliens_x

 

  def create_alien(ai_settings, screen, aliens, alien_number):

      """創建一個外星人並將其放在當前行"""

      alien = Alien(ai_settings, screen)

     alien_width = alien.rect.width

      alien.x = alien_width + 2 * alien_width * alien_number

      alien.rect.x = alien.x

      aliens.add(alien)

 

  def create_fleet(ai_settings, screen, aliens):

      """創建外星人群"""

      # 創建一個外星人,並計算每行可容納多少個外星人

      alien = Alien(ai_settings, screen)

     number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)

 

      # 創建第一行外星人

      for alien_number in range(number_aliens_x):

         create_alien(ai_settings, screen, aliens, alien_number)

 

 

 

 

 

 

 

 

函數get_number_aliens_x() 的代碼都來自create_fleet() ,且未做任何修改(見)。函數create_alien() 的代碼也都來自create_fleet() ,且未做任何修改,只是使用剛創建的外星人來獲取外星人寬度(見)。在處,我們將計算可用水準空間的代碼替換為對get_number_aliens_x() 的調用,並刪除了引用alien_width 的代碼行,因為現在這是在create_alien() 中處理的。在處,我們調用create_alien() 。通過這樣的重構,添加新行進而創建整群外星人將更容易。

13.3.5 添加行

要創建外星人群,需要計算螢幕可容納多少行,並對創建一行外星人的迴圈重複相應的次數。為計算可容納的行數,我們這樣計算可用垂直空間:將螢幕高度減去第一行外星人的上邊距(外星人高度)、飛船的高度以及最初外星人群與飛船的距離(外星人高度的兩倍):

 

available_space_y = ai_settings.screen_height – 3 * alien_height – ship_height

 

 

 

 

 

 

 

 

這將在飛船上方留出一定的空白區域,給玩家留出射殺外星人的時間。

每行下方都要留出一定的空白區域,並將其設置為外星人的高度。為計算可容納的行數,我們將可用垂直空間除以外星人高度的兩倍(同樣,如果這樣的計算不對,我們馬上就能發現,繼而將間距調整為合理的值)。

 

number_rows = available_height_y / (2 * alien_height)

 

 

 

 

 

 

 

 

知道可容納多少行後,便可重複執行創建一行外星人的代碼:

game_functions.py

 

def get_number_rows(ai_settings, ship_height, alien_height):

      """計算螢幕可容納多少行外星人"""

     available_space_y = (ai_settings.screen_height -

                              (3 * alien_height) - ship_height)

      number_rows = int(available_space_y / (2 * alien_height))

      return number_rows

 

  def create_alien(ai_settings, screen, aliens, alien_number, row_number):

      --snip--

      alien.x = alien_width + 2 * alien_width * alien_number

      alien.rect.x = alien.x

     alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number

      aliens.add(alien)

 

  def create_fleet(ai_settings, screen, ship, aliens):

      --snip--

      number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)

      number_rows = get_number_rows(ai_settings, ship.rect.height,

          alien.rect.height)

 

      # 創建外星人群

     for row_number in range(number_rows):

          for alien_number in range(number_aliens_x):

              create_alien(ai_settings, screen, aliens, alien_number,

                  row_number)

 

 

 

 

 

 

 

 

為計算螢幕可容納多少行外星人,我們在函數get_number_rows() 中實現了前面計算available_space_y number_rows 的公式(見),這個函數與get_number_aliens_x() 類似。計算公式用括弧括起來了,這樣可將代碼分成兩行,以遵循每行不超過79字元的建議(見)。這裡使用了int() ,因為我們不想創建不完整的外星人行。

為創建多行,我們使用兩個嵌套在一起的迴圈:一個外部迴圈和一個內部迴圈(見)。其中的部迴圈創建一行外星人,而外部迴圈從零數到要創建的外星人行數。Python將重複執行創建單行外星人的代碼,重複次數為number_rows 

為嵌套迴圈,我們編寫了一個新的for 迴圈,並縮進了要重複執行的代碼。(在大多數文字編輯器中,縮進代碼塊和取消縮進都很容易,詳情請參閱附錄B。)我們調用create_alien() 時,傳遞了一個表示行號的實參,將每行都沿螢幕依次向下放置。

create_alien() 的定義需要一個用於存儲行號的形參。在create_alien() 中,我們修改外星人的y 座標(見),並在第一行外星人上方留出與外星人等高的空白區域。相鄰外星人行的y 座標相差外星人高度的兩倍,因此我們將外星人高度乘以2,再乘以行號。第一行的行號為0,因此第一行的垂直位置不變,而其他行都沿螢幕依次向下放置。

create_fleet() 的定義中,還新增了一個用於存儲ship 物件的形參,因此在alien_invasion.py中調用create_fleet() 時,需要傳遞實參ship 

alien_invasion.py

 

# 創建外星人群

gf.create_fleet(ai_settings, screen, ship, aliens)

 

 

 

 

 

 

 

 

如果你現在運行這個遊戲,將看到一群外星人,如圖13-4所示。


13-4 整群外星人都現身了

在下一節,我們將讓外星人群動起來!

動手試一試

13-1 星星 :找一幅星星圖像,並在螢幕上顯示一系列整齊排列的星星。

13-2 更逼真的星星 :為讓星星的分佈更逼真,可隨機地放置星星。本書前面說過,可像下面這樣來生成亂數:

 

from random import randint

random_number = randint(-10,10)

 

 

 

 

 

 

 

 

上述代碼返回一個-1010之間的隨機整數。在為完成練習13-1而編寫的程式中,隨機地調整每顆星星的位置。

13.4 讓外星人群移動

下面來讓外星人群在螢幕上向右移動,撞到螢幕邊緣後下移一定的距離,再沿相反的方向移動。我們將不斷地移動所有的外星人,直到所有外星人都被消滅,有外星人撞上飛船,或有外星人抵達螢幕底端。下麵先來讓外星人向右移動。

13.4.1 向右移動外星人

為移動外星人,我們將使用alien.py中的方法update() ,且對外星人群中的每個外星人都調用它。首先,添加一個控制外星人速度的設置:

settings.py

 

    def __init__(self):

        --snip--

        # 外星人設置

        self.alien_speed_factor = 1

 

 

 

 

 

 

 

 

然後,使用這個設置來實現update() 

alien.py

 

      def update(self):

          """向右移動外星人"""

         self.x += self.ai_settings.alien_speed_factor

         self.rect.x = self.x

 

 

 

 

 

 

 

 

每次更新外星人位置時,都將它向右移動,移動量為alien_speed_factor 的值。我們使用屬性self.x 跟蹤每個外星人的準確位置,這個屬性可存儲小數值(見)。然後,我們使用self.x 的值來更新外星人的rect 的位置(見

在主while 迴圈中已調用了更新飛船和子彈的方法,但現在還需更新每個外星人的位置:

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, ship, bullets)

        ship.update()

        gf.update_bullets(bullets)

        gf.update_aliens(aliens)

        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

 

 

 

 

 

 

 

 

我們在更新子彈後再更新外星人的位置,因為稍後要檢查是否有子彈撞到了外星人。

最後,在檔game_functions.py末尾添加新函數update_aliens() 

game_functions.py

 

def update_aliens(aliens):

    """更新外星人群中所有外星人的位置"""

    aliens.update()

 

 

 

 

 

 

 

 

我們對編組aliens 調用方法update() ,這將自動對每個外星人調用方法update() 。如果你現在運行這個遊戲,會看到外星人群向右移,並逐漸在螢幕右邊緣消失。

13.4.2 創建表示外星人移動方向的設置

下麵來創建讓外星人撞到螢幕右邊緣後向下移動、再向左移動的設置。實現這種行為的代碼如下:

settings.py

 

        # 外星人設置

        self.alien_speed_factor = 1

        self.fleet_drop_speed = 10

        # fleet_direction1表示向右移,為-1表示向左移

        self.fleet_direction = 1

 

 

 

 

 

 

 

 

設置fleet_drop_speed 指定了有外星人撞到螢幕邊緣時,外星人群向下移動的速度。將這個速度與水準速度分開是有好處的,這樣你就可以分別調整這兩種速度了。

要實現fleet_direction 設置,可以將其設置為文本值,如'left' 'right' ,但這樣就必須編寫if-elif 語句來檢查外星人群的移動方向。鑒於只有兩個可能的方向,我們使用值1-1來表示它們,並在外星人群改變方向時在這兩個值之間切換。另外,鑒於向右移動時需要增大每個外星人的x 座標,而向左移動時需要減小每個外星人的x 座標,使用數字來表示方向更合理。

13.4.3 檢查外星人是否撞到了螢幕邊緣

現在需要編寫一個方法來檢查是否有外星人撞到了螢幕邊緣,還需修改update() ,以讓每個外星人都沿正確的方向移動:

alien.py

 

      def check_edges(self):

          """如果外星人位於螢幕邊緣,就返回True"""

          screen_rect = self.screen.get_rect()

         if self.rect.right >= screen_rect.right:

              return True

         elif self.rect.left <= 0:

              return True

 

      def update(self):

          """向左或向右移動外星人"""

         self.x += (self.ai_settings.alien_speed_factor *

                          self.ai_settings.fleet_direction)

          self.rect.x = self.x

 

 

 

 

 

 

 

 

我們可對任何外星人調用新方法check_edges() ,看看它是否位於螢幕左邊緣或右邊緣。如果外星人的rect right 屬性大於或等於螢幕的rect right 屬性,就說明外星人位於螢幕右邊緣(見)。如果外星人的rect left 屬性小於或等於0,就說明外星人位於螢幕左邊緣(見

我們修改了方法update() ,將移動量設置為外星人速度和fleet_direction 的乘積,讓外星人向左或向右移。如果fleet_direction 1,就將外星人當前的 x 座標增大alien_speed_factor ,從而將外星人向右移;如果fleet_direction -1,就將外星人當前的 x 座標減去alien_speed_factor ,從而將外星人向左移。

13.4.4 向下移動外星人群並改變移動方向

有外星人到達螢幕邊緣時,需要將整群外星人下移,並改變它們的移動方向。我們需要對game_functions.py做重大修改,因為我們要在這裡檢查是否有外星人到達了左邊緣或右邊緣。為此,我們編寫函數check_fleet_edges() change_fleet_direction() ,並對update_aliens() 進行修改:

game_functions.py

 

  def check_fleet_edges(ai_settings, aliens):

      """有外星人到達邊緣時採取相應的措施"""

     for alien in aliens.sprites():

          if alien.check_edges():

              change_fleet_direction(ai_settings, aliens)

              break

 

  def change_fleet_direction(ai_settings, aliens):

      """將整群外星人下移,並改變它們的方向"""

      for alien in aliens.sprites():

         alien.rect.y += ai_settings.fleet_drop_speed

      ai_settings.fleet_direction *= -1

 

  def update_aliens(ai_settings, aliens):

      """

      檢查是否有外星人位於螢幕邊緣,並更新整群外星人的位置

      """

     check_fleet_edges(ai_settings, aliens)

      aliens.update()

 

 

 

 

 

 

 

 

check_fleet_edges() 中,我們遍歷外星人群,並對其中的每個外星人調用check_edges() (見)。如果check_edges() 返回True ,我們就知道相應的外星人位於螢幕邊緣,需要改變外星人群的方向,因此我們調用change_fleet_direction() 並退出迴圈。在change_fleet_direction() 中,我們遍歷所有外星人,將每個外星人下移fleet_drop_speed 設置的值(見);然後,將fleet_direction 的值修改為其當前值與-1的乘積。

我們修改了函數update_aliens() ,在其中通過調用check_fleet_edges() 來確定是否有外星人位於螢幕邊緣。現在,函數update_aliens() 包含形參ai_settings ,因此我們調用它時指定了與ai_settings 對應的實參:

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, ship, bullets)

        ship.update()

        gf.update_bullets(bullets)

        gf.update_aliens(ai_settings, aliens)

        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

 

 

 

 

 

 

 

 

如果你現在運行這個遊戲,外星人群將在螢幕上來回移動,並在抵達螢幕邊緣後向下移動。現在可以開始射殺外星人,檢查是否有外星人撞到飛船,或抵達了螢幕底端。

動手試一試

13-3 雨滴 :尋找一幅雨滴圖像,並創建一系列整齊排列的雨滴。讓這些雨滴往下落,直到到達螢幕底端後消失。

13-4 連綿細雨 :修改為完成練習13-3而編寫的代碼,使得一行雨滴消失在螢幕底端後,螢幕頂端又出現一行新雨滴,並開始往下落。

13.5 射殺外星人

我們創建了飛船和外星人群,但子彈擊中外星人時,將穿過外星人,因為我們還沒有檢查碰撞。在遊戲程式設計中,碰撞指的是遊戲元素重疊在一起。要讓子彈能夠擊落外星人,我們將使用sprite.groupcollide() 檢測兩個編組的成員之間的碰撞。

13.5.1 檢測子彈與外星人的碰撞

子彈擊中外星人時,我們要馬上知道,以便碰撞發生後讓外星人立即消失。為此,我們將在更新子彈的位置後立即檢測碰撞。

方法sprite.groupcollide() 將每顆子彈的rect 同每個外星人的rect 進行比較,並返回一個字典,其中包含發生了碰撞的子彈和外星人。在這個字典中,每個鍵都是一顆子彈,而相應的值都是被擊中的外星人(第14章實現記分系統時,也會用到這個字典)。

在函數update_bullets() 中,使用下面的代碼來檢查碰撞:

game_functions.py

 

def update_bullets(aliens, bullets):

    """更新子彈的位置,並刪除已消失的子彈"""

    --snip--

    # 檢查是否有子彈擊中了外星人

    # 如果是這樣,就刪除相應的子彈和外星人

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

 

 

 

 

 

 

 

 

新增的這行代碼遍歷編組bullets 中的每顆子彈,再遍歷編組aliens 中的每個外星人。每當有子彈和外星人的rect 重疊時,groupcollide() 就在它返回的字典中添加一個鍵-值對。兩個實參True 告訴Pygame刪除發生碰撞的子彈和外星人。(要類比能夠穿行到螢幕頂端的高能子彈——消滅它擊中的每個外星人,可將第一個布林實參設置為False ,並讓第二個布林實參為True 。這樣被擊中的外星人將消失,但所有的子彈都始終有效,直到抵達螢幕頂端後消失。)

我們調用update_bullets() 時,傳遞了實參aliens 

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, ship, bullets)

        ship.update()

        gf.update_bullets(aliens, bullets)

        gf.update_aliens(ai_settings, aliens)

        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

 

 

 

 

 

 

 

 

如果你此時運行這個遊戲,被擊中的外星人將消失。如圖13-5所示,其中有一部分外星人被擊落。


13-5 可以射殺外星人了

13.5.2 為測試創建大子彈

只需通過運行這個遊戲就可以測試其很多功能,但有些功能在正常情況下測試起來比較煩瑣。例如,要測試代碼能否正確地處理外星人編組為空的情形,需要花很長時間將螢幕上的外星人都擊落。

測試有些功能時,可以修改遊戲的某些設置,以便專注於遊戲的特定方面。例如,可以縮小螢幕以減少需要擊落的外星人數量,也可以提高子彈的速度,以便能夠在單位時間內發射大量子彈。

測試這個遊戲時,我喜歡做的一項修改是增大子彈的尺寸,使其在擊中外星人後依然有效,如圖13-6所示。請嘗試將bullet_width 設置為300,看看將所有外星人都射殺有多快!

類似這樣的修改可提高測試效率,還可能激發出如何賦予玩家更大威力的思想火花。(完成測試後,別忘了將設置恢復正常。)

加入說明文字

13-6 威力更大的子彈讓遊戲的有些方法測試起來更容易

13.5.3 生成新的外星人群

這個遊戲的一個重要特點是外星人無窮無盡,一個外星人群被消滅後,又會出現一群外星人。

要在外星人群被消滅後又顯示一群外星人,首先需要檢查編組aliens 是否為空。如果為空,就調用create_fleet() 。我們將在update_bullets() 中執行這種檢查,因為外星人都是在這裡被消滅的:

game_functions.py

 

  def update_bullets(ai_settings, screen, ship, aliens, bullets):

      --snip--

      # 檢查是否有子彈擊中了外星人

      # 如果是,就刪除相應的子彈和外星人

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

 

     if len(aliens) == 0:

          # 刪除現有的子彈並新建一群外星人

         bullets.empty()

          create_fleet(ai_settings, screen, ship, aliens)

 

 

 

 

 

 

 

 

處,我們檢查編組aliens 是否為空。如果是,就使用方法empty() 刪除編組中餘下的所有精靈,從而刪除現有的所有子彈。我們還調用了create_fleet() ,再次在螢幕上顯示一群外星人。

現在,update_bullets() 的定義包含額外的形參ai_settings screen ship ,因此我們需要更新alien_invasion.py中對update_bullets() 的調用:

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, ship, bullets)

        ship.update()

        gf.update_bullets(ai_settings, screen, ship, aliens, bullets)

        gf.update_aliens(ai_settings, aliens)

        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

 

 

 

 

 

 

 

 

現在,當前外星人群消滅乾淨後,將立刻出現一個新的外星人群。

13.5.4 提高子彈的速度

如果你現在嘗試在這個遊戲中射殺外星人,可能發現子彈的速度比以前慢,這是因為在每次迴圈中,Pygame需要做的工作更多了。為提高子彈的速度,可調整settings.pybullet_speed_factor 的值。例如,如果將這個值增大到3,子彈在螢幕上向上穿行的速度將變得相當快:

settings.py

 

        # 子彈設置

        self.bullet_speed_factor = 3

        self.bullet_width = 3

        --snip--

 

 

 

 

 

 

 

 

這項設置的最佳值取決於你的系統速度,請找出適合你的值吧。

13.5.5 重構update_bullets()

下面來重構update_bullets() ,使其不再完成那麼多工。我們將把處理子彈和外星人碰撞的代碼移到一個獨立的函數中:

game_functions.py

 

def update_bullets(ai_settings, screen, ship, aliens, bullets):

    --snip--

    # 刪除已消失的子彈

    for bullet in bullets.copy():

        if bullet.rect.bottom <= 0:

            bullets.remove(bullet)

 

    check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets)

 

def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets):

    """回應子彈和外星人的碰撞"""

    # 刪除發生碰撞的子彈和外星人

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

 

    if len(aliens) == 0:

        # 刪除現有的所有子彈,並創建一個新的外星人群

        bullets.empty()

        create_fleet(ai_settings, screen, ship, aliens)

 

 

 

 

 

 

 

 

我們創建了一個新函數——check_bullet_alien_collisions() ,以檢測子彈和外星人之間的碰撞,以及在整群外星人都被消滅乾淨時採取相應的措施。這避免了update_bullets() 太長,簡化了後續的開發工作。

動手試一試

13-5 抓球 :創建一個遊戲,在螢幕底端放置一個玩家可左右移動的角色。讓一個球出現在螢幕頂端,且水準位置是隨機的,並讓這個球以固定的速度往下落。如果角色與球發生碰撞(表示將球抓住了),就讓球消失。每當角色抓住球或球因抵達螢幕底端而消失後,都創建一個新球。

13.6 結束遊戲

如果玩家根本不會輸,遊戲還有什麼趣味和挑戰性可言?如果玩家沒能在足夠短的時間內將整群外星人都消滅乾淨,且有外星人撞到了飛船,飛船將被摧毀。與此同時,我們還限制了可供玩家使用的飛船數,而有外星人抵達螢幕底端時,飛船也將被摧毀。玩家用光了飛船後,遊戲便結束。

13.6.1 檢測外星人和飛船碰撞

我們首先檢查外星人和飛船之間的碰撞,以便外星人撞上飛船時我們能夠作出合適的回應。我們在更新每個外星人的位置後立即檢測外星人和飛船之間的碰撞。

game_functions.py

 

  def update_aliens(ai_settings, ship, aliens):

      """

      檢查是否有外星人到達螢幕邊緣

        然後更新所有外星人的位置

      """

      check_fleet_edges(ai_settings, aliens)

      aliens.update()

 

      # 檢測外星人和飛船之間的碰撞

     if pygame.sprite.spritecollideany(ship, aliens):

         print("Ship hit!!!")

 

 

 

 

 

 

 

 

方法spritecollideany() 接受兩個實參:一個精靈和一個編組。它檢查編組是否有成員與精靈發生了碰撞,並在找到與精靈發生了碰撞的成員後就停止遍歷編組。在這裡,它遍歷編組aliens ,並返回它找到的第一個與飛船發生了碰撞的外星人。

如果沒有發生碰撞,spritecollideany() 將返回None ,因此處的if 代碼塊不會執行。如果找到了與飛船發生碰撞的外星人,它就返回這個外星人,因此if 代碼塊將執行:列印“Ship hit!!!”(見)。(有外星人撞到飛船時,需要執行的任務很多:需要刪除餘下的所有外星人和子彈,讓飛船重新居中,以及創建一群新的外星人。編寫完成這些任務的代碼前,需要確定檢測外星人和飛船碰撞的方法是否可行。而為確定這一點,最簡單的方式是編寫一條print 語句。)

現在,我們需要將ship 傳遞給update_aliens() 

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, ship, bullets)

        ship.update()

        gf.update_bullets(ai_settings, screen, ship, aliens, bullets)

        gf.update_aliens(ai_settings, ship, aliens)

        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

 

 

 

 

 

 

 

 

現在如果你運行這個遊戲,則每當有外星人撞到飛船時,終端視窗都將顯示“Ship hit!!!”。測試這項功能時,請將alien_drop_speed 設置為較大的值,如50100,這樣外星人將更快地撞到飛船。

13.6.2 回應外星人和飛船碰撞

現在需要確定外星人與飛船發生碰撞時,該做些什麼。我們不銷毀ship 實例並創建一個新的ship 實例,而是通過跟蹤遊戲的統計資訊來記錄飛船被撞了多少次(跟蹤統計資訊還有助於記分)。

下面來編寫一個用於跟蹤遊戲統計資訊的新類——GameStats ,並將其保存為檔game_stats.py

game_stats.py

 

  class GameStats():

      """跟蹤遊戲的統計資訊"""

 

      def __init__(self, ai_settings):

          """初始化統計資訊"""

          self.ai_settings = ai_settings

         self.reset_stats()

 

      def reset_stats(self):

          """初始化在遊戲運行期間可能變化的統計資訊"""

          self.ships_left = self.ai_settings.ship_limit

 

 

 

 

 

 

 

 

在這個遊戲運行期間,我們只創建一個GameStats 實例,但每當玩家開始新遊戲時,需要重置一些統計資訊。為此,我們在方法reset_stats() 中初始化大部分統計資訊,而不是在__init__() 中直接初始化它們。我們在__init__() 中調用這個方法,這樣創建GameStats 實例時將妥善地設置這些統計資訊(見),同時在玩家開始新遊戲時也能調用reset_stats() 

當前只有一項統計資訊——ships_left ,其值在遊戲運行期間將不斷變化。一開始玩家擁有的飛船數存儲在settings.pyship_limit 中:

settings.py

 

        # 飛船設置

        self.ship_speed_factor = 1.5

        self.ship_limit = 3

 

 

 

 

 

 

 

 

我們還需對alien_invasion.py做些修改,以創建一個GameStats 實例:

alien_invasion.py

 

  --snip--

  from settings import Settings

from game_stats import GameStats

  --snip--

 

  def run_game():

      --snip--

      pygame.display.set_caption("Alien Invasion")

 

      # 創建一個用於存儲遊戲統計資訊的實例

     stats = GameStats(ai_settings)

      --snip--

      # 開始遊戲主迴圈

      while True:

          --snip--

          gf.update_bullets(ai_settings, screen, ship, aliens, bullets)

         gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)

          --snip--

 

 

 

 

 

 

 

 

我們導入了新類GameStats (見),創建了一個名為stats 的實例(見),再調用update_aliens() 並添加了實參stats screen ship (見)。在有外星人撞到飛船時,我們將使用這些實參來跟蹤玩家還有多少艘飛船,以及創建一群新的外星人。

有外星人撞到飛船時,我們將餘下的飛船數減1,創建一群新的外星人,並將飛船重新放置到螢幕底端中央(我們還將讓遊戲暫停一段時間,讓玩家在新外星人群出現前注意到發生了碰撞,並將重新創建外星人群)。

下面將實現這些功能的大部分代碼放到函數ship_hit() 中:

game_functions.py

 

  import sys

from time import sleep

 

  import pygame

  --snip--

 

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

      """回應被外星人撞到的飛船"""

      # ships_left1

     stats.ships_left -= 1

 

      # 清空外星人列表和子彈列表

     aliens.empty()

      bullets.empty()

 

      # 創建一群新的外星人,並將飛船放到螢幕底端中央

     create_fleet(ai_settings, screen, ship, aliens)

      ship.center_ship()

 

      # 暫停

     sleep(0.5)

 

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

      --snip--

      # 檢測外星人和飛船碰撞

      if pygame.sprite.spritecollideany(ship, aliens):

          ship_hit(ai_settings, stats, screen, ship, aliens, bullets)

 

 

 

 

 

 

 

 

我們首先從模組time 中導入了函數sleep() ,以便使用它來讓遊戲暫停(見)。新函數ship_hit() 在飛船被外星人撞到時作出回應。在這個函數內部,將餘下的飛船數減1(見),然後清空編組aliens bullets (見

接下來,我們創建一群新的外星人,並將飛船居中(見),稍後將在Ship 類中添加方法center_ship() 。最後,我們更新所有元素後(但在將修改顯示到螢幕前)暫停,讓玩家知道其飛船被撞到了(見)。螢幕將暫時停止變化,讓玩家能夠看到外星人撞到了飛船。函數sleep() 執行完畢後,將接著執行函數update_screen() ,將新的外星人群繪製到螢幕上。

我們還更新了update_aliens() 的定義,使其包含形參stats screen bullets (見),讓它能夠在調用ship_hit() 時傳遞這些值。

下面是新方法center_ship() ,請將其添加到ship.py的末尾:

ship.py

 

    def center_ship(self):

        """讓飛船在螢幕上居中"""

        self.center = self.screen_rect.centerx

 

 

 

 

 

 

 

 

為讓飛船居中,我們將飛船的屬性center 設置為螢幕中心的x 座標,而該座標是通過屬性screen_rect 獲得的。

注意  我們根本沒有創建多艘飛船,在整個遊戲運行期間,我們都只創建了一個飛船實例,並在該飛船被撞到時將其居中。統計資訊ships_left 讓我們知道飛船是否用完。

請運行這個遊戲,射殺幾個外星人,並讓一個外星人撞到飛船。遊戲暫停後,將出現一群新的外星人,而飛船將在螢幕底端居中。

13.6.3 有外星人到達螢幕底端

如果有外星人到達螢幕底端,我們將像有外星人撞到飛船那樣作出回應。請添加一個執行這項任務的新函數,並將其命名為update_aliens() 

game_functions.py

 

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

      """檢查是否有外星人到達了螢幕底端"""

      screen_rect = screen.get_rect()

      for alien in aliens.sprites():

         if alien.rect.bottom >= screen_rect.bottom:

              # 像飛船被撞到一樣進行處理

              ship_hit(ai_settings, stats, screen, ship, aliens, bullets)

              break

 

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

      --snip--

      # 檢查是否有外星人到達螢幕底端

     check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets)

 

 

 

 

 

 

 

 

函數check_aliens_bottom() 檢查是否有外星人到達了螢幕底端。到達螢幕底端後,外星人的屬性rect.bottom 的值大於或等於螢幕的屬性rect.bottom 的值(見)。如果有外星人到達螢幕底端,我們就調用ship_hit() ;只要檢測到一個外星人到達螢幕底端,就無需檢查其他外星人,因此我們在調用ship_hit() 後退出迴圈。

我們在更新所有外星人的位置並檢測是否有外星人和飛船發生碰撞後調用check_aliens_bottom() (見)。現在,每當有外星人撞到飛船或抵達螢幕底端時,都將出現一群新的外星人。

13.6.4 遊戲結束

現在這個遊戲看起來更完整了,但它永遠都不會結束,只是ships_left 不斷變成更小的負數。下面在GameStats 中添加一個作為標誌的屬性game_active ,以便在玩家的飛船用完後結束遊戲:

game_stats.py

 

    def __init__(self, settings):

        --snip--

        # 遊戲剛啟動時處於活動狀態

        self.game_active = True

 

 

 

 

 

 

 

 

現在在ship_hit() 中添加代碼,在玩家的飛船都用完後將game_active 設置為False 

game_functions.py

 

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

    """回應飛船被外星人撞到"""

    if stats.ships_left > 0:

        # ships_left1

        stats.ships_left -= 1

        --snip--

        #暫停一會兒

        sleep(0.5)

 

    else:

        stats.game_active = False

 

 

 

 

 

 

 

 

ship_hit() 的大部分代碼都沒變。我們將原來的所有代碼都移到了一個if 語句塊中,這條if 語句檢查玩家是否至少還有一艘飛船。如果是這樣,就創建一群新的外星人,暫停一會兒,再接著往下執行。如果玩家沒有飛船了,就將game_active 設置為False 

13.7 確定應運行遊戲的哪些部分

alien_invasion.py中,我們需要確定遊戲的哪些部分在任何情況下都應運行,哪些部分僅在遊戲處於活動狀態時才運行:

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, ship, bullets)

 

        if stats.game_active:

            ship.update()

            gf.update_bullets(ai_settings, screen, ship, aliens, bullets)

            gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)

 

        gf.update_screen(ai_settings, screen, ship, aliens, bullets)

 

 

 

 

 

 

 

 

在主迴圈中,在任何情況下都需要調用check_events() ,即便遊戲處於非活動狀態時亦如此。例如,我們需要知道玩家是否按了Q鍵以退出遊戲,或按一下關閉視窗的按鈕。我們還需要不斷更新螢幕,以便在等待玩家是否選擇開始新遊戲時能夠修改螢幕。其他的函數僅在遊戲處於活動狀態時才需要調用,因為遊戲處於非活動狀態時,我們不用更新遊戲元素的位置。

現在,你運行這個遊戲時,它將在飛船用完後停止不動。

動手試一試

13-6 遊戲結束 :在為完成練習13-5而編寫的代碼中,跟蹤玩家有多少次未將球接著。在未接著球的次數到達三次後,結束遊戲。

13.8 小結

在本章中,你學習了:如何在遊戲中添加大量相同的元素,如創建一群外星人;如何使用嵌套迴圈來創建元素網格,還通過調用每個元素的方法update() 移動了大量的元素;如何控制物件在螢幕上移動的方向,以及如何回應事件,如有外星人到達螢幕邊緣;如何檢測和回應子彈和外星人碰撞以及外星人和飛船碰撞;如何在遊戲中跟蹤統計資訊,以及如何使用標誌game_active 來判斷遊戲是否結束了。

在與這個項目相關的最後一章中,我們將添加一個Play按鈕,讓玩家能夠開始遊戲,以及遊戲結束後再玩。每當玩家消滅一群外星人後,我們都將加快遊戲的節奏,並添加一個記分系統,得到一個極具可玩性的遊戲!

0 留言:

發佈留言