申明: 本站飛宇網 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.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_direction為1表示向右;為-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.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() 將圓整到最近的10、100、1000等整數倍。❶處的代碼讓Python將stats.score 的值圓整到最近的10的整數倍,並將結果存儲到rounded_score 中。
注意 在Python 2.7中,round() 總是返回一個小數值,因此我們使用int() 來確保報告的得分為整數。如果你使用的是Python 3,可省略對int() 的調用。
❷處使用了一個字串格式設置指令,它讓Python將數值轉換為字串時在其中插入逗號,例如,輸出1,000,000 而不是1000000 。如果你現在運行這個遊戲,看到的將是10的整數倍的整潔得分,即便得分很高亦如此,如圖14-3所示。
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.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所示。
注意 在一些經典遊戲中,得分帶標籤,如Score、High Score和Level。我們沒有顯示這些標籤,因為開始玩這款遊戲後,每個數字的含義將一目了然。要包含這些標籤,只需在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.py的check_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_left減1
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-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 留言:
發佈留言