【文章基於《Python編程-從入門到實踐》】python
【項目規劃】程序員
「開發大型項目時先作好規劃再動手編寫項目很重要」算法
下面是對《外星人入侵》的規劃:編程
①玩家控制一艘在屏幕底部中央的飛船,可經過箭頭鍵左右移動飛船,還可使用空格鍵射擊dom
②一羣外星人出如今天空中,他們不斷向下移動ide
③待玩家將全部外星人消滅後,會出現一羣移動速度更快地新外星人函數
④當有外星人撞到玩家的飛船或到達屏幕底部,玩家就損失一艘飛船工具
⑤玩家損失三艘飛船後,遊戲結束佈局
【安裝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--
最後即是修改參數的問題了
【完結撒花!】
''''''''
【錯誤】
一、class的__init__( )方法中,定義屬性時忘記添加self,根本關聯不到實例上!
二、我認爲,編寫控制飛船持續移動的代碼時,不能使用while語句而是改用if語句的緣由,是要避免while語句和主循環衝突。主循環的做用在於持續不斷地更新屏幕,移動的代碼被封裝在Ship類的upgrade( )方法中,在主循環中調用ship.upgrade( )時,若符合移動的條件,在主循環的無限重複下能不斷地執行if語句
這有些難理解,但要記得,實現飛船的某些功能,要到Ship類中編寫代碼;check_events( )函數僅適用於檢查事件,不直接進行遊戲元素的功能實現
三、traceback提示ai_settings未定義,我反覆看了self.ai_settings = ai_settings以及其餘處的ai_settings都沒發現...應該前面加self.
四、rect對象的top、bottom屬性對於咱們來講都是直觀的,打開的屏幕最上面就是top、最下面就是bottom;但pygame所建立的遊戲窗口原點(0 , 0)位於左上角,也就是說,在上下方向它與咱們日常所建立的平面直角座標系是相反的!從bottom到top,縱座標的值不斷減少。
所以在設置飛船能夠上下移動的時候要當心,up時y值在減少、且判斷不超出屏幕條件爲rect.top > screen_rect.top,注意是大於號(>);down時則是相反
五、傳入__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就好了?