2020年9月26日星期六

014 Python编程 從入門到實踐, 第二部分 專案 第14章 記分

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



第14章    記分

在本章中,我們將結束遊戲《外星人入侵》的開發。我們將添加一個Play按鈕,用於根據需要啟動遊戲以及在遊戲結束後重啟遊戲。我們還將修改這個遊戲,使其在玩家的等級提高時加快節奏,並實現一個記分系統。閱讀本章後,你將掌握足夠多的知識,能夠開始編寫隨玩家等級提高而加大難度以及顯示得分的遊戲。

14.1 添加Play按鈕

在本節中,我們將添加一個Play按鈕,它在遊戲開始前出現,並在遊戲結束後再次出現,讓玩家能夠開始新遊戲。

當前,這個遊戲在玩家運行alien_invasion.py時就開始了。下面讓遊戲一開始處於非活動狀態,並提示玩家按一下Play按鈕來開始遊戲。為此,在game_stats.py中輸入如下代碼:

game_stats.py

 

    def __init__(self, ai_settings):

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

        self.ai_settings = ai_settings

        self.reset_stats()

 

        # 讓遊戲一開始處於非活動狀態

        self.game_active = False

 

    def reset_stats(self):

        --snip--

 

 

 

 

 

 

 

 

現在遊戲一開始將處於非活動狀態,等我們創建Play按鈕後,玩家才能開始遊戲。

14.1.1 創建Button 

由於Pygame沒有內置創建按鈕的方法,我們創建一個Button 類,用於創建帶標籤的實心矩形。你可以在遊戲中使用這些代碼來創建任何按鈕。下面是Button 類的第一部分,請將這個類保存為檔button.py

button.py

 

  import pygame.font

 

  class Button():

 

     def __init__(self, ai_settings, 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)

 

          # 創建按鈕的rect物件,並使其居中

         self.rect = pygame.Rect(0, 0, self.width, self.height)

          self.rect.center = self.screen_rect.center

 

          # 按鈕的標籤只需創建一次

         self.prep_msg(msg)

 

 

 

 

 

 

 

 

首先,我們導入了模組pygame.font ,它讓Pygame能夠將文本渲染到螢幕上。方法__init__() 接受參數self ,物件ai_settings screen ,以及msg ,其中msg 是要在按鈕中顯示的文本(見)。我們設置按鈕的尺寸(見),然後通過設置button_color 讓按鈕的rect 物件為亮綠色,並通過設置text_color 讓文本為白色。

在(見處,我們指定使用什麼字體來渲染文本。實參None Pygame 使用預設字體,而48 指定了文本的字型大小。為讓按鈕在螢幕上居中,我們創建一個表示按鈕的rect 物件(見),並將其center 屬性設置為螢幕的center 屬性。

Pygame通過將你要顯示的字串渲染為圖像來處理文本。在處,我們調用prep_msg() 來處理這樣的渲染。

prep_msg() 的代碼如下:

button.py

 

      def prep_msg(self, msg):

          """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

 

 

 

 

 

 

 

 

方法prep_msg() 接受實參self 以及要渲染為圖像的文本(msg )。調用font.render() 將存儲在msg 中的文本轉換為圖像,然後將該圖像存儲在msg_image 中(見)。方法font.render() 還接受一個布林實參,該實參指定開啟還是關閉反鋸齒功能(反鋸齒讓文本的邊緣更平滑)。餘下的兩個實參分別是文本顏色和背景色。我們啟用了反鋸齒功能,並將文本的背景色設置為按鈕的顏色(如果沒有指定背景色,Pygame將以透明背景的方式渲染文本)。

處,我們讓文本圖像在按鈕上居中:根據文本圖像創建一個rect ,並將其center 屬性設置為按鈕的center 屬性。

最後,我們創建方法draw_button() ,通過調用它可將這個按鈕顯示到螢幕上:

button.py

 

    def draw_button(self):

        # 繪製一個用顏色填充的按鈕,再繪製文本

        self.screen.fill(self.button_color, self.rect)

        self.screen.blit(self.msg_image, self.msg_image_rect)

 

 

 

 

 

 

 

 

我們調用screen.fill() 來繪製表示按鈕的矩形,再調用screen.blit() ,並向它傳遞一幅圖像以及與該圖像相關聯的rect 物件,從而在螢幕上繪製文本圖像。至此,Button 類便創建好了。

14.1.2 在螢幕上繪製按鈕

我們將使用Button 類來創建一個Play按鈕。鑒於只需要一個Play按鈕,我們直接在alien_invasion.py中創建它,如下所示:

alien_invasion.py

 

  --snip--

  from game_stats import GameStats

  from button import Button

  --snip--

 

  def run_game():

      --snip--

      pygame.display.set_caption("Alien Invasion")

 

      # 創建Play按鈕

     play_button = Button(ai_settings, screen, "Play")

      --snip--

 

      # 開始遊戲主迴圈

      while True:

          --snip--

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

              play_button)

 

  run_game()

 

 

 

 

 

 

 

 

我們導入Button 類,並創建一個名為play_button 的實例(見),然後我們將play_button 傳遞給update_screen() ,以便能夠在螢幕更新時顯示按鈕(見

接下來,修改update_screen() ,以便在遊戲處於非活動狀態時顯示Play按鈕:

game_functions.py

 

def update_screen(ai_settings, screen, stats, ship, aliens, bullets,

        play_button):

    """更新螢幕上的圖像,並切換到新螢幕"""

    --snip--

 

    # 如果遊戲處於非活動狀態,就繪製Play按鈕

    if not stats.game_active:

        play_button.draw_button()

 

    # 讓最近繪製的螢幕可見

    pygame.display.flip()

 

 

 

 

 

 

 

 

為讓Play按鈕位於其他所有螢幕元素上面,我們在繪製其他所有遊戲元素後再繪製這個按鈕,然後切換到新螢幕。如果你現在運行這個遊戲,將在螢幕中央看到一個Play按鈕,如圖14-1所示。


14-1 遊戲處於非活動狀態時出現的Play按鈕

14.1.3 開始遊戲

為在玩家按一下Play按鈕時開始新遊戲,需在game_functions.py中添加如下代碼,以監視與這個按鈕相關的滑鼠事件:

game_functions.py

 

  def check_events(ai_settings, screen, stats, play_button, ship, bullets):

      """回應按鍵和滑鼠事件"""

      for event in pygame.event.get():

          if event.type == pygame.QUIT:

              --snip--

         elif event.type == pygame.MOUSEBUTTONDOWN:

             mouse_x, mouse_y = pygame.mouse.get_pos()

             check_play_button(stats, play_button, mouse_x, mouse_y)

 

  def check_play_button(stats, play_button, mouse_x, mouse_y):

      """在玩家按一下Play按鈕時開始新遊戲"""

     if play_button.rect.collidepoint(mouse_x, mouse_y):

          stats.game_active = True

 

 

 

 

 

 

 

 

我們修改了check_events() 的定義,在其中添加了形參stats play_button 。我們將使用stats 來訪問標誌game_active ,並使用play_button 來檢查玩家是否按一下了Play按鈕。

無論玩家按一下螢幕的什麼地方,Pygame都將檢測到一個MOUSEBUTTONDOWN 事件(見),但我們只想讓這個遊戲在玩家用滑鼠按一下Play按鈕時作出回應。為此,我們使用了pygame.mouse.get_pos() ,它返回一個元組,其中包含玩家按一下時滑鼠的x y 座標(見)。我們將這些值傳遞給函數check_play_button() (見),而這個函數使用collidepoint() 檢查滑鼠按一下位置是否在Play按鈕的rect 內(見)。如果是這樣的,我們就將game_active 設置為True ,讓遊戲就此開始!

alien_invasion.py 中調用check_events() ,需要傳遞另外兩個實參——stats play_button 

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, stats, play_button, ship,

            bullets)

        --snip--

 

 

 

 

 

 

 

 

至此,你應該能夠開始這個遊戲了。遊戲結束時,game_active 應為False ,並重新顯示Play按鈕。

14.1.4 重置遊戲

前面編寫的代碼只處理了玩家第一次按一下Play按鈕的情況,而沒有處理遊戲結束的情況,因為沒有重置導致遊戲結束的條件。

為在玩家每次按一下Play按鈕時都重置遊戲,需要重置統計資訊、刪除現有的外星人和子彈、創建一群新的外星人,並讓飛船居中,如下所示:

game_functions.py

 

  def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,

          bullets, mouse_x, mouse_y):

      """在玩家按一下Play按鈕時開始新遊戲"""

      if play_button.rect.collidepoint(mouse_x, mouse_y):

          # 重置遊戲統計資訊

         stats.reset_stats()

          stats.game_active = True

 

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

         aliens.empty()

          bullets.empty()

 

          # 創建一群新的外星人,並讓飛船居中

         create_fleet(ai_settings, screen, ship, aliens)

          ship.center_ship()

 

 

 

 

 

 

 

 

我們更新了check_play_button() 的定義,使其能夠訪問ai_settings stats ship aliens bullets 。為重置在遊戲期間發生了變化的設置以及刷新遊戲的視覺元素,它需要這些物件。

處,我們重置了遊戲統計資訊,給玩家提供了三艘新飛船。接下來,我們將game_active 設置為True (這樣,這個函數的代碼執行完畢後,遊戲就會開始),清空編組aliens bullets (見),創建一群新的外星人,並將飛船居中(見

check_events() 的定義需要修改,調用check_play_button() 的代碼亦如此:

game_functions.py

 

  def check_events(ai_settings, screen, stats, play_button, ship, aliens,

          bullets):

      """回應按鍵和滑鼠事件"""

      for event in pygame.event.get():

          if event.type == pygame.QUIT:

              --snip--

          elif event.type == pygame.MOUSEBUTTONDOWN:

              mouse_x, mouse_y = pygame.mouse.get_pos()

             check_play_button(ai_settings, screen, stats, play_button, ship,

                  aliens, bullets, mouse_x, mouse_y)

 

 

 

 

 

 

 

 

check_events() 的定義需要形參aliens ,以便將它傳遞給check_play_button() 。接下來,我們修改了調用check_play_button() 的代碼,以將合適的實參傳遞給它(見

下面來修改alien_invasion.py中調用check_events() 的代碼,以將實參aliens 傳遞給它:

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, stats, play_button, ship,

            aliens, bullets)

        --snip--

 

 

 

 

 

 

 

 

現在,每當玩家按一下Play按鈕時,這個遊戲都將正確地重置,讓玩家想玩多少次就玩多少次!

14.1.5 將Play按鈕切換到非活動狀態

當前,Play按鈕存在一個問題,那就是即便Play按鈕不可見,玩家按一下其原來所在的區域時,遊戲依然會作出回應。遊戲開始後,如果玩家不小心按一下了Play按鈕原來所處的區域,遊戲將重新開始!

為修復這個問題,可讓遊戲僅在game_active False 時才開始:

game_functions.py

 

  def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,

          bullets, mouse_x, mouse_y):

      """玩家按一下Play按鈕時開始新遊戲"""

     button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

     if button_clicked and not stats.game_active:

          #重置遊戲統計資訊

          --snip--

 

 

 

 

 

 

 

 

標誌button_clicked 的值為True False (見),僅當玩家按一下了Play按鈕 遊戲當前處於非活動狀態時,遊戲才重新開始(見)。為測試這種行為,可開始新遊戲,並不斷地按一下Play按鈕原來所在的區域。如果一切都像預期的那樣工作,按一下Play按鈕原來所處的區域應該沒有任何影響。

14.1.6 隱藏游標

為讓玩家能夠開始遊戲,我們要讓游標可見,但遊戲開始後,游標只會添亂。為修復這種問題,我們在遊戲處於活動狀態時讓游標不可見:

game_functions.py

 

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,

        bullets, mouse_x, mouse_y):

    """在玩家按一下Play按鈕時開始新遊戲"""

    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

    if button_clicked and not stats.game_active:

        # 隱藏游標

        pygame.mouse.set_visible(False)

        --snip--

 

 

 

 

 

 

 

 

通過向set_visible() 傳遞False ,讓Pygame在游標位於遊戲視窗內時將其隱藏起來。

遊戲結束後,我們將重新顯示游標,讓玩家能夠按一下Play按鈕來開始新遊戲。相關的代碼如下:

game_functions.py

 

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

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

    if stats.ships_left > 0:

        --snip--

    else:

        stats.game_active = False

        pygame.mouse.set_visible(True)

 

 

 

 

 

 

 

 

ship_hit() 中,我們在遊戲進入非活動狀態後,立即讓游標可見。關注這樣的細節讓遊戲顯得更專業,也讓玩家能夠專注於玩遊戲而不是費力搞明白使用者介面。

動手試一試

14-1 P開始新遊戲 :鑒於遊戲《外星人入侵》使用鍵盤來控制飛船,最好讓玩家也能夠通過按鍵來開始遊戲。請添加讓玩家在按P時開始遊戲的代碼。也許這樣做會有所幫助:將check_play_button() 的一些代碼提取出來,放到一個名為start_game() 的函數中,並在check_play_button() check_keydown_events() 中調用這個函數。

14-2 射擊練習 :創建一個矩形,它在螢幕右邊緣以固定的速度上下移動。然後,在螢幕左邊緣創建一艘飛船,玩家可上下移動該飛船,並射擊前述矩形目標。添加一個用於開始遊戲的Play按鈕,在玩家三次未擊中目標時結束遊戲,並重新顯示Play按鈕,讓玩家能夠通過按一下該按鈕來重新開始遊戲。

14.2 提高等級

當前,將整群外星人都消滅乾淨後,玩家將提高一個等級,但遊戲的難度並沒有變。下面來增加一點趣味性:每當玩家將螢幕上的外星人都消滅乾淨後,加快遊戲的節奏,讓遊戲玩起來更難。

14.2.1 修改速度設置

我們首先重新組織Settings 類,將遊戲設置劃分成靜態的和動態的兩組。對於隨著遊戲進行而變化的設置,我們還確保它們在開始新遊戲時被重置。settings.py的方法__init__() 如下:

settings.py

 

      def __init__(self):

          """初始化遊戲的靜態設置"""

          # 螢幕設置

          self.screen_width = 1200

          self.screen_height = 800

          self.bg_color = (230, 230, 230)

 

          # 飛船設置

          self.ship_limit = 3

 

          # 子彈設置

          self.bullet_width = 3

          self.bullet_height = 15

          self.bullet_color = 60, 60, 60

          self.bullets_allowed = 3

 

          # 外星人設置

          self.fleet_drop_speed = 10

 

          # 以什麼樣的速度加快遊戲節奏

         self.speedup_scale = 1.1

 

         self.initialize_dynamic_settings()

 

 

 

 

 

 

 

 

我們依然在__init__() 中初始化靜態設置。在處,我們添加了設置speedup_scale ,用於控制遊戲節奏的加快速度:2表示玩家每提高一個等級,遊戲的節奏就翻倍;1表示遊戲節奏始終不變。將其設置為1.1能夠將遊戲節奏提高到夠快,讓遊戲既有難度,又並非不可完成。最後,我們調用initialize_dynamic_settings() ,以初始化隨遊戲進行而變化的屬性(見

initialize_dynamic_settings() 的代碼如下:

settings.py

 

    def initialize_dynamic_settings(self):

        """初始化隨遊戲進行而變化的設置"""

        self.ship_speed_factor = 1.5

        self.bullet_speed_factor = 3

        self.alien_speed_factor = 1

 

        # fleet_direction1表示向右;為-1表示向左

        self.fleet_direction = 1

 

 

 

 

 

 

 

 

這個方法設置了飛船、子彈和外星人的初始速度。隨遊戲的進行,我們將提高這些速度,而每當玩家開始新遊戲時,都將重置這些速度。在這個方法中,我們還設置了fleet_direction ,使得遊戲剛開始時,外星人總是向右移動。每當玩家提高一個等級時,我們都使用increase_speed() 來提高飛船、子彈和外星人的速度:

settings.py

 

    def increase_speed(self):

        """提高速度設置"""

        self.ship_speed_factor *= self.speedup_scale

        self.bullet_speed_factor *= self.speedup_scale

        self.alien_speed_factor *= self.speedup_scale

 

 

 

 

 

 

 

 

為提高這些遊戲元素的速度,我們將每個速度設置都乘以speedup_scale 的值。

check_bullet_alien_collisions() 中,我們在整群外星人都被消滅後調用increase_speed() 來加快遊戲的節奏,再創建一群新的外星人:

game_functions.py

 

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

    --snip--

    if len(aliens) == 0:

        # 刪除現有的子彈,加快遊戲節奏,並創建一群新的外星人

        bullets.empty()

        ai_settings.increase_speed()

        create_fleet(ai_settings, screen, ship, aliens)

 

 

 

 

 

 

 

 

通過修改速度設置ship_speed_factor alien_speed_factor bullet_speed_factor 的值,足以加快整個遊戲的節奏!

14.2.2 重置速度

每當玩家開始新遊戲時,我們都需要將發生了變化的設置重置為初始值,否則新遊戲開始時,速度設置將是前一次遊戲增加了的值:

game_functions.py

 

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,

        bullets, mouse_x, mouse_y):

    """在玩家按一下Play按鈕時開始新遊戲"""

    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

    if button_clicked and not stats.game_active:

        # 重置遊戲設置

        ai_settings.initialize_dynamic_settings()

 

        # 隱藏游標

        pygame.mouse.set_visible(False)

        --snip--

 

 

 

 

 

 

 

 

現在,遊戲《外星人入侵》玩起來更有趣,也更有挑戰性。每當玩家將螢幕上的外星人消滅乾淨後,遊戲都將加快節奏,因此難度會更大些。如果遊戲的難度提高得太快,可降低settings.speedup_scale 的值;如果遊戲的挑戰性不足,可稍微提高這個設置的值。找出這個設置的最佳值,讓難度的提高速度相對合理:一開始的幾群外星人很容易消滅乾淨;接下來的幾群消滅起來有一定難度,但也不是不可能;而要將更靠後的外星人群消滅乾淨幾乎不可能。

動手試一試

14-3 有一定難度的射擊練習 :以你為完成練習14-2而做的工作為基礎,讓標靶的移動速度隨遊戲進行而加快,並在玩家按一下Play按鈕時將其重置為初始值。

14.3 記分

下面來實現一個記分系統,以即時地跟蹤玩家的得分,並顯示最高得分、當前等級和餘下的飛船數。

得分是遊戲的一項統計資訊,因此我們在GameStats 中添加一個score 屬性:

game_stats.py

 

class GameStats():

    --snip--

    def reset_stats(self):

        """初始化隨遊戲進行可能變化的統計資訊"""

        self.ships_left = self.ai_settings.ship_limit

        self.score = 0

 

 

 

 

 

 

 

 

為在每次開始遊戲時都重置得分,我們在reset_stats() 而不是__init__() 中初始化score 

14.3.1 顯示得分

為在螢幕上顯示得分,我們首先創建一個新類Scoreboard 。就當前而言,這個類只顯示當前得分,但後面我們也將使用它來顯示最高得分、等級和餘下的飛船數。下面是這個類的前半部分,它被保存為檔scoreboard.py

scoreboard.py

 

  import pygame.font

 

  class Scoreboard():

      """顯示得分資訊的類"""

 

     def __init__(self, ai_settings, screen, stats):

          """初始化顯示得分涉及的屬性"""

          self.screen = screen

          self.screen_rect = screen.get_rect()

          self.ai_settings = ai_settings

          self.stats = stats

 

          # 顯示得分資訊時使用的字體設置

         self.text_color = (30, 30, 30)

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

 

          # 準備初始得分圖像

         self.prep_score()

 

 

 

 

 

 

 

 

由於Scoreboard 在螢幕上顯示文本,因此我們首先導入模組pygame.font 。接下來,我們在__init__() 中包含形參ai_settings screen stats ,讓它能夠報告我們跟蹤的值(見)。然後,我們設置文本顏色(見)並產生實體一個字體物件(見

為將要顯示的文本轉換為圖像,我們調用了prep_score() (見),其定義如下:

scoreboard.py

 

      def prep_score(self):

        """將得分轉換為一幅渲染的圖像"""

         score_str = str(self.stats.score)

         self.score_image = self.font.render(score_str, True, self.text_color,

              self.ai_settings.bg_color)

 

          # 將得分放在螢幕右上角

         self.score_rect = self.score_image.get_rect()

         self.score_rect.right = self.screen_rect.right - 20

         self.score_rect.top = 20

 

 

 

 

 

 

 

 

prep_score() 中,我們首先將數字值stats.score 轉換為字串(見),再將這個字串傳遞給創建圖像的render() (見)。為在螢幕上清晰地顯示得分,我們向render() 傳遞了螢幕背景色,以及文本顏色。

我們將得分放在螢幕右上角,並在得分增大導致這個數字更寬時讓它向左延伸。為確保得分始終錨定在螢幕右邊,我們創建了一個名為score_rect rect (見),讓其右邊緣與螢幕右邊緣相距20圖元(見),並讓其上邊緣與螢幕上邊緣也相距20圖元(見

最後,我們創建方法show_score() ,用於顯示渲染好的得分圖像:

scoreboard.py

 

    def show_score(self):

        """在螢幕上顯示得分"""

        self.screen.blit(self.score_image, self.score_rect)

 

 

 

 

 

 

 

 

這個方法將得分圖像顯示到螢幕上,並將其放在score_rect 指定的位置。

14.3.2 創建記分牌

為顯示得分,我們在alien_invasion.py中創建一個Scoreboard 實例:

alien_invasion.py

 

  --snip--

  from game_stats import GameStats

  from scoreboard import Scoreboard

  --snip--

  def run_game():

      --snip--

      # 創建存儲遊戲統計資訊的實例,並創建記分牌

      stats = GameStats(ai_settings)

     sb = Scoreboard(ai_settings, screen, stats)

      --snip--

      # 開始遊戲主迴圈

      while True:

          --snip--

         gf.update_screen(ai_settings, screen, stats, sb, ship, aliens,

              bullets, play_button)

 

  run_game()

 

 

 

 

 

 

 

 

我們導入新創建的類Scoreboard ,並在創建實例stats 後創建了一個名為sb Scoreboard 實例(見)。接下來,我們將sb 傳遞給update_screen() ,讓它能夠在螢幕上顯示得分(見

為顯示得分,將update_screen() 修改成下面這樣:

game_functions.py

 

def update_screen(ai_settings, screen, stats, sb, ship, aliens, bullets,

        play_button):

    _--snip--_

    # 顯示得分

    sb.show_score()

 

    # 如果遊戲處於非活動狀態,就顯示Play按鈕

    if not stats.game_active:

        play_button.draw_button()

 

    # 讓最近繪製的螢幕可見

    pygame.display.flip()

 

 

 

 

 

 

 

 

我們在update_screen() 的形參列表中添加了sb ,並在繪製Play按鈕前調用show_score 

如果現在運行這個遊戲,你將在螢幕右上角看到0(當前,我們只想在進一步開發記分系統前確認得分出現在正確的地方)。圖14-2顯示了遊戲開始前的得分。


14-2 得分出現在螢幕右上角

下面來指定每個外星人值多少點!

14.3.3 在外星人被消滅時更新得分

為在螢幕上即時地顯示得分,每當有外星人被擊中時,我們都更新stats.score 的值,再調用prep_score() 更新得分圖像。但在此之前,我們需要指定玩家每擊落一個外星人都將得到多少個點:

settings.py

 

    def initialize_dynamic_settings(self):

        --snip--

 

        # 記分

        self.alien_points = 50

 

 

 

 

 

 

 

 

隨著遊戲的進行,我們將提高每個外星人值的點數。為確保每次開始新遊戲時這個值都會被重置,我們在initialize_dynamic_settings() 中設置它。

check_bullet_alien_collisions() 中,每當有外星人被擊落時,都更新得分:

game_functions.py

 

  def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

          aliens, bullets):

      """回應子彈和外星人發生碰撞"""

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

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

 

      if collisions:

         stats.score += ai_settings.alien_points

          sb.prep_score()

      --snip--

 

 

 

 

 

 

 

 

我們更新check_bullet_alien_collisions() 的定義,在其中包含了形參stats sb ,讓它能夠更新得分和記分牌。有子彈撞到外星人時,Pygame返回一個字典(collisions )。我們檢查這個字典是否存在,如果存在,就將得分加上一個外星人值的點數(見)。接下來,我們調用prep_score() 來創建一幅顯示最新得分的新圖像。

我們需要修改update_bullets() ,確保在函數之間傳遞合適的實參:

game_functions.py

 

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

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

    --snip--

 

    check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

        aliens, bullets)

 

 

 

 

 

 

 

 

update_bullets() 的定義中,需要新增形參stats sb ,而調用check_bullet_alien_collisions() 時,也需要傳遞實參stats sb 

我們還需要修改主while 迴圈中調用update_bullets() 的代碼:

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, stats, play_button, ship,

            aliens, bullets)

        if stats.game_active:

            ship.update()

            gf.update_bullets(ai_settings, screen, stats, sb, ship, aliens,

                bullets)

            --snip--

 

 

 

 

 

 

 

 

調用update_bullets() 時,需要傳遞實參stats sb 

如果你現在運行這個遊戲,得分將不斷增加!

14.3.4 將消滅的每個外星人的點數都計入得分

當前,我們的代碼可能遺漏了一些被消滅的外星人。例如,如果在一次迴圈中有兩顆子彈射中了外星人,或者因子彈更寬而同時擊中了多個外星人,玩家將只能得到一個被消滅的外星人的點數。為修復這種問題,我們來調整檢測子彈和外星人碰撞的方式。

check_bullet_alien_collisions() 中,與外星人碰撞的子彈都是字典collisions 中的一個鍵;而與每顆子彈相關的值都是一個列表,其中包含該子彈撞到的外星人。我們遍歷字典collisions ,確保將消滅的每個外星人的點數都記入得分:

game_functions.py

 

  def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

          aliens, bullets):

      --snip--

      if collisions:

         for aliens in collisions.values():

              stats.score += ai_settings.alien_points * len(aliens)

              sb.prep_score()

      --snip--

 

 

 

 

 

 

 

 

如果字典collisions 存在,我們就遍歷其中的所有值。別忘了,每個值都是一個列表,包含被同一顆子彈擊中的所有外星人。對於每個列表,都將一個外星人的點數乘以其中包含的外星人數量,並將結果加入到當前得分中。為測試這一點,請將子彈寬度改為300圖元,並核實你得到了更寬的子彈擊中的每個外星人的點數,再將子彈寬度恢復到正常值。

14.3.5 提高點數

玩家每提高一個等級,遊戲都變得更難,因此處於較高的等級時,外星人的點數應更高。為實現這種功能,我們添加一些代碼,以在遊戲節奏加快時提高點數:

settings.py

 

  class Settings():

      """存儲遊戲《外星人入侵》的所有設置的類"""

 

      def __init__(self):

          --snip--

          # 加快遊戲節奏的速度

          self.speedup_scale = 1.1

          # 外星人點數的提高速度

         self.score_scale = 1.5

 

          self.initialize_dynamic_settings()

 

      def increase_speed(self):

          """提高速度設置和外星人點數"""

          self.ship_speed_factor *= self.speedup_scale

          self.bullet_speed_factor *= self.speedup_scale

          self.alien_speed_factor *= self.speedup_scale

 

         self.alien_points = int(self.alien_points * self.score_scale)

 

 

 

 

 

 

 

 

我們定義了點數提高的速度,並稱之為score_scale (見)。很小的節奏加快速度(1.1)讓遊戲很快就變得極具挑戰性,但為讓記分發生顯著的變化,需要將點數的提高速度設置為更大的值(1.5)。現在,我們在加快遊戲節奏的同時,提高了每個外星人的點數。為讓點數為整數,我們使用了函數int() 

為顯示外星人的點數,我們在Settings 的方法increase_speed() 中添加了一條print 語句:

settings.py

 

    def increase_speed(self):

        --snip--

        self.alien_points = int(self.alien_points * self.score_scale)

        print(self.alien_points)

 

 

 

 

 

 

 

 

現在每當提高一個等級時,你都會在終端視窗看到新的點數值。

注意  確認點數在不斷增加後,一定要刪除這條print 語句,否則它可能會影響遊戲的性能以及分散玩家的注意力。

14.3.6 將得分圓整

大多數街機風格的射擊遊戲都將得分顯示為10的整數倍,下面讓我們的記分系統遵循這個原則。我們還將設置得分的格式,在大數字中添加用逗號表示的千位分隔符號。我們在Scoreboard 中執行這種修改:

scoreboard.py

 

      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.ai_settings.bg_color)

          --snip--

 

 

 

 

 

 

 

 

函數round() 通常讓小數精確到小數點後多少位,其中小數位數是由第二個實參指定的。然而,如果將第二個實參指定為負數,round() 將圓整到最近的101001000等整數倍。處的代碼讓Pythonstats.score 的值圓整到最近的10的整數倍,並將結果存儲到rounded_score 中。

注意  在Python 2.7中,round() 總是返回一個小數值,因此我們使用int() 來確保報告的得分為整數。如果你使用的是Python 3,可省略對int() 的調用。

處使用了一個字串格式設置指令,它讓Python將數值轉換為字串時在其中插入逗號,例如,輸出1,000,000 而不是1000000 。如果你現在運行這個遊戲,看到的將是10的整數倍的整潔得分,即便得分很高亦如此,如圖14-3所示。


14-3 得分為10的整數倍,並將逗號用作千分位分隔符號

14.3.7 最高得分

每個玩家都想超過遊戲的最高得分記錄。下面來跟蹤並顯示最高得分,給玩家提供要超越的目標。我們將最高得分存儲在GameStats 中:

game_stats.py

 

    def __init__(self, ai_settings):

        --snip--

        # 在任何情況下都不應重置最高得分

        self.high_score = 0

 

 

 

 

 

 

 

 

鑒於在任何情況下都不會重置最高得分,我們在__init__() 中而不是reset_stats() 中初始化high_score 

下面來修改Scoreboard 以顯示最高得分。先來修改方法__init__() 

scoreboard.py

 

      def __init__(self, ai_settings, screen, stats):

          --snip--

          # 準備包含最高得分和當前得分的圖像

          self.prep_score()

         self.prep_high_score()

 

 

 

 

 

 

 

 

最高得分將與當前得分分開顯示,因此我們需要編寫一個新方法prep_high_score() ,用於準備包含最高得分的圖像(見

方法prep_high_score() 的代碼如下:

scoreboard.py

 

      def prep_high_score(self):

          """將最高得分轉換為渲染的圖像"""

         high_score = int(round(self.stats.high_score, -1))

         high_score_str = "{:,}".format(high_score)

         self.high_score_image = self.font.render(high_score_str, True,

              self.text_color, self.ai_settings.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.score_rect.top

 

 

 

 

 

 

 

 

我們將最高得分圓整到最近的10的整數倍(見),並添加了用逗號表示的千分位分隔符號()。然後,我們根據最高得分生成一幅圖像(見),使其水準居中(),並將其top 屬性設置為當前得分圖像的top 屬性(見

現在,方法show_score() 需要在螢幕右上角顯示當前得分,並在螢幕頂部中央顯示最高得分:

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)

 

 

 

 

 

 

 

 

為檢查是否誕生了新的最高得分,我們在game_functions.py中添加一個新函數check_high_score() 

game_functions.py

 

  def check_high_score(stats, sb):

      """檢查是否誕生了新的最高得分"""

     if stats.score > stats.high_score:

          stats.high_score = stats.score

          sb.prep_high_score()

 

 

 

 

 

 

 

 

函數check_high_score() 包含兩個形參:stats sb 。它使用stats 來比較當前得分和最高得分,並在必要時使用sb 來修改最高得分圖像。在處,我們比較當前得分和最高得分,如果當前得分更高,就更新high_score 的值,並調用prep_high_score() 來更新包含最高得分的圖像。

check_bullet_alien_collisions() 中,每當有外星人被消滅,都需要在更新得分後調用check_high_score() 

game_functions.py

 

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

        aliens, bullets):

    --snip--

    if collisions:

        for aliens in collisions.values():

            stats.score += ai_settings.alien_points * len(aliens)

            sb.prep_score()

        check_high_score(stats, sb)

    --snip--

 

 

 

 

 

 

 

 

字典collisions 存在時,我們根據消滅了多少外星人來更新得分,再調用check_high_score() 

第一次玩這款遊戲時,當前得分就是最高得分,因此兩個地方顯示的都是當前得分。但再次開始這個遊戲時,最高得分出現在中央,而當前得分出現在右邊,如圖14-4所示。


14-4 最高得分顯示在螢幕頂部中央

14.3.8 顯示等級

為在遊戲中顯示玩家的等級,首先需要在GameStats 中添加一個表示當前等級的屬性。為確保每次開始新遊戲時都重置等級,在reset_stats() 中初始化它:

game_stats.py

 

    def reset_stats(self):

        """初始化隨遊戲進行可能變化的統計資訊"""

        self.ships_left = self.ai_settings.ship_limit

        self.score = 0

        self.level = 1

 

 

 

 

 

 

 

 

為讓Scoreboard 能夠在當前得分下方顯示當前等級,我們在__init__() 中調用了一個新方法prep_level() 

scoreboard.py

 

    def __init__(self, ai_settings, screen, stats):

        --snip--

 

        # 準備包含得分的初始圖像

        self.prep_score()

        self.prep_high_score()

        self.prep_level()

 

 

 

 

 

 

 

 

prep_level() 的代碼如下:

scoreboard.py

 

      def prep_level(self):

          """將等級轉換為渲染的圖像"""

         self.level_image = self.font.render(str(self.stats.level), True,

                  self.text_color, self.ai_settings.bg_color)

 

          # 將等級放在得分下方

          self.level_rect = self.level_image.get_rect()

         self.level_rect.right = self.score_rect.right

         self.level_rect.top = self.score_rect.bottom + 10

 

 

 

 

 

 

 

 

方法prep_level() 根據存儲在stats.level 中的值創建一幅圖像(見),並將其right 屬性設置為得分的right 屬性(見)。然後,將top 屬性設置為比得分圖像的bottom 屬性大10圖元,以便在得分和等級之間留出一定的空間(見

我們還需要更新show_score() 

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.screen.blit(self.level_image, self.level_rect)

 

 

 

 

 

 

 

 

在這個方法中,添加了一行在螢幕上顯示等級圖像的代碼。

我們在check_bullet_alien_collisions() 中提高等級,並更新等級圖像:

game_functions.py

 

  def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

          aliens, bullets):

      --snip--

      if len(aliens) == 0:

          # 如果整群外星人都被消滅,就提高一個等級

          bullets.empty()

          ai_settings.increase_speed()

 

          # 提高等級

         stats.level += 1

         sb.prep_level()

 

          create_fleet(ai_settings, screen, ship, aliens)

 

 

 

 

 

 

 

 

如果整群外星人都被消滅,我們就將stats.level 的值加1(見),並調用prep_level() ,以確保正確地顯示新等級(見

為確保開始新遊戲時更新記分和等級圖像,在按鈕Play被按一下時觸發重置:

game_functions.py

 

  def check_play_button(ai_settings, screen, stats, sb, play_button, ship,

          aliens, bullets, mouse_x, mouse_y):

      """在玩家按一下Play按鈕時開始新遊戲"""

      button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

      if button_clicked and not stats.game_active:

          --snip--

 

          # 重置遊戲統計資訊

          stats.reset_stats()

          stats.game_active = True

 

          # 重置記分牌圖像

         sb.prep_score()

          sb.prep_high_score()

          sb.prep_level()

 

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

          aliens.empty()

          bullets.empty()

 

          --snip--

 

 

 

 

 

 

 

 

check_play_button() 的定義需要包含物件sb 。為重置記分牌圖像,我們在重置相關遊戲設置後調用prep_score() prep_high_score() prep_level() (見

check_events() 中,現在需要向check_play_button() 傳遞sb ,讓它能夠訪問記分牌物件:

game_functions.py

 

  def check_events(ai_settings, screen, stats, sb, play_button, ship, aliens,

          bullets):

      """回應按鍵和滑鼠事件"""

      for event in pygame.event.get():

          if event.type == pygame.QUIT:

              --snip--

          elif event.type == pygame.MOUSEBUTTONDOWN:

              mouse_x, mouse_y = pygame.mouse.get_pos()

             check_play_button(ai_settings, screen, stats, sb, play_button,

                  ship, aliens, bullets, mouse_x, mouse_y)

 

 

 

 

 

 

 

 

check_events() 的定義需要包含形參sb ,這樣調用check_play_button() 時,才能將sb 作為實參傳遞給它(見

最後,更新alien_invasion.py中調用check_events() 的代碼,也向它傳遞sb 

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        gf.check_events(ai_settings, screen, stats, sb, play_button, ship,

            aliens, bullets)

        --snip--

 

 

 

 

 

 

 

 

現在你可以知道升到多少級了,如圖14-5所示。


14-5 當前等級顯示在當前得分的正下方

注意  在一些經典遊戲中,得分帶標籤,如ScoreHigh ScoreLevel。我們沒有顯示這些標籤,因為開始玩這款遊戲後,每個數字的含義將一目了然。要包含這些標籤,只需在Scoreboard 中調用font.render() 前,將它們添加到得分字串中即可。

14.3.9 顯示餘下的飛船數

最後,我們來顯示玩家還有多少艘飛船,但使用圖形而不是數位。為此,我們在螢幕左上角繪製飛船圖像來指出還餘下多少艘飛船,就像眾多經典的街機遊戲那樣。

首先,需要讓Ship 繼承Sprite ,以便能夠創建飛船編組:

ship.py

 

  import pygame

  from pygame.sprite import Sprite

 

class Ship(Sprite):

 

      def __init__(self, ai_settings, screen):

          """初始化飛船,並設置其起始位置"""

         super(Ship, self).__init__()

          --snip--

 

 

 

 

 

 

 

 

在這裡,我們導入了Sprite ,讓Ship 繼承Sprite (見),並在__init__() 的開頭就調用了super() (見

接下來,需要修改Scoreboard ,在其中創建一個可供顯示的飛船編組。下面是其中的import 語句和方法__init__() 

scoreboard.py

 

import pygame.font

from pygame.sprite import Group

 

from ship import Ship

 

class Scoreboard():

    """報告得分資訊的類"""

 

    def __init__(self, ai_settings, screen, stats):

        --snip--

        self.prep_level()

        self.prep_ships()

       --snip--

 

 

 

 

 

 

 

 

鑒於要創建一個飛船編組,我們導入Group Ship 類。調用prep_level() 後,我們調用了prep_ships() 

prep_ships() 的代碼如下:

scoreboard.py

 

      def prep_ships(self):

          """顯示還餘下多少艘飛船"""

         self.ships = Group()

         for ship_number in range(self.stats.ships_left):

              ship = Ship(self.ai_settings, self.screen)

             ship.rect.x = 10 + ship_number * ship.rect.width

             ship.rect.y = 10

             self.ships.add(ship)

 

 

 

 

 

 

 

 

方法prep_ships() 創建一個空編組self.ships ,用於存儲飛船實例(見)。為填充這個編組,根據玩家還有多少艘飛船運行一個迴圈相應的次數(見)。在這個迴圈中,我們創建一艘新飛船,並設置其x 座標,讓整個飛船編組都位於螢幕左邊,且每艘飛船的左邊距都為10圖元(見)。我們還將y 座標設置為離螢幕上邊緣10圖元,讓所有飛船都與得分圖像對齊(見)。最後,我們將每艘新飛船都添加到編組ships 中(見

現在需要在螢幕上繪製飛船了:

scoreboard.py

 

    def show_score(self):

        --snip--

        self.screen.blit(self.level_image, self.level_rect)

        # 繪製飛船

        self.ships.draw(self.screen)

 

 

 

 

 

 

 

 

為在螢幕上顯示飛船,我們對編組調用了draw() Pygame將繪製每艘飛船。

為在遊戲開始時讓玩家知道他有多少艘飛船,我們在開始新遊戲時調用prep_ships() 。這是在game_functions.pycheck_play_button() 中進行的:

game_functions.py

 

def check_play_button(ai_settings, screen, stats, sb, play_button, ship,

        aliens, bullets, mouse_x, mouse_y):

    """在玩家按一下Play按鈕時開始新遊戲"""

    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

    if button_clicked and not stats.game_active:

        --snip--

        # 重置記分牌圖像

        sb.prep_score()

        sb.prep_high_score()

        sb.prep_level()

        sb.prep_ships()

        --snip--

 

 

 

 

 

 

 

 

我們還在飛船被外星人撞到時調用prep_ships() ,從而在玩家損失一艘飛船時更新飛船圖像:

game_functions.py

 

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

      --snip--

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

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

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

 

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

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

 

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

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

      if stats.ships_left > 0:

          # ships_left1

          stats.ships_left -= 1

 

          # 更新記分牌

         sb.prep_ships()

 

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

          --snip--

 

 

 

 

 

 

 

 

首先,我們在update_aliens() 的定義中添加了形參sb (見)。然後,我們向ship_hit() (見)和check_aliens_bottom() (見)都傳遞了sb ,讓它們都能夠訪問記分牌物件。

接下來,我們更新了ship_hit() 的定義,使其包含形參sb (見)。我們在將ships_left 的值減1後調用了prep_ships() (見),這樣每次損失了飛船時,顯示的飛船數都是正確的。

check_aliens_bottom() 中需要調用ship_hit() ,因此對這個函數進行更新:

game_functions.py

 

def check_aliens_bottom(ai_settings, screen, stats, sb, ship, aliens,

        bullets):

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

    screen_rect = screen.get_rect()

    for alien in aliens.sprites():

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

            # 像飛船被外星人撞到一樣處理

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

            break

 

 

 

 

 

 

 

 

現在,check_aliens_bottom() 包含形參sb ,並在調用ship_hit() 時傳遞了實參sb 

最後,在alien_invasion.py中修改調用update_aliens() 的代碼,向它傳遞實參sb 

alien_invasion.py

 

    # 開始遊戲主迴圈

    while True:

        --snip--

        if stats.game_active:

            ship.update()

            gf.update_bullets(ai_settings, screen, stats, sb, ship, aliens,

                bullets)

            gf.update_aliens(ai_settings, screen, stats, sb, ship, aliens,

                bullets)

            --snip--

 

 

 

 

 

 

 

 

14-6顯示了完整的記分系統,它在螢幕左上角指出了還餘下多少艘飛船。


14-6 遊戲《外星人入侵》的完整記分系統

動手試一試

14-4 歷史最高分 :每當玩家關閉並重新開始遊戲《外星人入侵》時,最高分都將被重置。請修復這個問題,調用sys.exit() 前將最高分寫入文件,並當在GameStats 中初始化最高分時從文件中讀取它。

14-5 重構 :找出執行了多項任務的函數和方法,對它們進行重構,以讓代碼高效而有序。例如,對於check_bullet_alien_collisions() ,將其中在外星人群被消滅乾淨時開始新等級的代碼移到一個名為start_new_level() 的函數中;又比如,對於Scoreboard 的方法__init__() ,將其中調用四個不同方法的代碼移到一個名為prep_images() 的方法中,以縮短方法__init__() 。如果你重構了check_play_button() ,方法prep_images() 也可為check_play_button() start_game() 提供幫助。

注意  重構項目前,請閱讀附錄D,瞭解如果重構時引入了bug,如何將項目恢復到可正確運行的狀態。

14-6 擴展遊戲《外星人入侵》 :想想如何擴展遊戲《外星人入侵》。例如,可讓外星人也能夠向飛船射擊,或者添加盾牌,讓飛船躲到它後面,使得只有從兩邊射來的子彈才能摧毀飛船。另外,還可以使用像pygame.mixer 這樣的模組來添加音效,如爆炸聲和射擊聲。

14.4 小結

在本章中,你學習了如何創建用於開始新遊戲的Play按鈕,如何檢測滑鼠事件,以及在遊戲處於活動狀態時如何隱藏游標。你可以利用學到的知識在遊戲中創建其他按鈕,如用於顯示玩法說明的Help按鈕。你還學習了如何隨遊戲的進行調整其節奏,如何實現記分系統,以及如何以文本和非文本方式顯示資訊。

0 留言:

發佈留言