《Python程式設計:從入門到實踐》筆記。
本章主要是對上一篇的繼續,新增“外星人”,“外星人”與飛船的互動。
1. 回顧專案
開發較大的專案時,進入每個開發階段前回顧一下開發計劃,搞清楚接下來要通過程式碼實現哪些功能至關重要。本篇將設計一下內容:
- 研究即有程式碼,確定實現新功能前是否需要重構程式碼
- 在螢幕左上角新增一個外星人,並指定合適的邊距
- 根據第一個外星人的邊距和螢幕尺寸計算螢幕上可容納多少個外星人。編寫一個迴圈來填滿螢幕的上半部分
- 讓外星艦隊向兩邊和下方移動,直到外星人被全部擊落,或有外星人撞到飛船,或有外星人抵達螢幕底部。如果所有外星人都被擊落,再建立一批外星人。如果有外星人撞到飛船或到達螢幕底部,則銷燬飛船並再建立一群外星人。
- 限制玩家可用的飛機數,消耗完則遊戲結束
希望各位上一篇的程式碼沒有刪掉。在開始新的程式碼前,我們先在前面的check_keydown_events()
函式中新增“通過快捷鍵Q結束遊戲”的程式碼:
def check_keydown_event(event, ship, ai_settings, screen, bullets):
-- snip --
elif event.key == pygame.K_q:
sys.exit()
複製程式碼
2. 建立外星人
首先我們需要編寫一個外星人Alien
類。新建alien.py
模組,在其中加入如下程式碼:
import pygame
from pygame.sprite import Sprite
class Alien(Sprite):
"""表示單個外星人的類"""
def __init__(self, ai_settings, screen):
"""初始化外星人並設定其起始位置"""
super(Alien, self).__init__()
self.screen = screen
self.ai_settings = ai_settings
# 載入外星人影象,並設定其rect屬性
self.image = pygame.image.load("images/alien.bmp")
self.rect = self.image.get_rect()
# 每個外星人最初都在螢幕左上角附近
self.rect.x = self.rect.width
self.rect.y = self.rect.height
# 儲存外星人的準確位置
self.x = float(self.rect.x)
def blitme(self):
"""在指定位置繪製外星人"""
self.screen.blit(self.image, self.rect)
複製程式碼
它和Bullet
類一樣繼承自Sprite
類。現在開始建立多行外星人。
2.1 修改game_functions.py模組
首先在game_functions.py
模組中新增create_fleet()
函式用於建立外星艦隊:
def create_fleet(ai_settings, screen, ship, aliens):
"""建立外星艦隊"""
alien = Alien(ai_settings, screen)
# 計算每行能放多少個
number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)
# 計算能放多少行
number_rows = get_number_rows(ai_settings, ship.rect.height,
alien.rect.height)
# 巢狀迴圈建立外星艦隊
for row_number in range(number_rows):
for alien_number in range(number_aliens_x):
# 建立外星人並將其加入艦隊
create_alien(ai_settings, screen, aliens, alien_number, row_number)
複製程式碼
然後我們依次補充下面三個函式(注意各個函式的引數),這三個函式也位於game_functions.py
中:
get_number_aliens_x()
: 計算一行能放多少個外星人
def get_number_aliens_x(ai_settings, alien_width):
"""計算每行可容納多少個外星人"""
# 左右兩側留出一個外星人的寬度
available_space_x = ai_settings.screen_width - 2 * alien_width
# 列間距為一個外星人寬度
number_aliens_x = int(available_space_x / (2 * alien_width))
return number_aliens_x
複製程式碼
get_number_rows()
: 計算能放多少行外星人
def get_number_rows(ai_settings, ship_height, alien_height):
"""計算螢幕可容納多少行外星人"""
# 可用高度 = 視窗高度 - 上方一個外星人高度 - 下方一個飛船高度 - 兩個外星人高度作為緩衝空間
available_space_y = (ai_settings.screen_height - 3 * alien_height - ship_height)
# 行距為一個外星人高度
number_rows = int(available_space_y / (2 * alien_height))
return number_rows
複製程式碼
create_alien()
: 建立外星人
def create_alien(ai_settings, screen, aliens, alien_number, row_number):
"""建立一個外星人並將其放在當前行"""
alien = Alien(ai_settings, screen)
# 下面就是根據上面的公式計算每一個外星人在視窗中的位置(這是左上角的座標)
alien.x = alien.rect.width * (1 + 2 * alien_number)
alien.rect.x = alien.x
alien.rect.y = alien.rect.height * (1 + 2 * row_number)
aliens.add(alien)
複製程式碼
現在我們還需要修改update_screen()
函式:
def update_screen(ai_settings, screen, ship, bullets, aliens):
-- snip --
# 繪製外星人,放在繪製子彈的程式碼後面,讓外星人的影象覆蓋掉子彈的影象
aliens.draw(screen)
-- snip --
複製程式碼
注意,該函式增加了一個引數aliens
,這是個Group
物件,所以程式碼中的draw()
方法也跟前一篇中的bullets.update()
方法一樣,一行程式碼更新所有物件。
2.2 修改alien_invasion.py模組
在主程式中新增建立外星人的程式碼:
def run_game():
-- snip --
gf.create_fleet(ai_settings, screen, ship, aliens)
while True:
-- snip --
# 比之前程式碼多傳入了一個aliens引數
gf.update_screen(ai_settings, screen, ship, bullets, aliens)
-- snip --
複製程式碼
現在我們執行程式將會得到如下結果:
3. 讓外星艦隊動起來
我們將讓外星艦隊在窗體中向右移動,撞到螢幕邊緣後下以一定距離下降,再沿反方向移動,直到外星人被消滅,或外星人撞上飛船,或有外星人到達窗體底部。
3.1 補充settings.py模組
class Settings:
def __init__(self):
-- snip --
self.fleet_drop_speed = 10
# 外星艦隊方向標誌:1向右,-1向左
# 也可以用如left, right之類的標誌,但這樣會很麻煩
self.fleet_direction = 1
複製程式碼
3.2 修改alien.py模組
我們需要在Alien
類中新增兩個方法,一個用於檢測窗體邊緣,一個用於更新Alien
物件:
class Alien(Sprite):
-- snip --
def check_edges(self):
"""如果外星人位於螢幕邊緣則返回True"""
screen_rect = self.screen.get_rect()
return self.rect.right >= screen_rect.right or self.rect.left <= 0
def update(self):
"""向右移動外星人"""
# 以後這樣的方式會用的很多
self.x += self.ai_settings.alien_speed_factor * self.ai_settings.fleet_direction
self.rect.x = self.x
複製程式碼
如果使用文字值來控制方向,那就需要新增if-else
語句來檢測艦隊移動方向。鑑於只有兩個可能的方向,這裡使用-1
和1
來表示,這樣更容易改變外星人物件的座標。
3.3 修改game_functions.py模組
首先,我們在該模組中新增一個更新外星艦隊的函式update_aliens()
:
def update_aliens(ai_settings, aliens):
"""檢查是否有外星人位於螢幕邊緣,並更新外星艦隊中所有外星人的位置"""
check_fleet_edges(ai_settings, aliens)
aliens.update() # “一鍵更新”
複製程式碼
check_fleet_edges()
函式用於檢測艦隊是否碰到了窗體邊緣,程式碼如下:
def check_fleet_edges(ai_settings, aliens):
"""有外星人到達邊緣時採取相應的措施"""
# 檢測艦隊中每一個外星人是否碰到了窗體邊緣
for alien in aliens.sprites():
if alien.check_edges():
change_fleet_direction(ai_settings, aliens)
break
複製程式碼
change_fleet_direction()
函式用於改變艦隊的移動方向,以及讓艦隊向下移動,程式碼如下:
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
複製程式碼
上面三個函式就是在game_functions.py
中的所有變動。
3.4 修改alien_invasion.py模組
在該模組中我們只需要在while
迴圈中新增一行程式碼:
# 開始遊戲的主迴圈
while True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
gf.update_bullets(bullets)
# 新增對外星艦隊的修改
gf.update_aliens(ai_settings, aliens)
gf.update_screen(ai_settings, screen, ship, bullets, aliens)
複製程式碼
最後執行主程式,得到如下效果:
截了一張靜態圖,實際是動態的。
4. 擊殺外星人
對於當前的程式,如果發射子彈,子彈將穿過外星人,而不是擊殺,下面我們繼續完善該專案,使其能擊殺外星人。而要實現這一點,關鍵就是要檢測到子彈影象與外星人影象是否重疊,重疊了則表示擊中。
4.1 修改game_functions.py
為何檢測子彈與衛星人的碰撞,我們需要修改update_bullets()
函式,這裡我們增加了update_bullets()
的引數,還呼叫了一個新函式:
def update_bullets(bullets, aliens, ship, screen, ai_settings):
-- snip --
check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets)
複製程式碼
函式check_bullet_alien_collisions()
用於檢測子彈與外星人的碰撞,當外星人被消滅光時,清空現有子彈,並生成新的外星艦隊,它的程式碼如下:
def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets):
"""檢測是否有子彈擊中了外星人,如果有,就刪除相應的子彈和外星人"""
# 下一篇中我們將用該變數實現分數統計
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
#如果外星人被消滅光,則生成新的外星人艦隊
if len(aliens) == 0:
# 刪除現有的子彈並建立新的艦隊
bullets.empty()
create_fleet(ai_settings, screen, ship, aliens)
複製程式碼
sprite.groupcollide()
方法用於檢測物件之間的碰撞,它將bullets
中的每個子彈的rect
與aliens
中的每個外星人的rect
進行比較,並返回一個字典。該字典以第一個引數中的物件為鍵,以第二個引數中的鍵為值,在這裡,以bullets
中發生了碰撞的bullet
為鍵,它的值為與之碰撞的alien
(不是aliens
)!第三個參數列示是否刪除第一個引數中發生了碰撞的物件,而四個參數列示是否刪除第二個引數中發生了碰撞的物件。
4.2 修改alien_invasion.py
只需要修改呼叫update_bullets()
函式的那行程式碼即可,增加幾個引數:
gf.update_bullets(bullets, aliens, ship, screen, ai_settings)
複製程式碼
基礎功能基本完成。
4.3 測試技巧補充
對於上述程式碼,我們可能需要測試當消滅完外星人後,新的艦隊是否能被正確建立等,如果我們以現在遊戲的設定,即子彈速度為1,子彈寬度為3,那測試起來將會很痛苦。此時,我們可以修改修改遊戲的引數,比如將子彈寬度修改為300,子彈速度修改為3,這樣就相當於對遊戲進行了快進,此時程式碼的執行效果如下:
不過最後記得將引數修改回去。
5. 結束遊戲
接下來我們實現外星人碰到飛船,外星人抵達窗體底部,飛船數用光導致遊戲結束的程式碼。
5.1 建立GameStats類
首先我們建立一個用於儲存遊戲資訊的GameStats
類,存放在game_stats.py
檔案中:
class GameStats:
"""跟蹤遊戲的統計資訊"""
def __init__(self, ai_settings):
"""初始化統計資訊"""
# 用於控制遊戲啟動與否
self.game_active = True
self.ai_settings = ai_settings
self.reset_stats()
def reset_stats(self):
"""初始化在遊戲執行期間可能變化的統計資訊"""
# 重置飛船數
self.ships_left = self.ai_settings.ship_limit
複製程式碼
5.2 修改settings.py
從上述程式碼可以看出,我們需要在settings.py
中新增一項表示“飛船數”的資訊:
class Settings:
def __init__(self):
"""初始化遊戲的設定"""
# 螢幕設定
-- snip --
# 設定飛船數量限制
self.ship_limit = 3
-- snip --
複製程式碼
5.3 響應飛船與外星人的碰撞,修改game_functions.py
我們在更新每個外星人的位置後立即檢測外星人和飛船之間的碰撞,隨後再檢查外星人是否到達了窗體底部。修改update_aliens()
函式,使用sprite
中的spritecollideany()
方法來檢測碰撞:將第二引數中的每一個元素與第一個引數比較,檢測是否碰撞,返回第二個引數中第一個發生碰撞的物件,如果沒有發生碰撞則返回None
:
# 增加了引數和碰撞檢測
def update_aliens(ai_settings, aliens, ship, screen, bullets, stats):
-- snip --
# 檢測外星人和飛船之間的碰撞
if pygame.sprite.spritecollideany(ship, aliens):
ship_hit(ai_settings, stats, screen, ship, aliens, bullets)
check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets)
複製程式碼
為此我們需要增加兩個函式:
ship_hit()
:當外星人與飛船發生碰撞時,呼叫次函式
-- snip --
from time import sleep
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
"""響應被外星人撞到的飛船"""
# 將ship_left減1
if stats.ships_left > 0:
stats.ships_left -= 1
# 清空外星人列表和子彈列表
aliens.empty()
bullets.empty()
# 建立一群新的外星人,並將飛船恢復到初始位置
create_fleet(ai_settings, screen, ship, aliens)
ship.center_ship()
# 暫停
sleep(0.5)
else:
stats.game_active = False
複製程式碼
從上面的程式碼還可以看出,我們還需要在Ship
類中新增一個center_ship()
方法:
def center_ship(self):
"""讓飛船在螢幕上居中"""
self.center = self.screen_rect.centerx
複製程式碼
check_aliens_bottom()
: 當飛船到達窗體底部時呼叫次函式
def check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets):
"""檢測是否有外星人到達了螢幕底部"""
screen_rect = screen.get_rect()
for alien in aliens.sprites():
if alien.rect.bottom >= screen_rect.bottom:
# 和飛船被碰撞是的程式碼沒啥區別,故呼叫同一個函式
ship_hit(ai_settings, stats, screen, ship, aliens, bullets)
break
複製程式碼
5.4 修改主程式alien_invasion.py
修改遊戲的迴圈部分:
# 開始遊戲的主迴圈
while True:
gf.check_events(ai_settings, screen, ship, bullets)
# 決定程式執行時該執行的部分
if stats.game_active:
ship.update()
gf.update_bullets(bullets, aliens, ship, screen, ai_settings)
gf.update_aliens(ai_settings, aliens, ship, screen, bullets, stats)
gf.update_screen(ai_settings, screen, ship, bullets, aliens)
複製程式碼
在主迴圈中,任何情況下都需要呼叫check_events()
,即使遊戲處於非活動狀態;還需要不斷更新螢幕,以便在等待玩家是否選擇重新開始遊戲時能夠修改螢幕;其他函式僅在遊戲處於活動狀態時太需要呼叫。
6. 小結
本篇講述了:
- 如何在遊戲中新增大量相同的元素;
- 如何用巢狀迴圈來建立元素網格;
- 如何控制物件在螢幕上移動的方向以及響應事件;
- 如何檢測和響應元素碰撞;
- 如何在遊戲中跟蹤統計資訊;
- 如何使用標誌
game_active
來判斷遊戲是否結束。
下一篇中,同時也是本專案的最後一篇,我們將:
- 新增一個Play按鈕讓玩家能夠開始遊戲,以及遊戲結束後再開始;
- 每當玩家消滅一群外星人後,加快遊戲節奏;
- 新增一個分數系統。
迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~