讀古今文學網 > 父與子的編程之旅:與小卡特一起學Python > 18.5 另一個遊戲PyPong >

18.5 另一個遊戲PyPong

這一節中,我們將把前面學到的內容集中在一起(包括動畫精靈、碰撞檢測和事件),建立一個簡單的「球拍與球」遊戲,類似於 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.randintrandom.random,所以你應該知道如何生成隨機數,包括整數和浮點數。)