這一節中,我們將把前面學到的內容集中在一起(包括動畫精靈、碰撞檢測和事件),建立一個簡單的「球拍與球」遊戲,類似於 Pong。
先來看一個簡單的單機版本。我們的遊戲需要:
一個來回反彈的球;
一個打球的球拍;
一種控制球拍的方法;
一種記錄分數並在窗口上顯示分數的方法;
一種確定有幾條「命」的方法——你有幾次機會。
我們將在構建程序過程中逐個分析以上的需求。
從前的美好時光
Pong 是最早人們在家裡玩的視頻遊戲之一。原來的 Pong 遊戲沒有任何軟件——只是一堆電路!那時還沒有家用計算機。Pong 要插入到你的電視上,你要用操縱桿來控制「球拍」。下面是這個遊戲在電視屏幕上的效果圖:
很少有人知道的秘密:
奶奶不僅是一個 Pong 遊戲高手,還是乒乓球世界冠軍呢!
球
我們之前使用的沙灘球對於 Pong 遊戲來說有點大。我們需要小一點的球。卡特和我為這個遊戲想出了這個有些滑稽的網球小人:
嘿,如果你被球拍打來打去,也會嚇得夠嗆!
我們將在這個遊戲中使用動畫精靈,所以需要為我們的球建立一個精靈,然後為它創建一個實例。我們將使用包含 __init__
和 move
方法的 Ball
類。
創建球的實例時,我們會告訴它使用哪個圖像、球的速度以及球的起始位置:
myBall = MyBallClass(\'wackyball.bmp\', ball_speed, [50, 50])
還需要把這個球增加到一個組,以便完成球和球拍之間的碰撞檢測。可以創建組,同時把球增加到這個組:
ballGroup = pygame.sprite.Group(myBall)
球拍
對於球拍,我們仍然堅持 Pong 遊戲的傳統,只是使用一個簡單的矩形。我們將要使用一個白色背景,所以把球拍創建為一個黑色矩形。也要為球拍建立一個精靈類和實例:
注意,對於球拍,我們並沒有加載圖像文件:這裡只是用黑色填充一個矩形表面來創建一個圖像。不過,每個精靈都需要一個 image
屬性,所以我們使用 Surface.convert
方法把表面轉換為一個圖像。
這個球拍只能左右移動,不能上下移動。我們讓球拍的 x 位置(它的左右位置)跟著鼠標移動,所以用戶可以用鼠標來控制球拍。因為這個工作在事件循環中完成,所以球拍不需要一個單獨的 move
方法。
控制球拍
上一節已經提到過,我們將用鼠標控制球拍。這裡要使用 MOUSEMOTION
事件,這說明只要鼠標在 Pygame 窗口內部移動,球拍就會移動。由於鼠標在 Pygame 窗口內時 Pygame 才能「看到」鼠標,所以球拍會自動限制在窗口的邊界以內。我們將讓球拍的中心跟隨鼠標移動。
代碼應當像這樣:
elif event.type == pygame.MOUSEMOTION: paddle.rect.centerx = event.pos[0]
event.pos
是一個列表,包含鼠標位置的 [x, y]
值。所以 event.pos[0]
會提供鼠標移動時的 x 位置。當然,如果鼠標在左邊界或右邊界上,球拍會有一半在窗口之外,不過這是可以的。
還需要最後一點:球和球拍之間的碰撞檢測。我們就是利用這種「碰撞」才能用球拍「打」球。出現碰撞時,只需讓球的 y 速度反向(所以如果球在向下走,碰到球拍時它會反彈,開始向上移動)。代碼如下:
if pygame.sprite.spritecollide(paddle, ballGroup, False): myBall.speed[1] = -myBall.speed[1]
還要記住每次循環時都要重繪。如果把這些內容都集中在一起,就得到了一個非常基本的類似 Pong 的程序。代碼清單 18-4 給出了(至今為止)完整的代碼。
代碼清單 18-4 PyPong 的第一個版本
運行這個程序時應該能得到下面的結果。
也許吧,這可能不是最讓人興奮的遊戲,不過我們只是剛剛起步,才開始在 Pygame 中編寫遊戲。下面再向我們的 PyPong 遊戲加些東西。
記錄分數並用 pygame.font
顯示
我們要跟蹤兩個方面:還有幾條命以及得了多少分。為了力求簡單,每次球碰到窗口頂邊時我們會給 1 分。另外給每個玩家 3 條命。
還需要一種方法來顯示這個分數。Pygame 使用一個名為 font
的模塊顯示文本。可以這樣來使用。
建立一個
font
對象,告訴 Pygame 你想要的字體樣式和大小。渲染文本,向字體對像傳入一個字符串,它會返回一個繪製有這個文本的新的表面。
把這個表面塊移到顯示表面。
術語箱
計算機圖形學中,渲染(render)是指繪製某個東西,或者讓它可見。
在這裡,字符串就是玩家的分數(不過首先必須把它從一個 int
轉換為一個 string
)。
我們需要類似下面的代碼,要放在代碼清單 18-4 中的事件循環前面(而且要在 paddle=MyPaddleClass([270,400])
代碼行後面):
第一行中的第一個參數(這裡是 None
)可以告訴 Pygame 我們希望使用什麼字體(類型樣式)。通過傳入 None
,就是在告訴 Pygame 要使用一個默認字體。
然後,在事件循環內部,我們需要這樣的代碼:
這樣每次循環時都會重繪分數文本。
當然了,卡特,我們還沒有創建 points
變量(我正打算這麼做呢)。在創建 font
對象的代碼前面增加這樣一行代碼:
score = 0
現在,要跟蹤分數……因為我們已經用球的 move
方法檢測了球什麼時候碰到窗口的頂邊(來完成反彈),所以只需要在這裡再增加幾行:
Traceback (most recent call last): File \"C:...\", line 59, in <module>myBall.move File \"C:...\", line 24, in movescore = score + 1UnboundLocalError: local variable \'score\'referenced before assignment
唉呀!我們忘記命名空間的問題了。還記得第 15 章中那個又大又長的解釋嗎?現在可以看到命名空間的一個實際例子了。儘管我們確實有一個名為 score
的變量,但是這裡試圖從 Ball
類的 move
方法中使用這個變量。這個類在尋找一個名為 score
的局部變量,而這個局部變量並不存在。實際上,我們希望使用先前已經創建的全局變量,所以只需要告訴 move
方法使用全局變量 score
,如下:
def move(self): global score
還要讓 score_font
(分數的 font
對像)和 score_surf
(包含渲染文本的表面塊)作為全局變量,因為它們是用 move
方法更新的。所以代碼實際上應當像這樣:
def move(self): global score, score_font, score_surf
現在應該能正常工作了!再試試看。應該能看到窗口左上角的分數,而且當你把球彈到窗口頂邊時這個分數應該會增加。
跟蹤還有幾條命
現在來跟蹤還有幾條命。對目前來說,如果漏了球,它就會從窗口底邊掉下去,再也看不到了。我們希望給玩家 3 條命或者 3 個機會,所以下面建立一個名為 lives
的變量,把它設置為 3。
lives = 3
玩家漏了球而且球掉到窗口底邊後,要將 lives
減 1,等待幾秒,然後重新開始,又提供一個新球:
if myBall.rect.top >= screen.get_rect.bottom: lives = lives - 1 pygame.time.delay(2000) myBall.rect.topleft = [50, 50]
這個代碼要放在 while
循環中。順便說一句,為什麼對於球我們會寫成 myBall.rect
,而對於 screen
要寫為 get_rect
呢?這有下面幾個原因。
myBall
是一個動畫精靈,動畫精靈都包含一個rect
。screen
是一個表面,而表面不包含rect
。可以用get_rect
函數找到包圍一個表面的rect
。
如果做了上述修改,並運行程序,你會看到玩家現在有 3 條命。
增加一個生命計數器
很多遊戲會給玩家多條命,大多數這樣的遊戲都會採用某種方法顯示還剩下幾條命。我們這個遊戲也可以做到這一點。
一種簡單的方法是顯示一些球,剩幾條命就顯示幾個球。可以把這些球放在右上角。以下是畫出生命計數器的 for
循環中使用的小公式:
for i in range (lives): width = screen.get_rect.width screen.blit(myBall.image, [width - 40 * i, 20])
這個代碼也要放在主 while
循環中,應當放在事件循環前面(但要在 screen.blit(score_text, textpos)
代碼行之後)。
遊戲結束
最後還需要增加一點:當玩家丟掉最後一條命時要顯示一個「遊戲結束」的消息。我們要建立兩個字體對象,分別包含我們的消息和玩家的最後分數,渲染這兩個文本(創建繪有文本的表面),再將這些表面塊移到 screen
。
另外還要在最後一局結束後避免球再次出現。為了做到這一點,要建立一個 done
變量告訴我們何時遊戲結束。運行在主 while
循環中的以下代碼會完成這項工作。
把所有這些內容集中在一起,可以得到最終的 PyPong 程序,如代碼清單 18-5 所示。
代碼清單 18-5 最終的 PyPong 代碼
如果運行代碼清單 18-5 中的代碼,應該能看到這樣的結果。
如果在編輯器中注意觀察,可以看到這大約有 75 行代碼(加上一些空行)。這是目前為止我們創建的最大的程序了,雖然運行時看起來很簡單,但卻包含了豐富的內容。
下一章,我們將要學習 Pygame 中的聲音,另外還會向這個 PyPong 遊戲添加一些聲音。
你學到了什麼
在這一章,你學到了以下內容。
事件。
Pygame 事件循環。
事件處理。
鍵盤事件。
鼠標事件。
定時器事件(以及用戶事件類型)。
pygame.font
(用於向 Pygame 程序添加文本)。把所有內容集中在一起建立一個遊戲!
測試題
1. 程序可以響應哪兩種事件?
2. 處理事件的代碼叫什麼?
3. Pygame 檢測按鍵時使用的事件類型名是什麼?
4. MOUSEMOVE
事件的哪個屬性指出了鼠標位於窗口的哪個位置?
5. 如何找出 Pygame 中下一個可用的事件編號(例如,如果你想添加一個用戶事件)?
6. 如何創建一個定時器在 Pygame 中生成定時器事件?
7. 在 Pygame 窗口中顯示文本時要使用什麼對像?
8. 要讓文本出現在一個 Pygame 窗口中,需要哪 3 個步驟?
動手試一試
1. 如果球沒有碰到球拍的頂邊,而是碰到了球拍的左右兩邊,有沒有什麼奇怪的現象發生?它會在球拍中間持續反彈一段時間。你明白這是為什麼嗎?你能解決這個問題嗎?我在後面的答案中給出了一個解決方案,不過在看答案之前你自己先試試看。
2. 試著重寫這個程序(代碼清單 18-4 或代碼清單 18-5),讓球的反彈有點隨機性。可以改變球在球拍或牆上反彈的方式,使用隨機的速度,或者也可以採用你能想到的其他做法。(我們在第 15 章見過 random.randint
和 random.random
,所以你應該知道如何生成隨機數,包括整數和浮點數。)