【文章基於《Python編程-從入門到實踐》】
【項目規划】
“開發大型項目時先做好規划再動手編寫項目很重要”
下面是對《外星人入侵》的規划:
①玩家控制一艘在屏幕底部中央的飛船,可通過箭頭鍵左右移動飛船,還可以使用空格鍵射擊
②一群外星人出現在天空中,他們不斷向下移動
③待玩家將所有外星人消滅后,會出現一群移動速度更快地新外星人
④當有外星人撞到玩家的飛船或到達屏幕底部,玩家就損失一艘飛船
⑤玩家損失三艘飛船后,游戲結束
【安裝Pygame】
pip是一個負責下載並安裝Python包的程序(在Python 3中pip有時也被稱為pip3),可以先查看系統是否已經安裝了pip:
> pip --version# 在Linux和OS X系統中檢查
> python -m pip --version# 在Windows系統中檢查【為何我兩個都行??】
Windows系統的用戶通過http://www.lfd.uci.edu/~gohlke/pythonlibs/#pygame,查找到對應Python版本、電腦操作系統類型、電腦位數的文件進行下載
【開始游戲項目】
【創建游戲窗口】
import sys import pygame def run_game(): # 初始化游戲並創建一個屏幕對象 pygame.init() screen = pygame.display.set_mode((1200, 600)) pygame.display.set_caption('Alien Invasion') # 開始游戲主循環 while True: # 監視屏幕和鼠標事件 for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() # 讓最近繪制的屏幕可見 pygame.display.flip() run_game()
以上為創建一個新的空游戲窗口的代碼,我們來逐個分析:
① pygame.init( )
② screen = pygame.display.set_mode((1200 , 600))
③ pygame.display.set_caption('Alien Invasion')
要創建一個游戲窗口,首先要考慮窗口的大小以及窗口的名稱(即游戲名稱)
①處的pygame.init( )初始化背景設置,讓Pygame能夠正常運行;
②處中,display為pygame的控制窗口和屏幕顯示的模塊,執行display模塊的set_mode方法:pygame.display.set_code( )。向該方法傳入一個tuple(1200 , 600),表示創建一個寬1200像素、高600像素的游戲窗口,並指向screen這一變量
除了tuple外,還可以向set_mode( )傳遞一個list等
③處同樣調用pygame的display模塊,而set_caption( )方法是設置窗口名稱
while True:
游戲需要實現“重新游戲”的功能,無論是通關還是失敗,因此在Pygame中創建好一個游戲窗口后,直接進入游戲的主循環;在主循環內部通過其他方法退出循環
for event in pygame.event.get( ):
if event.type == pygame.QUIT:
sys.exit( )
[事件循環]:事件是用戶玩游戲時執行的操作,如按鍵和鼠標移動。由於事件隨時可能產生,而且量也會很大,Pygame通過pygame.event.get( )將一系列事件放在一個隊列里,逐個處理
上述代碼首先遍歷pygame.event整個隊列,並用if語句來檢查,如果其中有一個事件的類型為pygame.QUIT,就調用sys模塊中的exit( )方法,退出窗口,實現“關閉”游戲的效果
一般情況下,類型為pygame.QUIT的事件都是用戶鼠標點擊“退出游戲”按鈕
pygame.display.flip( )
[管理屏幕更新]:上述代碼命令Pygame讓最近繪制的屏幕可見。
在這里,每次執行while循環時都會繪制一個空屏幕,並擦去舊屏幕使得只有新屏幕可見。當我們移動游戲元素時,pygame.display.flip( )將不斷更新屏幕以顯示元素的位置,從而營造平滑移動的效果。因此要修改當前屏幕,先得完成所有的修改,再通過flip( )顯示更新
綜上,創建窗口有以下步驟:【窗口設置】→【主循環】→【檢測事件】→【更新屏幕】
【RGB】
RGB色彩模式是工業界的一種顏色標准,是通過對紅(R)、綠(G)、藍(B)三個顏色通道的變化以及它們相互之間的疊加來得到各式各樣的顏色的,RGB即是代表紅、綠、藍三個通道的顏色,這個標准幾乎包括了人類視力所能感知的所有顏色,是目前運用最廣的顏色系統之一。
通過形如(153,153,255)的形式確定紅(R)、綠(G)、藍(B)分別的強度,可以混合出幾乎所有顏色(256*256*256 = 16777216≈1600萬種)
默認創建的游戲窗口都是黑色的,太乏味了,可以將其設置為另一種顏色:
bg_color = (153 , 153 , 255)
由於窗口顏色的設置為構建窗口的一部分,所以應該將上面的代碼置於while主循環之前,就如同pygame.display.set_mode( )和pygame.display.set_caption( )一樣
然而bg_color = (153 , 153 , 255)只是定義了一個名為bg_color的tuple,還未發揮作用。
我們之前提及過,動畫的平滑移動效果是通過pygame.display.flip( )不斷更新屏幕造就的,所以我們在主循環中添加代碼:
screen.fill(bg_color)
screen指向創建出來的窗口,通過fill( )方法對整個屏幕進行顏色填充。這樣,每次循環時都會重繪屏幕,達到更換屏幕顏色的效果。
【創建設置類】
當游戲項目增大時,要修改游戲的外觀等設置,如果一一去查找分布在文件不同位置的設置,浪費時間;因此可以定義一個Settings類,里面儲存了《外星人入侵》的所有設置:
class Settings(object): '''儲存《外星人入侵》的所有設置''' def __init__(self): self.screen_width = 1200 self.screen_height = 600 self.bg_color = (153, 153, 255)
隨后更改原游戲代碼:
import sys import pygame from ... import Settings def run_game(): pygame.init() ai_settings = Settings() # 不要遺漏了創建實例,以及下面set_mode()接收一個tuple screen = pygame.display.set_mode((ai_settings.screen_width, ai_settings.screen_height)) pygame.display.set_caption('Alien Invasion') While True: for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() screen.fill(ai_settings.bg_color) pygame.display.filp()
當要修改設置時,只需直接修改Settings中的值即可
【補充】fill( )和set_mode( )一樣,都是接受一個tuple
【添加飛船圖像】
首先要選定飛船圖像,選定時務必注意許可,http://pixabay.com/網站提供的圖形都無需許可,大可放心使用並對其進行修改
在游戲中幾乎可以使用任何類型的圖像文件,但最好使用位圖(.bmp),Python默認加載位圖。“雖然可配置Python以使用其他文件類型,但有些文件類型要求在計算機上安裝相應的圖像庫”可通過Photoshop、GIMP和Paint等工具將.jpg、.png或.gif格式的圖像轉換為位圖
將圖像ship.bmp添加到文件夾alien_invasion的子文件夾images中,加載圖像后,可以使用pygame的blit( )方法繪制它
【創建Ship類】
對於pygame而言,將一張飛船圖像加載到創建的屏幕中是十分簡單的,難點在於如何確定飛船的位置。一般的.bmp圖像沒有什么位置之分,因此我們將圖像矩形化,也就是讓Pygame像處理矩形一樣處理游戲元素。由於矩形為簡單的幾何形狀,Pygame處理其是高效的。
在這個游戲中,每個游戲元素都是一個surface,可通過get_rect( )方法來獲取對應圖像的rect對象
import pygame class Ship(object): def __init__(self, screen): self.screen = screen '''加載圖像並獲取其外接矩形''' self.image = pygame.image.load('images/ship.bmp') self.rect = self.image.get_rect() self.screen_rect = self.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)
下面還是來逐個分析:
self.screen = screen
事實上,創建Ship實例時要傳入的實參screen就是之前通過set_mode( )創建的屏幕窗口,這里只是將screen與實例綁定,待會有用
self.image = pygame.image.load('images/ship.bmp')
self.rect = self.image.get_rect( )
self.screen_rect = self.screen.get_rect( )
首先使用Pygame內置image模塊的load( )方法,通過相對搜索路徑,load( )返回一個表示飛船的surface,並將這個surface儲存到self.image中,待使用
之前有提及,為了將飛船安放在屏幕底部中央,我們要獲取飛船圖像的外接矩形,一旦獲取了飛船圖像的外接矩形(即rect對象),我們就可以設置rect對象的橫中心線(centerx)、底部(bottom)等屬性了
而上述的后面兩行代碼分別獲取飛船圖像、屏幕的外接矩形(別忘了屏幕也是surface)
對於【rect對象】,這里拓展一下:獲取某個圖像的外接矩形(rect對象)后,可以
①查看rect對象的size、width、height等參數
②可以設置橫中心線(centerx)、縱中心線(centery)使游戲元素居中;設置屬性top、bottom、left、right使游戲元素與屏幕邊緣對齊
③直接為x、y賦值以達到確定位置的目的(x、y表示rect對象的中心坐標)
順便一提,在Pygame中,原點(0 , 0)位於屏幕左上角,x值為橫坐標向右、y值為縱坐標向下
self.rect.centerx = self.screen_rect.centerx
self.rect.bottom = self.screen_rect.bottom
這兩行代碼就是真正地確定了飛船的初始位置:令飛船的rect對象的橫中心線(centerx)與屏幕的rect對象的橫中心線重合(或相等)、令飛船的rect對象的底部(bottom)與屏幕的rect對象的底部重合(或相等),要知道centerx、bottom等都是rect對象的屬性
def blitme(self):
self.screen.blit(self.image , self.rect)
【blit( )】
百度翻譯為“位塊傳輸”,“將一個平面的一部分或全部圖象整塊從這個平面復制到另一個平面”
在Pygame中,方法blit( )由屏幕調用,接收兩個參數:一個是圖像,另一個是放置該圖像的位置;之前的self.rect.centerx = ... 、self.rect.bottom = ...都是為了設置該位置
提前加載飛船圖像、設置圖像位置后,在Ship類外面調用blitme( )就會繪制在特定位置的飛船
【在屏幕上繪制】
創建Ship類后,我們需要將其應用到run_game( )函數中,注意以下兩點:
①在主循環之前創建Ship的實例,以免每次循環時都創建一艘飛船:ship = Ship(screen)
②確保飛船圖像出現在背景前面,代碼行ship.blitme( )需出現在screen.fill(bg_color)之后
【重構:模塊game_functions】
在大型項目中,往往需要在添加新代碼前重構既有代碼,旨在簡化既有代碼結構,使其更擁有擴展
在《外星人入侵》的游戲代碼中,除卻在運行游戲的函數run_game( )之外的Settings類、Ship( )類可以儲存在其他模塊(.py文件)中,需要時再導入外,我們還可以創建一個game_functions( )的新模塊,用於儲存大量讓游戲《外星人入侵》運行的代碼
import sys import pygame def check_events(): for event = pygame.event.get(): if event.type == pygame.QUIT: sys.exit() def upgrade_screen(ai_settings, screen, ship): screen.fill(ai_settings.bg_color) ship.blitme() pygame.display.flip()
將上述代碼儲存到game_functions.py文件中,在原alien_invasion.py文件中導入該模塊:import game_functions as gf,在需要的地方之間調用函數:
while True: gf.check_events() gf.upgrade_screen(ai_settings, screen, ship)
你會發覺,sys模塊只是用在檢查事件中,當check_events( )函數在game_functions模塊中提前導入了sys模塊,那么,在alien_invasion模塊中就無需再導入sys模塊了!
這樣,程序員在檢查代碼時就能夠從更高級的層面去查看代碼
【響應按鍵(單擊)】
用戶按下鍵盤時,Pygame會在屬性event中檢測到,因此按鍵屬於檢測事件,對於按鍵的相關代碼理應放在check_events( )函數中:
def check_events(ship): for event in pygame.event.get(): --snip-- elif event.type == pygame.KEYDOWN: if event.key == pygame.K_RIGHT: ship.rect.centerx += 1
Pygame不斷監視事件,當判斷事件的類型為「鍵盤按鍵」(即type == pygame.KEYDOWN)時,若再判斷出按下的是右箭頭鍵,則增大飛船的rect.centerx值。每次按右箭頭鍵一次,飛船向右移動1像素
此外,由於要控制飛船,需修改函數check_events( )的傳入參數,添加ship;在run_game( )調用時也應當添加相應的實參
【響應按鍵(持續)】
單擊鍵盤造成的移動可以被Pygame檢查到並進行簡單的圖像位移,但當持續移動時,必定涉及到while循環;若在check_events( )中更改ship.rect.centerx值,由於涉及到循環,而Pygame的運行又是不斷在執行主循環,因此當兩個循環碰撞時,會引發異常。
要想避免出現兩個循環,可以將操控持續移動的代碼剝離出循環性質(比如更改while為if),再將這段代碼的觸發置於主循環中。這樣,當主循環不斷執行時,持續的位移也可以實現。
為了在主循環中操作持續移動,我們定義一個update( )函數,用於觸發;再在check_events( )函數中利用moving_right之類的標志,響應鍵盤。
class Ship(object): def __init__(self, screen): --snip-- self.moving_right = False def update(self): if self.moving_right == True: self.rect.centerx += 1
同時修改check_events( )函數:
def check_events(ship): --snip-- elif event.type == pygame.KEYDOWN: if event.key == pygame.K_RIGHT: ship.moving_right = True elif event.type == pygame.KEYUP: if event.key == pygame.K_RIGHT: ship.moving_right = False
通過更改后的check_events( )函數檢查鍵盤來更改moving_right屬性的值,再經update( )方法執行;最后一步就是將update( )方法置於主循環中:
while True: gf.check_events(ship) ship.update() gf.update_screen(ai_settings, screen, ship)
同理可實現向左移動
事實上,也可嘗試將ship.update( )整合進gf.update_screen( )中
【調整速度】
每次執行while循環時我們設定飛船移動1像素,但當要往后增大游戲挑戰難度時,我們需要調高飛船速度。通過將飛船的速度設置為浮點數(如1.5),我們可以更細致地控制飛船。
但是rect對象的centerx等屬性只能儲存整數,若直接賦值浮點數,rect.centerx只會取整數部分,因此我們設定一個中間值center:
'''設置放入Settings類中''' self.ship_speed_factor = 1.5 '''更改Ship類''' def __init__(self, screen, ai_settings): --snip-- self.center = float(self.rect.centerx)# 注意這行代碼必須放到定義了centerx之后 def update(self): --snip-- if self.moving_left:self.center -= ai_settings.ship_speed_factor if self.moving_right:self.center += ai_settings.ship_speed_factor self.rect.centerx = self.center
定義的center屬性類型為float,可以儲存浮點數;盡管self.rect.centerx = self.center還是只會儲存center的整數部分,但由於center的增加速率由原來的1.0變成了1.5,rect.centerx想要達到下一個整數值的速率也間接增大了,也就實現了加速的目的
【不妨添加飛船上下移動的代碼,使游戲更加有趣】
【限制活動范圍】
我們不想讓飛船左右移動超出創建的屏幕范圍,可以修改update( )方法:
def update(self): if self.moving_right and self.rect.right < self.screen_rect.right:# 不是 <1200,邏輯嚴謹 self.center += self.ai_settings.ship_speed_factor if self.moving_left and self.rect.left > 0: self.center -= self.ai_settings.ship_speed_factor
這樣,只有飛船在限定范圍才會發生“移動”
當還未學習這種方法時,我copy了網上的一種方法,雖然不好,但貼出來以作警示:
if self.rect.left < 0: self.rect.left = 0 elif self.rect.right > 1200: self.rect.right = 1200
不同於“在限定范圍才會發生移動”不同,這是“若超出限定范圍立刻回歸”,不太好
【重構check_event( )】
由於check_event( )函數是檢測事件的,隨着游戲開發的進行,它將會越來越長。因此我們先將處理KEYDOWN事件和KEYUP事件剝離出來,分開儲存在game_functions.py文件中,但仍與check_events( )函數有關聯
【注意:重構時務必檢查傳入函數的參數】
--snip--
重構check_event( )函數后,響應按鍵時要稍微注意連續使用if和使用if ... elif ...的區別:
[連續使用if]表明各個按鍵之間是獨立的,按下→鍵飛船向右移動,如果這時不松開→鍵直接按下←鍵,pygame默認響應最新的事件,飛船是會向左移動的,哪怕你→鍵還按着
[使用if ... elif ...]的話,當按下→鍵后再按下←鍵,pygame對按鍵的響應還停留在屬於→鍵的if(或elif)中,只要這時不松開→鍵,pygame就無法響應其它鍵
【設置子彈】
關於子彈,我們首先設置單個子彈的相關屬性,並存儲到Settings類中:
self.bullet_speed_factor = 1 self.bullet_width = 3 self.bullet_height = 15 self.bullet_color = 15, 15, 15
隨后,我們就像定義飛船那樣,再定義一個Bullet類,需要注意的是這個Bullet類繼承自pygame的sprite模塊的Sprite類。
Sprite,/spraɪt/,精靈,可以看作是一種可以在屏幕上移動的圖像對象,能與其它圖像對象交互,可以營造出“碰撞”的效果。bullet在后面的代碼會觸碰“外星人”,因此首先將其設置為Sprite子類
from pygame.sprite import Sprite class Bullet(Sprite): def __init__(self, ai_settings, screen, ship): super().__init__() self.screen = screen self.rect = pygame.Rect(0, 0, ai_settings.bullet_width, ai_settings.bullet_height) self.rect.centerx = ship.rect.centerx self.rect.top = ship.rect.top self.y = float(self.rect.y) self.color = ai_settings.bullet_color self.speed = ai_settings.bullet_speed_factor 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)
下面還是慢慢分析吧...
super( ).__init__( )
但凡是繼承自Sprite的類都需將Sprite的屬性繼承並初始化,【詳細情況未知?】
self.screen = screen
這一行代碼反倒是我疑惑最多的地方,在Bullet類中只是將screen初始化了,並沒有初始化ai_settings和ship。我在后面額外將ai_settings和ship也初始化綁定到self上了,並沒有影響游戲的運行。
我的理解是,在最后的pygame.draw.rect( ... )中對screen進行了操作,就得綁定到self上;而ai_settings和ship並沒有進行操作,只是單純地“借用”了兩者的值。
在編程能力還不熟練的時候,可以將所有的形參都初始化,往后再慢慢學習
self.rect = pygame.Rect(0 , 0 , ai_settings.bullet_width , ai_settings.bullet_height)
self.rect.centerx = ship.rect.centerx
self.rect.top = ship.rect.top
由於子彈並非基於圖像,所以我們得使用pygame的Rect類(注意Rect是一個class)從空白創建一個矩形。
創建子彈矩形時傳入的參數依次為:矩形左上角的x坐標,矩形左上角的y坐標,所創建矩形的寬度,所創建矩形的高度;將得到的矩形圖像儲存在self.rect中。
我們首先將該圖像的大小設置,位置的話隨便,之后根據子彈從飛船射出,設置子彈與飛船的centerx值和top值相同。這一步可以回憶一下設置Ship類時self.rect.centerx = self.screen_rect.centerx以及self.rect.bottom = self.screen_rect.bottom
self.y = float(self.rect.y)
這里的代碼參考更改飛船速度時的self.center = float(self.rect.centerx),都是為了能夠更精細地控制子彈的速度。體現這一點的就在update( )方法中:
self.color = ai_settings.bullet_color
def update(self):
self.y -= self.speed
self.rect.y = self.y
這里不妨比對一下Ship類的update( )方法,都是為了實現圖像的平滑移動
self.speed = ai_settings.bullet_speed_factor
def draw_bullet(self):
pygame.draw.rect(self.screen , self.color , self.rect)
同樣地,這里也可以對比Ship的blitme( )方法。由於飛船是基於圖像的,所以我們通過讓屏幕surface調用blit( )方法:self.screen.blit(self.image , self.rect),傳入形參「圖像」和「圖像位置」來將飛船顯示到屏幕上。
而子彈不基於圖像,是通過pygame.Rect( )創建的矩形,因此將子彈顯示到屏幕上是通過pygame.draw.rect( )方法,該方法的實際參數為:pygame.draw.rect(surface , color , rect , width = 0),在上述的代碼中,由於要傳入color,將其綁定到self后,基於與定義blitme( )同樣的目的定義draw_bullet( )
隨后我們引入【Group】這個概念
我們知道飛船能夠形成平滑移動的效果是依靠update( )方法,不斷更改self.rect.centerx的值實現的;而讓子彈往上運動也一樣,通過update( )不斷更改self.y。而為了更好地管理屏幕中可能會出現的所有子彈,我們想定義一個組:
bullets = pygame.sprite.Group()
這個Group也是sprite模塊中的一個類,上面代碼就是創建一個Group實例。Group類似列表list,它作用是為里面的所有子彈執行相同的操作,比如update( )
接下來是響應空格鍵,玩家按下空格鍵,子彈將從飛船頂端射出,並持續向上運行;下面代碼添加到game_functions模塊的check_keydown_events( )函數中:
elif event.key == pygame.K_SPACE: new_bullet = Bullet(ai_settings, screen, ship) bullets.add(new_bullet)
當Pygame檢測到玩家按下空格鍵后,首先創建一個Bullet類的實例,隨后立即將這個實例儲存到Group中。
再然后,由於玩家按下空格鍵產生了子彈,必須讓子彈飛,在函數update_screen( )中將子彈添加到屏幕上:
def update_screen(): --snip-- for bullet in bullets.sprites(): bullet.draw_bullet()
調用bullets組的sprites( )方法,可以返回一個包含儲存在內的所有精靈的列表,並為每個精靈執行draw_bullet( )方法,使其顯示在屏幕上
屏幕上顯示的子彈會出現在飛船頂端,模擬為飛船射出,隨后得讓子彈向上運動,於是在主循環中添加下面代碼:
while 1: gf.check_events(...) ship.update() bullets.update() gf.update_screen(...)
這樣就實現了飛船發射子彈的效果
你得注意,對bullets組直接調用了update( )方法,而調用draw_bullet( )方法時卻是通過for循環為其中的每個子彈調用draw_bullets( )
值得一提的是,類似ship.blitme( )、bullet.draw_bullet( )這樣,將圖像顯示在surface上的代碼通常是包含在game_fucntions模塊的update_screen( )方法中的,當然你可以將這兩個方法都從update_screen( )中提取出來,但必須放在screen.fill( )和pygame.display.flip( )之間。放到上面了,screen.fill( )為窗口填充顏色會覆蓋飛船和子彈、放到下面了,每次主循環都錯過了pygame.display.filp( )的更新窗口,到下一循環又被screen.fill( )干掉了...
除了將在surface上顯示圖像的方法放到update_screen( )方法中外,諸如ship.update( )、bullets.update( )等,為了實現營造圖像平滑移動效果的update方法,都應該顯式地置於主循環中
將ship.blitme( )等方法打包在update_screen( )方法中是有好處的,類似ship.update( )、bullets.update( )都是在修改相關的數據,待全部修改完成后,再一次性通過update_screen( )顯示
【刪除已消失的子彈】
從之前不設置飛船的邊界,飛船會無限地跑出游戲窗口之外來看,子彈射出到從窗口上邊界消失,實際上依然存在,它們的y坐標變成負數,且越來越小。這些窗口外“看不見”的子彈將繼續消耗內存和處理能力,因此我們需要刪除它們。
while 1: --snip-- for bullet in bullets.copy(): if bullet.rect.bottom <= 0: bullets.remove(bullet) # print(len(bullets)) gf.update_screen(ai_settings, screen, ship, bullets)
注意那個copy( )方法,在for循環中,不應從要循環的列表或編組中刪除元素,所以我們通過copy( )方法創建一個bullets組的副本。
遍歷bullets的副本,檢查其中每個bullet是否超過窗口上方(rect.bottom <= 0),若是,則將該bullet通過remove( )從bullets組中移除;還可敲上print(len(bullets))的代碼,從終端窗口中可以看到游戲中子彈的數量,檢驗已消失的子彈確實被刪除了
【番外(什么鬼?)】
關於for循環中不應從要循環的隊列中刪除元素,我們可以舉個簡單的例子:遍歷一個2到10的list,剔除其中的合數,保留質數
L = list(range(2, 11)) for x in L: for i in range(2, x): if x % i == 0: L.remove(x) break
結果卻顯示L為[2, 3, 5, 7, 9],其中的元素{9}沒有被剔除;但如果你遍歷的不是L本身,而是L的副本,比如for x in L[:]:,結果會是讓人滿意的[2, 3, 5, 7]
為什么會這樣?我認為,for循環是依據下標遍歷的,從下標0開始一直到下標len(list/tuple)。上面的例子有一段特殊的序列:[8, 9, 10],難得的連着三個都為合數。當for依據下標遍歷到{8}時,假設這時的下標為n,{8}符合remove的條件,{8}被剔除;隨后由於{8}被剔除,后面的元素({9}和{10})往前補位,{9}就來到了之前{8}所對應的下標n的位置。for認為下標n已經遍歷過了,這時應該遍歷到n+1的位置,於是就跳過了{9}!直接檢查{10}了
也就是說,{3}、{5}、{7}被保留並不是因為是質數,而是for循環中壓根就沒對它們進行篩選,直接忽略了!所以哪怕{3}、{5}、{7}都被替代為合數,上述代碼依舊不會將它們篩選出。所以這個篩選質數的算法有大bug
若遍歷的是L[:],在for循環中即使remove掉L中的元素,L[:]依舊不會受到任何影響,因為L[:]只是臨時創建的L的一個副本,連指向都沒有的副本。
【重構update_bullets( )】
為了盡量簡化while循環中的代碼,我們將下列代碼分離:
bullets.update() for bullet in bullets.copy(): if bullet.rect.bottom <= 0: bullets.remove(bullet)
將上面的代碼封裝在game_functions模塊的update_bullets(bullets)函數中
【self】
在編寫《外星人入侵》的游戲代碼中有留意到,傳入的參數未必都需要綁定到self上,我目測大致有以下情況:
①賦值式(=)時,若參數在右側,無需綁定self,因為只是單純地引用參數的內容;如果是比較式(>)、(<=),則必須要綁定self
②被內部方法再次調用時(即再次作為參數),必須綁定self
但要想判斷某個參數在代碼中的情況,還得先編寫代碼,再返回前面補齊self.,暫時先將傳入的全部參數都綁定到self上
【創建外星人】
創建外星人的類與Ship類並沒有什么不同:
class Alien(pygame.sprite.Sprite): def __init__(self, ai_settings, screen): super().__init__() self.ai_settings = ai_settings self.screen = screen self.image = pygame.image.load('images/alien.bmp') self.rect = self.image.get_rect() self.screen_rect = self.screen.get_rect() self.rect.x = self.rect.width self.rect.y = self.rect.height def blitme(self): self.screen.blit(self.image, self.rect)
上述代碼中,我們暫時定義Alien的位置在窗口左上角附近,但還沒有編寫Alien的update( )方法。
然后,與Ship類似,將Alien的blitme( )方法放在update_screen( )函數中,細節略
【創建一行外星人】
首先創建一個Group:
aliens = pygame.sprite.Group()
在這個Group中,我們儲存的外星人每一個的位置都不同,因此要分別定義它們的橫坐標;我們在game_functions模塊中定義:
def creat_fleet(ai_settings, screen, aliens): sample_alien = Alien(ai_settings, screen) alien_width = sample_alien.rect.width available_space_x = ai_settings.screen_width - 2 * alien_width numbers_alien_x = int(available_space_x / (2 * alien_width)) for alien_number in range(numbers_alien_x): alien = Alien(ai_settings, screen) alien.rect.x = alien_width + 2 * alien_width * alien_number aliens.add(alien)
①我們首先創建一個sample_alien實例,目的是獲取單個外星人的寬度,方便根據外星人的寬度和游戲窗口的寬度,酌情安排所有外星人的空間
②然后進行計算,規定每個外星人距離窗口左右邊界的距離不得小於1個外星人的寬度,據此計算出所有外星人可用的x軸空間;又規定每個外星人之間的間距為1個外星人的寬度,也就假設每個外星人所擁有的x軸空間其實為2倍的外星人寬度;通過available_space_x除以2 * alien_width,求得窗口的一行應當放置的外星人個數
③由於每個外星人的橫坐標不同,因此通過遍歷range設置x坐標(即alien.rect.x代碼行),最后將各個橫坐標不同的外星人添加入aliens組中
【創建多行外星人】
首先考慮數據,我們規定可用的垂直空間為:游戲窗口的高度-第一行外星人的上邊距(1個外星人高度)-飛船高度-2倍外星人高度;其中2倍的外星人高度是留給飛船的設計空間
這樣,模仿創建一行外星人的代碼來編寫創建多行外星人,只需使用兩個嵌套在一起的循環:
def creat_fleet(ai_settings, screen, ship, aliens): sample_alien = Alien(ai_settings, screen) alien_width = sample_alien.rect.width alien_height = sample_alien.rect.height available_space_x = ai_settings.screen_width - 2 * alien_width numbers_alien_x = int(available_space_x / (2 * alien_width)) available_space_y = ai_settings.screen_height - 3 * alien_height - ship.rect.height numbers_alien_y = int(available_space_y / (2 * alien_height)) for number_alien_y in range(numbers_alien_y): for number_alien_x in range(number_alien_x): alien = Alien(ai_settings, screen) alien.rect.y = alien_height + 2 * alien_height * number_alien_y alien.rect.x = alien_width + 2 * alien_width * number_alien_x aliens.add(alien)
然后,我們需在主游戲模塊instance的while循環之前,提前調用create_fleet( )函數創建外星人群
最后,更新update_screen( )函數,添加一條aliens.draw(screen)【why......】
【外星人移動】
設置了初始的外星人布局后,我們令外星人移動。
我們的設想是,一開始外星人向右移動,當觸碰到屏幕邊界,往下移動一段距離后,改變方向往左移動;再次觸碰到邊界時又向下並反向,不斷循環:
class Settings(object): def __init__(self): --snip-- self.fleet_drop_speed = 10 self.fleet_direction = 1
首先在設置類中定義外星人向下移動的速度,以及由於外星人整體只有左右移動,定義fleet_direction為1:當向右移時為1,當向左移時為-1
class Alien(pygame.sprite.Sprite): --snip-- def check_edges(self): 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.speed * self.ai_settings.fleet_direction self.rect.x = self.x
定義check_edges( )方法旨在檢測某個瞬間,任意一個外星人是否觸摸到邊界,若是,則通過返回True值來表示;而update( )方法令外星人隨着主循環而移動,只是為其添加了一個方向:fleet_direction
然后我們在game_functions模塊中添加{當檢測到外星人觸碰邊界時,外星人群的向下位移和方向改變}:
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
首先通過check_fleet_edges( )函數檢測,整個外星人群中是否有某個外星人觸碰到邊界;當有外星人觸碰邊界,調用change_fleet_direction( )函數
change_fleet_direction( )函數先令整個外星人群向下移動,再更改fleet_direction的值。隨着fleet_direction不斷在1和-1間變動,關聯着fleet_direction值的Alien的update( )方法將不斷調整外星人的x值
最后,由於新添了Alien的update( )方法,需像ship.update( )和bullets.update( )那樣添加到主循環的screen.fill( )和pygame.display.flip( )之間。並且,在調用Alien的update( )方法之前調用check_fleet_edges( ),保證外星人群的轉向。
因此我們像封裝update_bullet( )那樣,封裝這兩個方法於game_functions模塊中:
def update_alien(ai_settings, aliens): check_fleet_edges(ai_settings, aliens) aliens.update()
然后將update_alien( )函數置於主循環中即可
【出現了未知的Bug,只有一列的外星人在運動;但是注釋掉update_alien( )函數,屏幕上還是顯示許多列外星人...】
【射殺外星人】
當子彈射擊外星人時,我們要檢查“碰撞”,查看兩者是否重疊在一起。若是,則營造射殺的效果
我們通過pygame.sprite.groupcollide( )來檢測兩個編組的成員之間的碰撞:
def update_bullet(aliens, bullets): --snip-- collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
方法groupcollide( )實為:pygame.sprite.groupcollide(group1 , group2 , dokill1 , dokill2),它相當於多次調用pygame.sprite.spritecollide( )方法
groupcollide( )作用於兩個Group,其所需要的前兩個參數為[相互碰撞的兩個組],而后兩個參數為[當發生碰撞后是否刪除對應的組中元素]
groupcollide( )將返回一個字典,指向collisions變量,其中包含了發生碰撞的子彈和外星人,在這個字典中,鍵為子彈,而值則為對應的外星人。在后面實現計分系統時,會用到這個字典。
由於groupcollide( )的作用機理是快速遍歷兩個編組,檢查是否有rect重疊,因此上述的代碼也可以添加到update_alien( )中
【生成新的外星人群】
當一個外星人群被消滅后,根據游戲規則,我們要創建一群新的外星人且難度增大。鑒於外星人的消滅在update_bullet( )代碼中,因此仍在里面添加代碼。
我們的思路是,檢查aliens編組是否為空,若為空,表明外星人群已消滅,再次調用create_fleet( )函數創建新的外星人群,並提高外星人群的移動速度:
def update_bullet(ai_settings, screen, ship, bullets, aliens): --snip-- if len(aliens) == 0: bullets.empty() create_fleet(ai_settings, screen, ship, aliens) ai_settings.alien_speed_factor += 1
你會發現隨着屏幕元素的增多,游戲運行速度下降了。這是因為Pygame在每次循環中要做的工作更多了。可嘗試調整Settings類中的屬性,找到合適的值。
【檢測外星人與飛船的碰撞】
外星人與飛船碰撞后,需要執行的任務很多,包括:刪除余下的所有外星人和子彈、飛船重新居中、創建新的外星人群。為單純地檢測外星人與飛船碰撞的效果,我們在這里先不執行這些任務:
def update_alien(ai_settings, ship, aliens): --snip-- if pygame.sprite.spritecollideany(ship, aliens): print(''Ship hit!'')
更新外星人的位置后,立即檢測外星人與飛船的碰撞,因此將上述代碼添加到update_alien( )中
方法pygame.sprite.spritecollideany( )接收兩個參數:一個精靈和一個編組;當精靈和編組中的成員未發生碰撞時,其返回None;如果檢測到碰撞,則返回與飛船碰撞的外星人
【響應碰撞&統計游戲信息】
我們可以在飛船與外星人碰撞后,刪除飛船並重新創建一個位於窗口中央;但我們不這樣做,我們通過跟蹤游戲的統計信息來記錄飛船被撞了多少次:
首先我們規定,一次游戲中只有3艘飛船,即飛船被撞毀3次后游戲結束,為此在game_functions模塊中添加游戲屬性:
self.ship_limit = 3
然后我們創建一個新的game_stats模塊,其中包含統計信息的新類——GameStats:
class GameStats(object): 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
我們允許玩家在死亡3次后,再次重新開始游戲,這時就得將游戲中的相關屬性調整:比如飛船的生命值重新更改為3。
於是我們不將統計信息置於__init__( )方法內,而是放在reset_stats( )方法中,這樣,每當玩家需要重新開始游戲的時候,我們就調用reset_stats( )來重置統計信息,而不是逐個地將統計信息修改回來
需要注意的是,我們定義在reset_stats( )中的屬性仍然可以通過普通的方法進行調用
之后,我們在主循環之前創建GameStats類的實例:
stats = GameStats(ai_settings)
然后我們在game_functions模塊中定義ship_hit( )函數,執行當飛船與外星人碰撞后的操作:
import time def ship_hit(ai_settings, screen, ship, bullets, aliens, stats): stats.ships_left -= 1 aliens.empty() bullets.empty() create_fleet(ai_settings, screen, ship, aliens) ship.center_ship() time.sleep(1)
首先通過stats.ships_left -= 1更新統計數據,然后立刻將屏幕上的外星人和子彈清除,並創建一群新的外星人
你會發現,我們還得需要將飛船更新。我們使用統計信息的目的就在此,不刪除已有的飛船,因此統計信息中已經“記住”飛船死亡一次,我們只需將飛船恢復到屏幕中央下方的起始位置
為此,我們在Ship類中定義一個新方法center_ship( ):
def center_ship(self): self.center = self.screen_rect.centerx
【注意我們有self.center = float(self.rect.centerx)這一行代碼,記住,當有這種通過小數暫存數據的代碼的時候,若要更新數據,一定要直接更新self.center,而不是self.rect.centerx】
因此,center_ship( )中的代碼不能寫成self.rect.centerx = self.screen_rect.centerx!
最后,為了讓玩家在新外星人群出現前注意到發生了碰撞,我們很貼心地讓游戲暫停1秒
最最后,我們不要忘了更新update_alien( )函數:
def update_alien(ai_settings, screen, ship, bullets, aliens, stats): if pygame.sprite.spritecollideany(ship, aliens): ship_hit(ai_settings, screen, ship, bullets, aliens, stats)
此外,重新開始游戲,外星人群的運動速度應該被恢復,因此將Settings類中的alien_speed_factor也儲存到GameStats類的reset_stats( )方法中,然后耐心修改調用該屬性的地方
【外星人到達屏幕底端】
已經設置好外星人與飛船碰撞的處理代碼,當外星人到達屏幕底端時的處理就變得很簡單
def check_aliens_bottom(ai_settings, screen, ship, bullets, aliens, stats): for alien in aliens.sprites(): if alien.rect.bottom >= ai_settings.screen_height: ship_hit(ai_settings, screen, ship, bullets, aliens, stats) break
由於[外星人到達屏幕底端]和[外星人與飛船碰撞]的處理一樣,我們可以直接在檢查到外星人到達底端的時候,調用ship_hit( )。感受下將各項功能分割,再通過函數調用的便捷性,create_fleet( )不也是么
將上述代碼儲存在game_functions模塊中,我們還不忘在update_alien( )函數中調用:
def update_alien(ai_settings, screen, ship, bullets, aliens, stats): --snip-- check_aliens_bottom(ai_settings, screen, ship, bullets, aliens, stats)
【游戲結束】
目前我們通過ship_limit賦值給ships_left來控制飛船的生命值,但當飛船死亡3次時游戲仍在繼續,下面來設置死亡3次后游戲結束:
def reset_stats(): self.game_active = True
先在統計信息中設置游戲狀態,初始時都為True
def ship_hit(...): if stats.ships_left > 0: stats.ships_left -= 1 else: stats.game_active = False
在響應飛船被撞毀的函數ship_hit( )中添加if語句進行判斷,當死亡3次后,更改代表游戲狀態的數值game_active為False
while 1: gf.check_events(...) if stats.game_active: ship_update() gf.update_bullets(...) gf.update_aliens(...) gf.update_screen(...)
由上面代碼可以看出,當game_active為False時,與游戲元素運動有關的3個函數是不會執行的;而check_events( )和update_screen( )是即使游戲處於非活動狀態也應調用,例如:check_events( )知道玩家是否按下Q鍵退出游戲,update_screen( )使得當玩家開始新游戲時屏幕得以刷新
【設置按鈕】
我們讓玩家在一開始就通過點擊''Play''按鈕開始游戲,因此先得讓游戲處於停止運行狀態:
class GameStats(object): def __init__(self, ai_settings): --snip-- self.game_active = False
Python沒有內置的創建按鈕的方法,我們就創建一個Button類,用於創建帶標簽的實心矩形:
import pygame class Button(object): def __init__(self, screen, msg):【←為什么書上這里會有 ai_settings形參?!】 self.screen = screen self.screen_rect = self.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)
上述代碼儲存在新開的button模塊中。下面進行解析:
def __init__(self , screen , msg):
self.screen = screen
self.screen_rect = self.screen.get_rect( )
傳入參數screen和msg,其中msg為message的縮寫,若要創建''Play''按鈕,則這時要傳入的msg為字符串'Play'。
鑒於Python沒有內置的創建按鈕的方法,我們的打算是:先創建一個矩形,再將文本'Play'置於矩形中,當檢測到鼠標點擊矩形所在的區域的時候,修改game_active為True,實現按鈕功能
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.width和self.height分別設置即將創建的矩形的寬和高
self.button_color和self.text_color分別設置按鈕矩形的顏色為綠色、文本的顏色為白色
self.font = pygame.font.SysFont(None , 48)是對即將創建的文本的設置,實參None讓Pygame使用默認字體,48表示字號。
pygame.font.SysFont( )返回的是一個Font對象,Pygame需要將Font對象渲染為圖像來處理文本,否則無法繪制在屏幕上。對Font對象的渲染在下面。
self.rect = pygame.Rect(0 , 0 , self.width , self.height)
self.rect.center = self.screen_rect.center
這一步簡單地創建一個矩形,並設置到屏幕中央
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
為了使代碼更Pythonic,將渲染文本的代碼置於prep_msg( )方法中。
①處通過Font.render( )方法實現了對文本的渲染,Font.render( )接收4個形參。msg為傳入的文本字符串,而形參True指定開啟/關閉反鋸齒功能;第三個參數即為文本的顏色
第四個self.button_color參數有點特殊,它的意義是[文本的背景色],如果沒有就默認為透明。我們將字符串渲染到上一步的矩形中,由於self.button_color本身就是為矩形准備的顏色參數,所以無論這里是[透明]還是[self.button_color],都不會有太大差別。因此該參數可傳可不傳,因為文本后面也會置於self.button_color的顏色背景中。
②③處先獲取被渲染的文本圖像的矩形,再讓其在按鈕矩形上居中
def draw_button(self):
self.screen.fill(self.button_color , self.rect)
self.screen.blit(self.msg_image , self.msg_image.rect)
最后再設置一個draw_button( )方法將按鈕繪制到屏幕中,這個方法是獨立的,並不是像prep_msg( )那樣只是對代碼的重構。
screen.fill( )在創建游戲屏幕背景色的時候出現過,只是那時僅有一個參數bg_color,而這個方法的第二個參數是用於指定范圍的。當沒有傳入該參數的時候,默認將第一個顏色參數作用於整個屏幕。
這里傳入第二個參數self.rect,即按鈕的位置。通過將這個位置填充為self.button_color指定的顏色,模擬按鈕的存在。
最后像blit飛船ship的圖像那樣,將文本圖像blit到屏幕中
【屏幕上繪制】
定義好Button類后,將其與游戲代碼聯系起來:
首先,導入Button類並創建實例:
from button import Button play_button = Button(screen, 'Play')
然后,更新update_screen( )函數。
我們只是想當游戲處於非活動狀態時才繪制Play按鈕,而游戲運行期間不進行繪制,因此在update_screen( )函數中添加:
if not stats.game_active: play_button.draw_button()
【開始游戲】
將按鈕繪制到屏幕上后,我們要對用戶使用鼠標點擊按鈕產生反應,即檢測並反應鼠標事件
--snip-- elif event.type == pygame.MOUSEBUTTONDOWN: mouse_x , mouse_y = pygame.mouse.get_pos() check_play_button(stats, play_button) def check_play_button(stats, play_button) if play_button.rect.collidepoint(mouse_x, mouse_y): stats.game_active = True
首先在check_events( )函數中添加一行代碼,檢測事件的類型是否為[鼠標點擊],如果是,就通過pygame.mouse.get_pos( )方法,立即獲取本次鼠標點擊的位置mouse_x和mouse_y
獲取鼠標點擊位置后,調用check_play_button( )函數,判斷本次點擊是否在''Play''按鈕的范圍內。
在check_play_button( )函數的定義中,使用Rect.rect.collidepoint( )方法。該方法檢測傳入的點(mouse_x , mouse_y)是否位於該矩形的內部;如果是,修改game_active為True,開始游戲
【重置游戲】
嘗試開始游戲並撞機3次后,游戲會停止,這時的''Play''按鈕再次出現,按下即可重新開始游戲。
但是由於stats中的ship_limit屬性已經減小為0,這種情況下再次撞機,就沒有“三條生命”了,直接出現''Play''按鈕。
為此,我們在check_play_button( )函數中增加一些功能,主要是重設數據stats
def check_play_button(ai_settings, ship, bullets, aliens, stats, play_button, mouse_x, mouse_y): if play_button.rect.collidepoint(mouse_x, mouse_y): stats.game_active = True stats.reset_stats() bullets.empty() aliens.empty() create_fleet(ai_settings, ship, aliens, stats) ship.center_ship()
新添加的代碼與ship_hit( )函數相似,只是少了ship_limit -= 1等代碼
【將按鈕切換到非活動狀態】
游戲過程中,若玩家不小心用鼠標點擊到''Play''按鈕所在的區域,游戲依然會做出響應,重新開始。
簡單的就是check_play_button( )函數中,添加重置游戲的條件:
if play_button.rect.collidepoint(mouse_x, mouse_y) and not stats.game_actvie: --snip--
添加not stats.game_actvie條件,即,當game_actvie為False時,點擊''Play''按鈕才能啟動重啟游戲的功能;若game_active為True,怎么點擊都無效
個人認為之所以之前''Play''按鈕不可見,點擊后仍可生效,是因為游戲一開始就為了顯示''Play''按鈕而調用了stats.reset_stats( ),也就是說,''Play''按鈕從一開始就已經存在。之后由於不滿足play_button.draw_button( )的條件,以及flip( )導致按鈕不可見,但當鼠標點擊到按鈕的范圍時,仍滿足play_button.rect.collidepoint(mouse_x , mouse_y)條件
【隱藏光標】
避免游戲過程中光標的添亂,我們可以設置當game_active為False時隱藏光標,當game_active為True時又顯示光標
if play_button.rect.collidepoint(mouse_x, mouse_y) and not stats.game_active: pygame.mouse.set_visible(False) --snip--
又:
else: stats.game_active = False pygame.mouse.set_visible(True)
【開始游戲快捷鍵P】
當屏幕出現''Play''按鈕時,我們除了用鼠標點擊按鈕外,還可以設置使用快捷鍵P
首先要重構check_play_button( )函數,原函數為:
def check_play_button(...): if play_button.rect.collidepoint(mouse_x, mouse_y) and not stats.game_active: pygame.mouse.set_visible(False) stats.reset_stats() stats.game_active = True bullets.empty() aliens.empty() create_fleet(...) ship.center_ship()
現將if代碼塊中的代碼包裝到start_game( )函數中,也就是:
def check_play_button(...): if play_button.rect.collidepoint(mouse_x, mouse_y) and not stats.game_active: start_game(...)
然后在check_keydown_events( )函數中添加:
elif event.key == pygame.K_p: if not stats.game_active: statr_game(...)
不要忘了規定只在stats.game_active為False時才能重啟游戲;還要小心傳參
【等級提升】
為了使游戲更具趣味性和挑戰性,我們在每次完全擊殺外星人群后,加快游戲的節奏
所謂“加快游戲的節奏”,是指將ship、bullet、alien的speed值增大
首先我們對Settings類進行修改,將游戲設置划分為靜態設置和動態設置兩組:
Class settings(object): def __init__(self): --snip-- self.speedup_scale = 1.1 self.initialize_dynatic_settings() def initialize_dynatic_settings(self): self.ship_speed_factor = 1.5 self.bullet_speed_factor = 1 self.alien_speed_factor = 1
新定義的speedup_scale為加速范圍,用於乘以ship、bullet、alien的speed值,實現加速;而initialize_dynatic_settings( )方法與GameStats類的reset_stats( )方法類似,都是為了在重置游戲的時候將游戲的相關屬性復原
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
increase_speed()方法將三個speed值都增大
修改Settings類后,我們需要調用新添加的兩個方法。
一個在update_bullet( )函數中:
if len(aliens) == 0: ai_settings.increase_speed() create_fleet(...)
因為負責檢測外星人群是否被完全消滅的代碼位於update_bullet( )函數中;
另一個在start_game( )函數中直接添加:
ai_settings.initialize_dynatic_settings()
我們知道,函數start_game( )用於[當飛船死亡次數超過3,游戲重置]時的情況,因此這時也會執行stats.reset_stats( ),同理
【顯示得分】
每次射殺外星人后都將得分,為了將得分顯示到屏幕上,我們首先想辦法將一個數字blit到屏幕上
而其做法與將''Play''並無二致
首先創建一個scoreboard.py文件,並在game_stats的initialize_dynatic_settings( )方法中添加:
self.score = 0
然后,編寫scoreboard模塊中的代碼:
import pygame class Scoreboard(object): def __init__(self, ai_settings, screen, stats): self.ai_settings = ai_settings self.screen = screen self.stats = stats self.screen_rect = self.screen.get_rect() 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.score_rect = self.score_image.get_rect() sefl.score_rect.right = self.screen_rect.right - 20 self.score_rect.rop = 20 def show_score(self): self.screen.blit(self.score_image, self.score.rect)
綜上,你會發現Scoreboard類的內容與Button類的內容有一點相似
隨后,在instance模塊中import Scoreboard並創建實例sb;由於要繪制到屏幕上,再在update_screen( )函數中添加:
sb.show_score()
別忘了向update_screen( )函數添加實參sb
【更新得分】
目前我們的分數只是game_stats模塊中的self.score = 0,下面讓我們將self.score更新
首先確定單個外星人的“價值”,我們在Settings類的initialize_dynatic_settings( )方法中添加:
self.alien_points = 50
暫時指定每擊殺一個外星人,分數增加50。之所以在initialize_dynatic_settings( )方法中添加,是因為之后隨着游戲節奏的加快,會上調這個值;而當游戲重新開始時,應該恢復這個值為50
更新分數,前提是擊殺了外星人,即子彈與外星人發生了碰撞,因此我們將目光投向update_bullets( )函數,因為該函數負責檢測子彈與外星人的碰撞:pygame.sprite.groupcollide(bullets , aliens , True , True),由於update_bullets擁有上面的代碼行,因此更改代碼:
collision = pygame.sprite.groupcollide(bullets, aliens, True, True) if collision: stats.score += ai_settings.alien_points sb.prep_score()
每當發生一次碰撞,方法groupcollide( )返回一個字典,當檢測到collision為True時,就說明這一刻成功擊殺了外星人,更新stats.score值
更新stats.score值后,不要忘了,繪制到屏幕上的分數是通過prep_scroe( )方法中的score_str = str(self.stats.score)獲取的,所以也得調用一次sb.prep_score( ),更新屏幕上的數值
【同時消滅】
當創建大子彈同時消滅多個外星人的時候,你會發現,pygame還是只添加一個外星人的分值
【groupcollide( )】
方法groupcollide( )返回的是碰撞的雙方組成的一個字典dict,由於傳入的形參為bullets、aliens,因此該字典為{bullet:alien};倘若一顆大子彈同時擊中多個外星人,則返回的字典為{bullet:[alien1 , alien2 ... , alienx}。
之前的代碼只是檢測一次遍歷中字典collision是否為True,若為True,則執行一次[加分]。當同時擊中多個外星人,返回的仍是一個字典,檢測為True后執行一次加分。
因此我們可以這樣修改:
if collision: for aliens in collision.values(): stats.score += ai_settings.alien_points * len(aliens) sb.prep_score()
△我認為,在一次遍歷中,存在不止發射了一顆子彈的情況;也就是說,collision並非總是含有一個鍵值對元素,有可能含有多個。因此,首先通過for aliens in collision.values( )獲取遍歷所有可能的鍵值對元素,獲取每個值的長度(長度即一次性擊殺的外星人數),通過乘以長度得到更准確的分數
【提高點數】
之前有提及,擊殺單個外星人的分數為50,但隨着游戲節奏的上升,這個值會變大
class Settings(object): def __init__(self): --snip-- self.scoreup_scale = 1.5 def increase_speed(self): --snip-- self.alien_points = int(self.alien_points * self.scoreup_scale)
我們注意到加快游戲節奏的代碼主要存儲在increase_speed( )中,因此先像定義self.speedup_scale那樣,定義外星人分值的增加幅度self.scoreup_scale為1.5,再在increase_speed( )中改變self.alien_points值,並通過int( )取整
【將得分圓整】
“大多數街機風格的射擊游戲都將得分顯示為10的整數倍(即個位數恆為0)”
我們還將設置得分的格式,在大數字中添加用逗號表示的千位分隔符:
def prep_score(self): rounded_score = int(round(self.stats.score, -1)) score_str = '{: ,}'.format(rounded_score) --snip--
【round( )】
函數round( )通常通過指定第二個實參,讓小數精確到小數點后多少位。而當第二個實參為負數時,round( )將圓整到最近的10、100、1000等整數倍
Python 2.7中,round( )總是返回一個小數值,因此使用int( );而Python 3可以省略對int( )的調用
再通過'{: ,}'.format( )進行千分位分隔,而format( )本身就返回str對象
【補充】
需要注意的是,我們更新屏幕上的數字是通過調用sb.prep_score( )的,而這個方法被我們置於update_bullets( )的if collision代碼塊中,也就是一旦有子彈與外星人碰撞,就更新stats.score的值並更新屏幕上的顯示得分。
然而當三次死亡后我們再次點擊'Play'開始游戲,這時的得分還沒有更新!依舊保持為上一局的得分!只有當你擊殺第一個外星人時,分數才會更新為50
原因就在於我們將sb.prep_score( )放在檢測子彈碰撞的函數中,只要沒有發生子彈碰撞,就不會調用sb.prep_score( ),屏幕上的分數也就無法更新
為了讓重新開始游戲時的得分顯示為'0',我們需要在其他地方再次調用sb.prep_score( )。而我們知道當重啟游戲(點擊'Play'按鈕)時,調用的是start_game( )函數,因此在該函數添加:
sb.prep_score()
然后你會發現,重啟游戲之后,得分就立刻變成'0'了
【最高分】
整場游戲中,我們打算將最高得分繪制到屏幕中央正上方,而繪制的步驟,與之前繪制得分無異:
首先在GameStats中添加high_socre = 0,注意不要將其添加到initialize_dynatic_settings( )中,因為每次重啟游戲時都不會重置high_score值
然后返回Scoreboard類中:
def __init__(self): --snip-- self.prep_high_score() 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.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)
這時運行程序,就可以看到屏幕正上方的'0'了
然后更新stats.high_score值,我們在game_functions模塊中添加函數:
def update_high_score(stats, sb): if stats.score > stats.high_score: stats.high_score = stats.score sb.prep_high_score()
新添加的update_high_score( )函數比較了stats.score和stats.high_score的大小。如果符合條件,就更改stats.high_score的值,並且通過sb.prep_high_score( )更新屏幕上的數字圖片
我們之前將sb.prep_score( )置於update_bullets( )函數中,因為在該函數中有檢測子彈與外星人碰撞的代碼,所以這次我們也將update_high_score( )放入其中:
if collision: for aliens in collision.values(): stats.score += ai_settings.alien_points * len(aliens) sb.prep_score() update_high_score(stats, sb) --snip--
【將high_score寫入文件】
我們發現,high_score記錄的是歷史最高的分數值,然而當關閉游戲后再次運行程序,這個值會清零;因此我們需要將high_score這個值寫入到文件中,讓文件一直儲存着這個值,當下次打開游戲的時候,就從文件中讀取這個值。
首先我們在存有游戲代碼的文件夾中創建一個txt文本,命名為high_score,打開high_score.txt,在文本中輸入一個'0'
然后我們要讀取這個文件夾中的內容,因此在prep_high_score( )方法中添加:
def prep_high_score(self): with open('high_score.txt') as file: content = int(file.read())# 記住read( )返回str對象 high_score = int(round(content, -1)) high_score_str = '{: ,}'.format(high_score) --snip--
這樣,我們就將high_score.txt中的'0'讀取出來了,並無縫對接到之前的代碼中
然后使通過寫入更新high_score值,就在update_high_score( )函數中進行修改:
def update_high_score(stats, sb): if stats.score > stats.high_score: stats.high_score = stats.score with open('high_score.txt', 'w') as file: file.write(str(stats.high_score))# write()只接收str對象 sb.prep_high_score()
通過寫入模式('w')而不是附加模式('a')打開high_score.txt,將新的stats.high_score值存儲進去
[注意] 到這一步,stats.high_score還是有用的,它的作用就是比較,不能刪去
【注意】
盡管寫入模式('w')在不存在目標文件時,會自動創建一個,但在這里必須提前手動創建一個high_score.txt文件。因為在整個pygame的執行順序中,創建sb實例要早於調用update_high_score( )函數。而創建sb實例就意味着要執行[with open('high_score.txt') as file]的讀取操作,讀取操作不會自動創建文件,因此會報錯FileNotFoundError
還有就是,如果你要“走后門”更改high_score.txt的內容,必須更改為int類型對象,如果是其它類型的對象(哪怕是None對象),都要重新修改代碼以適應這種情況。
【提升等級】
我們標明每一群外星人的等級,以便玩家檢查
首先在GameStats的reset_stats( )方法中添加:
self.level = 1
在Scoreboard類的__init__( )方法中調用self.prep_level( ),開始設置prep_level( ):
def prep_level(self): self.level_image = self.font.render(str(self.stats.level), True, self.text_color) self.level_rect = self.image.get_rect() self.level_rect.right = self.screen_rect.right - 20 self.level_rect.top = self.score_rect.bottom + 10
注意render( )的第一個參數必須為str對象
再將下列代碼添加進show_score( )方法,解決繪制到屏幕上的問題:
self.screen.blit(self.level_image, self.level_rect)
剩下的就是隨着游戲的進行,更新level值,我們添加進update_bullets( )函數中:
if len(aliens) == 0: ai_settings.increase_speed() create_fleet(ai_settings , ship, aliens, stats) stats.level += 1 sb.prep_level()# 很容易遺漏這一步,更新數值后應同步更新屏幕
參照sb.prep_score( ),再在start_game( )添加sb.prep_level( )
【顯示飛船】
我們的設定是,玩家除了一開始的飛船外,還有三架備用的飛船,我們打算將這三架顯示到屏幕上,提醒玩家還剩下多少飛船
我們之前繪制飛船是通過blit( )的,而現在飛船的數量不止1架,因此最好通過pygame.sprite.Group( )將它們囊括。通過創建Group( )可以像繪制外星人一樣在屏幕上顯示,你也可以把即將要顯示的三架飛船看成是不會移動的微型外星人群
既然要模仿Alien,那么首先得讓Ship類成為精靈:
class Ship(pygame.sprite.Sprite): def __init__(self, ai_settings, screen): super().__init__() --snip--
由於前面的代碼基本已經完成,我們想編寫顯示飛船數量的代碼,考慮到即將要編寫的代碼的主要作用是繪制,那么不妨直接寫入scoreboard.py中,僅僅是影響執行順序問題而已
from ship import Ship def prep_ships(self): self.ships = pygame.sprite.Group() for ship_number in range(self.stats.ship_limit): ship = Ship(ai_settings, screen) ship.rect.left = 10 + ship_number * ship.rect.width ship.rect.top = 10 self.ships.add(ship)
在新定義的prep_ships( )方法中,我們像繪制外星人群那樣繪制了“飛船群”,還記得Group如果繪制到屏幕上嗎?參考aliens的aliens.draw(screen),因此我們在show_score( )方法中添加:
def show_score(self): --snip-- self.ships.draw(self.screen)
這樣,所謂的'show_score'已經名存實亡了(N_N)
之后便是更新屏幕的問題,也就是找到恰當的地方調用prep_ships( )
首先是start_game( )函數,在游戲開始之初就應該將三架飛船繪制到屏幕上;然后是ship_hit( )函數,因為ships組中的元素數量是由stats.ship_limit決定的,而更新stats.ship_limit值就在ship_hit( )函數中:
def ship_hit(...): if stats.ship_limit > 0: stats.ship_limit -= 1 else: stats.game_active = False pygame.mouse.set_visible(True) sb.prep_ships() --snip--
最后便是修改參數的問題了
【完結撒花!】
''''''''
【錯誤】
1、class的__init__( )方法中,定義屬性時忘記添加self,根本關聯不到實例上!
2、我認為,編寫控制飛船持續移動的代碼時,不能使用while語句而是改用if語句的原因,是要避免while語句和主循環沖突。主循環的作用在於持續不斷地更新屏幕,移動的代碼被封裝在Ship類的upgrade( )方法中,在主循環中調用ship.upgrade( )時,若符合移動的條件,在主循環的無限重復下能不斷地執行if語句
這有些難理解,但要記得,實現飛船的某些功能,要到Ship類中編寫代碼;check_events( )函數僅適用於檢查事件,不直接進行游戲元素的功能實現
3、traceback提示ai_settings未定義,我反復看了self.ai_settings = ai_settings以及其他處的ai_settings都沒發現...應該前面加self.
4、rect對象的top、bottom屬性對於我們來說都是直觀的,打開的屏幕最上面就是top、最下面就是bottom;但pygame所創建的游戲窗口原點(0 , 0)位於左上角,也就是說,在上下方向它與我們平常所創建的平面直角坐標系是相反的!從bottom到top,縱坐標的值不斷減小。
因此在設置飛船可以上下移動的時候要小心,up時y值在減小、且判斷不超出屏幕條件為rect.top > screen_rect.top,注意是大於號(>);down時則是相反
5、傳入__init__的參數中,若該參數僅僅是引用其儲存的值或內容,無需綁定到self上;若要在class中對其進行修改利用,必須綁定到self上,否則會報錯未定義
比如Ship類的參數ai_settings、screen,ai_settings只是為了設置飛船速度而調用的一個值,本身與直接傳入1.5沒有什么不同;但screen參數在blitme( )方法中要進行利用:self.screen.blit(self.image , self.rect),需綁定到self上
6、關於draw和blit,我是這樣想的。憑空創建的矩形圖像,通過pygame.draw.rect( )來顯示,包含在draw_...( )函數中;單個本地圖像,通過self.Surface.blit( )顯示,包含在blitme( )函數中;多個矩形圖像,囊括在Group中,須遍歷多個本地圖像,囊括在Group中,使用Group特有的draw(Surface)函數,而不是以上三種
【問題】
①如何確定參數是否要綁定到self?
②混淆使用if ... elif ...和if ... if ...會有什么不良后果?
③定義飛船能同時相應多個方向鍵,為何往左上方移動時無法射出子彈,其它方向都可以?
④super( ).__init__( )繼承的是Sprite的什么?
⑤為何pygame的原點(0,0)在左上角?
⑥Group.sprites與Group.copy有什么不同?我把兩個替換着用都沒有問題啊!?你說呢,就像list和list[:]一樣,一個本身,一個副本,取決於你用在什么地方
⑦問題類似③,同時按住左鍵和右鍵,為什么飛船只能往下?
⑧繪制外星人通過aliens.draw(screen),那Alien類中的blitme( )方法還有沒有用?
⑨random類是如何保證完全隨機的?
⑩為什么update外星人后,全屏的外星人群只剩下一列在移動?
十分感謝StackOverflow的朋友,原因是在create_fleet( )函數時:
alien = Alien(...) alien.rect.x = alien_width + 2 * alien_width * number_alien_x alien.rect.y = alien_height + 2 * alien_height * number_alien_y
我們直接定義的是alien.rect.x,而返回到Alien類中,我們是先定義self.x = float(self.rect.x),再在update(self)方法中更改self.x並self.rect.x = self.x
由於在create_fleet( )中沒有定義self.x,所以self.x值就是剛創建Alien實例時的默認值0;但是self.rect.x被定義了,所以暫時看到了滿屏的外星人群
這時調用Alien的update( )方法:
def update(self): self.x += (self.speed * self.ai_setting.fleet_direction) self.rect.x = self.x
一次調用后self.x值為self.speed,你會發現,所有的外星人的self.x值都相同!再通過self..rect.x = self.x,原本一行外星人各自的self.rect.x值都不同,這下子就都一樣了,全為self.speed!然后所有外星人都重疊了!
此外,之所以在后面射殺外星人中沒有察覺到外星人的重疊,是因為“射殺”的設置造成的。我們檢測到外星人的rect與子彈的rect有重合,就判定外星人“被射殺”。重疊的外星人是同一時間檢測到與子彈重疊的,因此同時全部被“射殺”
【建議】因此在設定值時,留意是否為float( )承載的值,若是,要先定義該float( )值,不要嫌麻煩
11、復原ship位置時為什么不能直接通過ship.rect.bottom = screen_rect.bottom進行操作,而是必須通過ship.x、ship.y值?
12、突然發現沒有將Ship類繼承pygame.sprite.Sprite,更沒有super( ).__init__( ),但是運行竟然沒差別?
13、為什么書上的演示代碼是import pygame.font,它在下面也是self.font = pygame.font.SysFont( ... ),完全可以import pygame就行了?