spring boot 網(wǎng)站開發(fā)網(wǎng)站編輯
十九、碰撞檢測(cè)
原文:
inventwithpython.com/invent4thed/chapter19.html
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
碰撞檢測(cè)涉及確定屏幕上的兩個(gè)物體何時(shí)相互接觸(即發(fā)生碰撞)。碰撞檢測(cè)對(duì)于游戲非常有用。例如,如果玩家觸碰到敵人,他們可能會(huì)失去生命值?;蛘呷绻婕矣|碰到硬幣,他們應(yīng)該自動(dòng)撿起它。碰撞檢測(cè)可以幫助確定游戲角色是否站在堅(jiān)實(shí)的地面上,或者他們腳下只有空氣。
在我們的游戲中,碰撞檢測(cè)將確定兩個(gè)矩形是否重疊。本章的示例程序?qū)⒑w這種基本技術(shù)。我們還將看看我們的pygame
程序如何通過鍵盤和鼠標(biāo)接受玩家的輸入。這比我們?yōu)槲谋境绦蛩龅恼{(diào)用input()
函數(shù)要復(fù)雜一些。但在 GUI 程序中使用鍵盤要更加互動(dòng),而在我們的文本游戲中甚至無法使用鼠標(biāo)。這兩個(gè)概念將使您的游戲更加令人興奮!
本章涵蓋的主題
-
Clock
對(duì)象 -
pygame
中的鍵盤輸入 -
pygame
中的鼠標(biāo)輸入 -
碰撞檢測(cè)
-
在迭代列表時(shí)不修改列表
碰撞檢測(cè)程序的示例運(yùn)行
在這個(gè)程序中,玩家使用鍵盤的箭頭鍵在屏幕上移動(dòng)一個(gè)黑色的方塊。較小的綠色方塊代表食物,出現(xiàn)在屏幕上,方塊觸碰到它們時(shí)會(huì)“吃”掉它們。玩家可以在窗口的任何地方點(diǎn)擊以創(chuàng)建新的食物方塊。此外,按 ESC 鍵退出程序,按 X 鍵將玩家傳送到屏幕上的隨機(jī)位置。
圖 19-1 顯示了程序完成后的樣子。
圖 19-1: pygame 碰撞檢測(cè)程序的屏幕截圖
碰撞檢測(cè)程序的源代碼
開始一個(gè)新文件,輸入以下代碼,然后將其保存為collisionDetection.py。如果在輸入此代碼后出現(xiàn)錯(cuò)誤,請(qǐng)使用在線 diff 工具將您輸入的代碼與本書代碼進(jìn)行比較,網(wǎng)址為www.nostarch.com/inventwithpython#diff
。
collision Detection.py
import pygame, sys, random
from pygame.locals import *# Set up pygame.
pygame.init()
mainClock = pygame.time.Clock()# Set up the window.
WINDOWWIDTH = 400
WINDOWHEIGHT = 400
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),0, 32)
pygame.display.set_caption('Collision Detection')# Set up the colors.
BLACK = (0, 0, 0)
GREEN = (0, 255, 0)
WHITE = (255, 255, 255)# Set up the player and food data structures.
foodCounter = 0
NEWFOOD = 40
FOODSIZE = 20
player = pygame.Rect(300, 100, 50, 50)
foods = []
for i in range(20):foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - FOODSIZE),random.randint(0, WINDOWHEIGHT - FOODSIZE), FOODSIZE, FOODSIZE))# Set up movement variables.
moveLeft = False
moveRight = False
moveUp = False
moveDown = FalseMOVESPEED = 6# Run the game loop.
while True:# Check for events.for event in pygame.event.get():if event.type == QUIT:pygame.quit()sys.exit()if event.type == KEYDOWN:# Change the keyboard variables.if event.key == K_LEFT or event.key == K_a:moveRight = FalsemoveLeft = Trueif event.key == K_RIGHT or event.key == K_d:moveLeft = FalsemoveRight = Trueif event.key == K_UP or event.key == K_w:moveDown = FalsemoveUp = Trueif event.key == K_DOWN or event.key == K_s:moveUp = FalsemoveDown = Trueif event.type == KEYUP:if event.key == K_ESCAPE:pygame.quit()sys.exit()if event.key == K_LEFT or event.key == K_a:moveLeft = Falseif event.key == K_RIGHT or event.key == K_d:moveRight = Falseif event.key == K_UP or event.key == K_w:moveUp = Falseif event.key == K_DOWN or event.key == K_s:moveDown = Falseif event.key == K_x:player.top = random.randint(0, WINDOWHEIGHT -player.height)player.left = random.randint(0, WINDOWWIDTH -player.width)if event.type == MOUSEBUTTONUP:foods.append(pygame.Rect(event.pos[0], event.pos[1],FOODSIZE, FOODSIZE))foodCounter += 1if foodCounter >= NEWFOOD:# Add new food.foodCounter = 0foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH -FOODSIZE), random.randint(0, WINDOWHEIGHT - FOODSIZE),FOODSIZE, FOODSIZE))# Draw the white background onto the surface.windowSurface.fill(WHITE)# Move the player.if moveDown and player.bottom < WINDOWHEIGHT:player.top += MOVESPEEDif moveUp and player.top > 0:player.top -= MOVESPEEDif moveLeft and player.left > 0:player.left -= MOVESPEEDif moveRight and player.right < WINDOWWIDTH:player.right += MOVESPEED# Draw the player onto the surface.pygame.draw.rect(windowSurface, BLACK, player)# Check whether the player has intersected with any food squares.for food in foods[:]:if player.colliderect(food):foods.remove(food)# Draw the food.for i in range(len(foods)):pygame.draw.rect(windowSurface, GREEN, foods[i])# Draw the window onto the screen.pygame.display.update()mainClock.tick(40)
導(dǎo)入模塊
pygame
碰撞檢測(cè)程序?qū)肓伺c第 18 章中的動(dòng)畫程序相同的模塊,還有random
模塊:
import pygame, sys, random
from pygame.locals import *
使用時(shí)鐘來控制程序的節(jié)奏
第 5 到 17 行大部分做的事情與動(dòng)畫程序相同:它們初始化了pygame
,設(shè)置了WINDOWHEIGHT
和WINDOWWIDTH
,并分配了顏色和方向常量。
然而,第 6 行是新的:
mainClock = pygame.time.Clock()
在動(dòng)畫程序中,調(diào)用time.sleep(0.02)
會(huì)減慢程序的運(yùn)行速度,以防止它運(yùn)行得太快。雖然這個(gè)調(diào)用在所有計(jì)算機(jī)上都會(huì)暫停 0.02 秒,但程序的其余部分的速度取決于計(jì)算機(jī)的速度。如果我們希望這個(gè)程序在任何計(jì)算機(jī)上以相同的速度運(yùn)行,我們需要一個(gè)函數(shù),在快速計(jì)算機(jī)上暫停時(shí)間更長(zhǎng),在慢速計(jì)算機(jī)上暫停時(shí)間更短。
pygame.time.Clock
對(duì)象可以在任何計(jì)算機(jī)上暫停適當(dāng)?shù)臅r(shí)間。第 110 行在游戲循環(huán)內(nèi)調(diào)用了mainClock.tick(40)
。對(duì)Clock
對(duì)象的tick()
方法的調(diào)用等待足夠的時(shí)間,以便它以大約 40 次迭代每秒的速度運(yùn)行,無論計(jì)算機(jī)的速度如何。這確保游戲永遠(yuǎn)不會(huì)比您預(yù)期的速度更快。對(duì)tick()
的調(diào)用應(yīng)該只出現(xiàn)一次在游戲循環(huán)中。
設(shè)置窗口和數(shù)據(jù)結(jié)構(gòu)
第 19 到 22 行設(shè)置了一些用于在屏幕上出現(xiàn)的食物方塊的變量:
# Set up the player and food data structures.
foodCounter = 0
NEWFOOD = 40
FOODSIZE = 20
foodCounter
變量將從值0
開始,NEWFOOD
為40
,FOODSIZE
為20
。稍后我們將看到這些變量在創(chuàng)建食物時(shí)如何使用。
第 23 行設(shè)置了玩家位置的pygame.Rect
對(duì)象:
player = pygame.Rect(300, 100, 50, 50)
player
變量有一個(gè)pygame.Rect
對(duì)象,表示方塊的大小和位置。玩家的方塊將像動(dòng)畫程序中的方塊一樣移動(dòng)(參見“移動(dòng)每個(gè)方塊”在第 280 頁(yè)),但在這個(gè)程序中,玩家可以控制方塊的移動(dòng)方向。
接下來,我們?cè)O(shè)置了一些代碼來跟蹤食物方塊:
foods = []
for i in range(20):foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - FOODSIZE),random.randint(0, WINDOWHEIGHT - FOODSIZE), FOODSIZE, FOODSIZE))
程序?qū)⑹褂?code>foods列表來跟蹤每個(gè)食物方塊的Rect
對(duì)象。第 25 和 26 行在屏幕周圍隨機(jī)放置了 20 個(gè)食物方塊。您可以使用random.randint()
函數(shù)來生成隨機(jī)的 x 和 y 坐標(biāo)。
在第 26 行,程序調(diào)用pygame.Rect()
構(gòu)造函數(shù)來返回一個(gè)新的pygame.Rect
對(duì)象。它將表示一個(gè)新食物方塊的位置和大小。pygame.Rect()
的前兩個(gè)參數(shù)是左上角的 x 和 y 坐標(biāo)。您希望隨機(jī)坐標(biāo)在0
和窗口大小減去食物方塊大小之間。如果將隨機(jī)坐標(biāo)設(shè)置在0
和窗口大小之間,那么食物方塊可能會(huì)被推到窗口之外,就像圖 19-2 中一樣。
pygame.Rect()
的第三個(gè)和第四個(gè)參數(shù)是食物方塊的寬度和高度。寬度和高度都是FOODSIZE
常量中的值。
圖 19-2:對(duì)于 400×400 窗口中的 100×100 方塊,將左上角設(shè)置為 400 會(huì)將矩形放在窗口外。要在內(nèi)部,左邊緣應(yīng)該設(shè)置為 300。
pygame.Rect()
的第三個(gè)和第四個(gè)參數(shù)是食物方塊的寬度和高度。寬度和高度都是FOODSIZE
常量中的值。
設(shè)置跟蹤移動(dòng)的變量
從第 29 行開始,代碼設(shè)置了一些變量,用于跟蹤玩家方塊的移動(dòng)方向:
# Set up movement variables.
moveLeft = False
moveRight = False
moveUp = False
moveDown = False
這四個(gè)變量具有布爾值,用于跟蹤哪個(gè)箭頭鍵被按下,并最初設(shè)置為False
。例如,當(dāng)玩家按下鍵盤上的左箭頭鍵移動(dòng)方塊時(shí),moveLeft
被設(shè)置為True
。當(dāng)他們松開鍵時(shí),moveLeft
被設(shè)置回False
。
第 34 到 43 行幾乎與以前的pygame
程序中的代碼相同。這些行處理游戲循環(huán)的開始以及玩家退出程序時(shí)的操作。我們將跳過對(duì)此代碼的解釋,因?yàn)槲覀冊(cè)谏弦徽轮幸呀?jīng)涵蓋過了。
處理事件
pygame
模塊可以根據(jù)鼠標(biāo)或鍵盤的用戶輸入生成事件。以下是pygame.event.get()
可以返回的事件:
QUIT
當(dāng)玩家關(guān)閉窗口時(shí)生成。
KEYDOWN
當(dāng)玩家按下鍵時(shí)生成。具有key
屬性,告訴按下了哪個(gè)鍵。還有一個(gè)mod
屬性,告訴按下該鍵時(shí)是否按下了 SHIFT、CTRL、ALT 或其他鍵。
KEYUP
當(dāng)玩家釋放鍵時(shí)生成。具有與KEYDOWN
類似的key
和mod
屬性。
MOUSEMOTION
:每當(dāng)鼠標(biāo)在窗口上移動(dòng)時(shí)生成。具有pos
屬性(縮寫為position),返回窗口中鼠標(biāo)位置的元組(x, y)
。rel
屬性還返回一個(gè)(x, y)
元組,但它給出自上一個(gè)MOUSEMOTION
事件以來的相對(duì)坐標(biāo)。例如,如果鼠標(biāo)從(200, 200)
向左移動(dòng) 4 像素到(196, 200)
,那么rel
將是元組值(-4, 0)
。button
屬性返回一個(gè)三個(gè)整數(shù)的元組。元組中的第一個(gè)整數(shù)是左鼠標(biāo)按鈕,第二個(gè)整數(shù)是中間鼠標(biāo)按鈕(如果存在),第三個(gè)整數(shù)是右鼠標(biāo)按鈕。如果鼠標(biāo)移動(dòng)時(shí)它們沒有被按下,則這些整數(shù)將為0
,如果它們被按下,則為1
。
MOUSEBUTTONDOWN
:當(dāng)鼠標(biāo)在窗口中按下按鈕時(shí)生成。此事件具有pos
屬性,它是鼠標(biāo)按下按鈕時(shí)鼠標(biāo)位置的(x, y)
元組。還有一個(gè)button
屬性,它是從1
到5
的整數(shù),告訴哪個(gè)鼠標(biāo)按鈕被按下,如表 19-1 中所述。
MOUSEBUTTONUP
:當(dāng)鼠標(biāo)按鈕釋放時(shí)生成。這與MOUSEBUTTONDOWN
具有相同的屬性。
當(dāng)生成MOUSEBUTTONDOWN
事件時(shí),它具有button
屬性。button
屬性是與鼠標(biāo)可能具有的不同類型的按鈕相關(guān)聯(lián)的值。例如,左鍵的值為1
,右鍵的值為3
。表 19-1 列出了鼠標(biāo)事件的所有button
屬性,但請(qǐng)注意,鼠標(biāo)可能沒有這里列出的所有button
值。
表 19-1: button
屬性值
button 的值 | 鼠標(biāo)按鈕 |
---|---|
1 | 左鍵 |
2 | 中鍵 |
3 | 右鍵 |
4 | 滾輪向上滾動(dòng) |
5 | 滾輪向下滾動(dòng) |
我們將使用這些事件來讓玩家使用KEYDOWN
事件和鼠標(biāo)按鈕點(diǎn)擊來控制框。
處理 KEYDOWN 事件
處理按鍵和釋放事件的代碼從第 44 行開始;它包括KEYDOWN
事件類型:
if event.type == KEYDOWN:
如果事件類型是KEYDOWN
,則Event
對(duì)象具有一個(gè)key
屬性,指示按下了哪個(gè)鍵。當(dāng)玩家按下箭頭鍵或 WASD 鍵(發(fā)音為wazz-dee,這些鍵與箭頭鍵的布局相同,但位于鍵盤左側(cè))時(shí),我們希望移動(dòng)框。我們將使用if
語(yǔ)句來檢查按下的鍵,以便確定框應(yīng)該移動(dòng)的方向。
第 46 行將key
屬性與K_LEFT
和K_a
進(jìn)行比較,它們是表示鍵盤上左箭頭鍵和 WASD 中 A 的pygame.locals
常量。第 46 至 57 行檢查每個(gè)箭頭和 WASD 鍵:
# Change the keyboard variables.if event.key == K_LEFT or event.key == K_a:moveRight = FalsemoveLeft = Trueif event.key == K_RIGHT or event.key == K_d:moveLeft = FalsemoveRight = Trueif event.key == K_UP or event.key == K_w:moveDown = FalsemoveUp = Trueif event.key == K_DOWN or event.key == K_s:moveUp = FalsemoveDown = True
當(dāng)按下這些鍵之一時(shí),代碼告訴 Python 將相應(yīng)的移動(dòng)變量設(shè)置為True
。Python 還會(huì)將相反方向的移動(dòng)變量設(shè)置為False
。
例如,當(dāng)按下左箭頭鍵時(shí),程序執(zhí)行第 47 和 48 行。在這種情況下,Python 將moveLeft
設(shè)置為True
,moveRight
設(shè)置為False
(即使moveRight
可能已經(jīng)是False
,Python 也會(huì)將其設(shè)置為False
,以確保)。
在第 46 行,event.key
可以等于K_LEFT
或K_a
。如果按下左箭頭鍵,則event.key
中的值將設(shè)置為與K_LEFT
相同的值,如果按下 A 鍵,則設(shè)置為與K_a
相同的值。
通過執(zhí)行第 47 和 48 行的代碼,如果按鍵是K_LEFT
或K_a
,則左箭頭鍵和 A 鍵將執(zhí)行相同的操作。W、A、S 和 D 鍵用作更改移動(dòng)變量的替代鍵,讓玩家可以使用左手而不是右手。您可以在圖 19-3 中看到這兩組鍵的示例。
圖 19-3:WASD 鍵可以編程為與箭頭鍵執(zhí)行相同的操作。
字母和數(shù)字鍵的常量很容易找到:A 鍵的常量是K_a
,B 鍵的常量是K_b
,依此類推。3 鍵的常量是K_3
。表 19-2 列出了其他鍵盤鍵的常用常量變量。
**表 19-2:**鍵盤鍵的常量變量
pygame 常量變量 | 鍵盤鍵 |
---|---|
K_LEFT | 左箭頭 |
K_RIGHT | 右箭頭 |
K_UP | 上箭頭 |
K_DOWN | 下箭頭 |
K_ESCAPE | ESC |
K_BACKSPACE | 退格鍵 |
K_TAB | TAB |
K_RETURN | RETURN 或 ENTER |
K_SPACE | 空格鍵 |
K_DELETE | DEL |
K_LSHIFT | 左 SHIFT |
K_RSHIFT | 右 SHIFT |
K_LCTRL | 左 CTRL |
K_RCTRL | 右 CTRL |
K_LALT | 左 ALT |
K_RALT | 右 ALT |
K_HOME | HOME |
K_END | END |
K_PAGEUP | PGUP |
K_PAGEDOWN | PGDN |
K_F1 | F1 |
K_F2 | F2 |
K_F3 | F3 |
K_F4 | F4 |
K_F5 | F5 |
K_F6 | F6 |
K_F7 | F7 |
K_F8 | F8 |
K_F9 | F9 |
K_F10 | F10 |
K_F11 | F11 |
K_F12 | F12 |
處理 KEYUP 事件
當(dāng)玩家釋放他們按下的鍵時(shí),將生成一個(gè)KEYUP
事件:
if event.type == KEYUP:
如果玩家釋放的鍵是 ESC,則 Python 應(yīng)終止程序。請(qǐng)記住,在pygame
中,您必須在調(diào)用sys.exit()
函數(shù)之前調(diào)用pygame.quit()
函數(shù),我們?cè)诘?59 到 61 行中這樣做:
if event.key == K_ESCAPE:pygame.quit()sys.exit()
第 62 到 69 行如果釋放了該方向鍵,則將移動(dòng)變量設(shè)置為False
:
if event.key == K_LEFT or event.key == K_a:moveLeft = Falseif event.key == K_RIGHT or event.key == K_d:moveRight = Falseif event.key == K_UP or event.key == K_w:moveUp = Falseif event.key == K_DOWN or event.key == K_s:moveDown = False
通過KEYUP
事件將移動(dòng)變量設(shè)置為False
會(huì)使框停止移動(dòng)。
傳送玩家
您還可以將傳送添加到游戲中。如果玩家按下 X 鍵,則第 71 和 72 行將玩家框的位置設(shè)置為窗口上的隨機(jī)位置:
if event.key == K_x:player.top = random.randint(0, WINDOWHEIGHT -player.height)player.left = random.randint(0, WINDOWWIDTH -player.width)
第 70 行檢查玩家是否按下了 X 鍵。然后,第 71 行設(shè)置一個(gè)隨機(jī)的 x 坐標(biāo),將玩家傳送到窗口的高度減去玩家矩形的高度之間。第 72 行執(zhí)行類似的代碼,但是針對(duì) y 坐標(biāo)。這使玩家可以通過按 X 鍵在窗口周圍傳送,但他們無法控制將傳送到哪里——這是完全隨機(jī)的。
添加新的食物方塊
玩家可以通過兩種方式向屏幕添加新的食物方塊。他們可以單擊窗口中希望新食物方塊出現(xiàn)的位置,或者他們可以等到游戲循環(huán)迭代NEWFOOD
次數(shù),這樣新的食物方塊將在窗口上隨機(jī)生成。
我們首先看一下如何通過玩家的鼠標(biāo)輸入添加食物:
if event.type == MOUSEBUTTONUP:foods.append(pygame.Rect(event.pos[0], event.pos[1],FOODSIZE, FOODSIZE))
鼠標(biāo)輸入與鍵盤輸入一樣通過事件處理。當(dāng)玩家在單擊鼠標(biāo)后釋放鼠標(biāo)按鈕時(shí),將發(fā)生MOUSEBUTTONUP
事件。
第 75 行中,x 坐標(biāo)存儲(chǔ)在event.pos[0]
中,y 坐標(biāo)存儲(chǔ)在event.pos[1]
中。第 75 行創(chuàng)建一個(gè)新的Rect
對(duì)象來表示一個(gè)新的食物方塊,并將其放置在MOUSEBUTTONUP
事件發(fā)生的地方。通過向foods
列表添加新的Rect
對(duì)象,代碼在屏幕上顯示一個(gè)新的食物方塊。
除了可以由玩家自行添加外,食物方塊還可以通過第 77 到 81 行的代碼自動(dòng)生成:
foodCounter += 1if foodCounter >= NEWFOOD:# Add new food.foodCounter = 0foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH -FOODSIZE), random.randint(0, WINDOWHEIGHT - FOODSIZE),FOODSIZE, FOODSIZE))
變量foodCounter
跟蹤應(yīng)添加食物的頻率。每次游戲循環(huán)迭代時(shí),foodCounter
在第 77 行增加1
。
一旦foodCounter
大于或等于常量NEWFOOD
,foodCounter
將被重置,并且通過第 81 行生成一個(gè)新的食物方塊。您可以通過調(diào)整第 21 行上的NEWFOOD
來改變添加新食物方塊的速度。
84 行只是用白色填充窗口表面,我們?cè)凇疤幚硗婕彝顺鰰r(shí)”和第 279 頁(yè)中已經(jīng)討論過了,所以我們將繼續(xù)討論玩家如何在屏幕上移動(dòng)。
在窗口中移動(dòng)玩家
我們已將移動(dòng)變量(moveDown
,moveUp
,moveLeft
和moveRight
)設(shè)置為True
或False
,具體取決于玩家按下了哪些鍵?,F(xiàn)在我們需要移動(dòng)玩家的方框,該方框由存儲(chǔ)在player
中的pygame.Rect
對(duì)象表示。我們將通過調(diào)整player
的 x 和 y 坐標(biāo)來實(shí)現(xiàn)這一點(diǎn)。
# Move the player.if moveDown and player.bottom < WINDOWHEIGHT:player.top += MOVESPEEDif moveUp and player.top > 0:player.top -= MOVESPEEDif moveLeft and player.left > 0:player.left -= MOVESPEEDif moveRight and player.right < WINDOWWIDTH:player.right += MOVESPEED
如果moveDown
設(shè)置為True
(并且玩家的方框底部不在窗口的底部之下),則第 88 行將通過將MOVESPEED
添加到玩家當(dāng)前的top
屬性來向下移動(dòng)玩家的方框。第 89 到 94 行對(duì)其他三個(gè)方向執(zhí)行相同的操作。
在窗口上繪制玩家
第 97 行在窗口上繪制玩家的方框:
# Draw the player onto the surface.pygame.draw.rect(windowSurface, BLACK, player)
在移動(dòng)方框之后,第 97 行將其繪制在新位置。傳遞給第一個(gè)參數(shù)的windowSurface
告訴 Python 在哪個(gè)Surface
對(duì)象上繪制矩形。存儲(chǔ)在BLACK
變量中的(0, 0, 0)
告訴 Python 繪制黑色矩形。存儲(chǔ)在player
變量中的Rect
對(duì)象告訴 Python 要繪制的矩形的位置和大小。
檢查碰撞
在繪制食物方塊之前,程序需要檢查玩家的方框是否與任何方塊重疊。如果是,則需要從foods
列表中刪除該方塊。這樣,Python 就不會(huì)繪制任何盒子已經(jīng)吃掉的食物方塊。
我們將在第 101 行使用所有Rect
對(duì)象都具有的碰撞檢測(cè)方法colliderect()
:
# Check whether the player has intersected with any food squares.for food in foods[:]:if player.colliderect(food):foods.remove(food)
在每次for
循環(huán)迭代中,將foods
(復(fù)數(shù))列表中的當(dāng)前食物方塊放入變量food
(單數(shù))中。pygame.Rect
對(duì)象的colliderect()
方法將玩家矩形的pygame.Rect
對(duì)象作為參數(shù),并在兩個(gè)矩形發(fā)生碰撞時(shí)返回True
,如果它們沒有發(fā)生碰撞,則返回False
。如果為True
,第 102 行將從foods
列表中移除重疊的食物方塊。
不要在迭代列表時(shí)更改列表
請(qǐng)注意,這個(gè)for
循環(huán)與我們以前看到的任何其他for
循環(huán)略有不同。如果您仔細(xì)看第 100 行,它并不是在foods
上進(jìn)行迭代,而是在foods[:]
上進(jìn)行迭代。
記住切片的工作原理。foods[:2]
將計(jì)算列表的副本,其中包含從開頭到(但不包括)索引2
的項(xiàng)目。foods[:]
將為您提供包含從開頭到結(jié)尾的項(xiàng)目的列表的副本。基本上,foods[:]
創(chuàng)建一個(gè)新列表,其中包含foods
中所有項(xiàng)目的副本。這是復(fù)制列表的一種更簡(jiǎn)潔的方法,比如在第 10 章的井字棋游戲中getBoardCopy()
函數(shù)所做的。
在迭代列表時(shí),您不能添加或刪除項(xiàng)目。如果 foods 列表的大小始終在變化,Python 可能會(huì)丟失 food 變量的下一個(gè)值應(yīng)該是什么。想象一下,當(dāng)有人添加或刪除果凍豆時(shí),要數(shù)出罐子里果凍豆的數(shù)量會(huì)有多困難。
但是,如果您迭代列表的副本(并且副本永遠(yuǎn)不會(huì)更改),則從原始列表中添加或刪除項(xiàng)目將不會(huì)成為問題。
在窗口上繪制食物方塊
第 105 行和 106 行的代碼類似于我們用來為玩家繪制黑色方框的代碼:
# Draw the food.for i in range(len(foods)):pygame.draw.rect(windowSurface, GREEN, foods[i])
第 105 行循環(huán)遍歷foods
列表中的每個(gè)食物方塊,第 106 行將食物方塊繪制到windowSurface
上。
現(xiàn)在玩家和食物方塊都在屏幕上,窗口已準(zhǔn)備好更新,因此我們?cè)诘?109 行調(diào)用update()
方法,并通過在我們之前創(chuàng)建的Clock
對(duì)象上調(diào)用tick()
方法來完成程序:
# Draw the window onto the screen.pygame.display.update()mainClock.tick(40)
程序?qū)⒗^續(xù)通過游戲循環(huán)并保持更新,直到玩家退出。
總結(jié)
本章介紹了碰撞檢測(cè)的概念。在圖形游戲中,檢測(cè)兩個(gè)矩形之間的碰撞是如此普遍,以至于pygame
為pygame.Rect
對(duì)象提供了自己的碰撞檢測(cè)方法,名為colliderect()
。
這本書中的前幾個(gè)游戲都是基于文本的。程序的輸出是打印在屏幕上的文本,輸入是玩家在鍵盤上輸入的文本。但是圖形程序也可以接受鍵盤和鼠標(biāo)輸入。
此外,這些程序可以在玩家按下或釋放單個(gè)鍵時(shí)響應(yīng)單個(gè)按鍵。玩家不必輸入整個(gè)響應(yīng)并按下 ENTER 鍵。這樣可以實(shí)現(xiàn)即時(shí)反饋和更加互動(dòng)的游戲。
這個(gè)交互式程序很有趣,但讓我們超越繪制矩形。在第 20 章中,你將學(xué)習(xí)如何使用pygame
加載圖像和播放音效。
二十、使用聲音和圖像
原文:
inventwithpython.com/invent4thed/chapter20.html
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
在第 18 章和第 19 章中,您學(xué)習(xí)了如何制作具有圖形并可以接受鍵盤和鼠標(biāo)輸入的 GUI 程序。您還學(xué)會(huì)了如何繪制不同的形狀。在本章中,您將學(xué)習(xí)如何向游戲中添加聲音、音樂和圖像。
本章涵蓋的主題
-
聲音和圖像文件
-
繪制和調(diào)整精靈的大小
-
添加音樂和聲音
-
切換聲音開關(guān)
使用精靈添加圖像
sprite是屏幕上用作圖形的一部分的單個(gè)二維圖像。圖 20-1 顯示了一些示例 sprite。
圖 20-1:一些精靈的示例
精靈圖像繪制在背景上。您可以水平翻轉(zhuǎn)精靈圖像,使其面向另一邊。您還可以在同一窗口上多次繪制相同的精靈圖像,并且可以調(diào)整精靈的大小,使其比原始精靈圖像大或小。背景圖像也可以被視為一個(gè)大精靈。圖 20-2 顯示了精靈一起使用。
圖 20-2:一個(gè)完整的場(chǎng)景,精靈繪制在背景上
下一個(gè)程序?qū)⒀菔救绾问褂?code>pygame播放聲音和繪制精靈。
聲音和圖像文件
精靈存儲(chǔ)在計(jì)算機(jī)上的圖像文件中。pygame
可以使用幾種圖像格式。要了解圖像文件使用的格式,請(qǐng)查看文件名的末尾(最后一個(gè)句點(diǎn)之后)。這稱為文件擴(kuò)展名。例如,文件player.png是 PNG 格式。pygame
支持的圖像格式包括 BMP、PNG、JPG 和 GIF。
您可以從網(wǎng)絡(luò)瀏覽器下載圖像。在大多數(shù)網(wǎng)絡(luò)瀏覽器上,您可以通過右鍵單擊網(wǎng)頁(yè)中的圖像,然后從出現(xiàn)的菜單中選擇“保存”來這樣做。記住您保存圖像文件的硬盤位置,因?yàn)槟枰獙⑾螺d的圖像文件復(fù)制到與 Python 程序的*.py*文件相同的文件夾中。您還可以使用諸如 Microsoft Paint 或 Tux Paint 之類的繪圖程序創(chuàng)建自己的圖像。
pygame
支持的聲音文件格式為 MIDI、WAV 和 MP3。您可以像下載圖像文件一樣從互聯(lián)網(wǎng)下載音效文件,但音頻文件必須是這三種格式之一。如果您的計(jì)算機(jī)有麥克風(fēng),您還可以錄制聲音并制作自己的 WAV 文件以在游戲中使用。
Sprites and Sounds 程序的示例運(yùn)行
本章的程序與第 19 章的碰撞檢測(cè)程序相同。但是,在本程序中,我們將使用精靈而不是普通的方塊。我們將使用一個(gè)人的精靈來代表玩家,而不是黑色方塊,以及櫻桃的精靈,而不是綠色的食物方塊。我們還將播放背景音樂,并在玩家精靈吃掉櫻桃時(shí)添加聲音效果。
在這個(gè)游戲中,玩家精靈將吃掉櫻桃精靈,并且在吃掉櫻桃時(shí),它會(huì)變大。當(dāng)您運(yùn)行程序時(shí),游戲?qū)⒖雌饋硐駡D 20-3。
圖 20-3:Sprites and Sounds 游戲的屏幕截圖
Sprites and Sounds 程序的源代碼
開始一個(gè)新文件,輸入以下代碼,然后將其保存為spritesAndSounds.py。你可以從本書的網(wǎng)站www.nostarch.com/inventwithpython/
下載我們?cè)诒境绦蛑惺褂玫膱D像和聲音文件。將這些文件放在與spritesAndSounds.py程序相同的文件夾中。
如果在輸入此代碼后出現(xiàn)錯(cuò)誤,請(qǐng)使用在線的 diff 工具比較你輸入的代碼和書中的代碼,網(wǎng)址為www.nostarch.com/inventwithpython#diff
。
spritesAnd Sounds.py
import pygame, sys, time, random
from pygame.locals import *# Set up pygame.
pygame.init()
mainClock = pygame.time.Clock()# Set up the window.
WINDOWWIDTH = 400
WINDOWHEIGHT = 400
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),0, 32)
pygame.display.set_caption('Sprites and Sounds')# Set up the colors.
WHITE = (255, 255, 255)# Set up the block data structure.
player = pygame.Rect(300, 100, 40, 40)
playerImage = pygame.image.load('player.png')
playerStretchedImage = pygame.transform.scale(playerImage, (40, 40))
foodImage = pygame.image.load('cherry.png')
foods = []
for i in range(20):foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - 20),random.randint(0, WINDOWHEIGHT - 20), 20, 20))foodCounter = 0
NEWFOOD = 40# Set up keyboard variables.
moveLeft = False
moveRight = False
moveUp = False
moveDown = FalseMOVESPEED = 6# Set up the music.
pickUpSound = pygame.mixer.Sound('pickup.wav')
pygame.mixer.music.load('background.mid')
pygame.mixer.music.play(-1, 0.0)
musicPlaying = True# Run the game loop.
while True:# Check for the QUIT event.for event in pygame.event.get():if event.type == QUIT:pygame.quit()sys.exit()if event.type == KEYDOWN:# Change the keyboard variables.if event.key == K_LEFT or event.key == K_a:moveRight = FalsemoveLeft = Trueif event.key == K_RIGHT or event.key == K_d:moveLeft = FalsemoveRight = Trueif event.key == K_UP or event.key == K_w:moveDown = FalsemoveUp = Trueif event.key == K_DOWN or event.key == K_s:moveUp = FalsemoveDown = Trueif event.type == KEYUP:if event.key == K_ESCAPE:pygame.quit()sys.exit()if event.key == K_LEFT or event.key == K_a:moveLeft = Falseif event.key == K_RIGHT or event.key == K_d:moveRight = Falseif event.key == K_UP or event.key == K_w:moveUp = Falseif event.key == K_DOWN or event.key == K_s:moveDown = Falseif event.key == K_x:player.top = random.randint(0, WINDOWHEIGHT -player.height)player.left = random.randint(0, WINDOWWIDTH -player.width)if event.key == K_m:if musicPlaying:pygame.mixer.music.stop()else:pygame.mixer.music.play(-1, 0.0)musicPlaying = not musicPlayingif event.type == MOUSEBUTTONUP:foods.append(pygame.Rect(event.pos[0] - 10,event.pos[1] - 10, 20, 20))foodCounter += 1if foodCounter >= NEWFOOD:# Add new food.foodCounter = 0foods.append(pygame.Rect(random.randint(0, WINDOWWIDTH - 20),random.randint(0, WINDOWHEIGHT - 20), 20, 20))# Draw the white background onto the surface.windowSurface.fill(WHITE)# Move the player.if moveDown and player.bottom < WINDOWHEIGHT:player.top += MOVESPEEDif moveUp and player.top > 0:player.top -= MOVESPEEDif moveLeft and player.left > 0:player.left -= MOVESPEEDif moveRight and player.right < WINDOWWIDTH:player.right += MOVESPEED# Draw the block onto the surface.windowSurface.blit(playerStretchedImage, player)# Check whether the block has intersected with any food squares.for food in foods[:]:if player.colliderect(food):foods.remove(food)player = pygame.Rect(player.left, player.top,player.width + 2, player.height + 2)playerStretchedImage = pygame.transform.scale(playerImage,(player.width, player.height))if musicPlaying:pickUpSound.play()# Draw the food.for food in foods:windowSurface.blit(foodImage, food)# Draw the window onto the screen.pygame.display.update()mainClock.tick(40)
設(shè)置窗口和數(shù)據(jù)結(jié)構(gòu)
這個(gè)程序中的大部分代碼與第 19 章中的碰撞檢測(cè)程序相同。我們只關(guān)注添加精靈和聲音的部分。首先,在第 12 行,讓我們將標(biāo)題欄的標(biāo)題設(shè)置為描述這個(gè)程序的字符串:
pygame.display.set_caption('Sprites and Sounds')
為了設(shè)置標(biāo)題,你需要將字符串'Sprites and Sounds'
傳遞給pygame.display.set_caption()
函數(shù)。
添加一個(gè)精靈
現(xiàn)在我們已經(jīng)設(shè)置好標(biāo)題,我們需要實(shí)際的精靈。我們將使用三個(gè)變量來表示玩家,而不是之前程序中只使用一個(gè)。
# Set up the block data structure.
player = pygame.Rect(300, 100, 40, 40)
playerImage = pygame.image.load('player.png')
playerStretchedImage = pygame.transform.scale(playerImage, (40, 40))
foodImage = pygame.image.load('cherry.png')
第 18 行的player
變量將存儲(chǔ)一個(gè)Rect
對(duì)象,用于跟蹤玩家的位置和大小。player
變量不包含玩家的圖像。在程序開始時(shí),玩家的左上角位于(300, 100),玩家的初始高度和寬度為 40 像素。
表示玩家的第二個(gè)變量是第 19 行的playerImage
。pygame.image.load()
函數(shù)傳遞了要加載的圖像文件的文件名字符串。返回值是一個(gè)Surface
對(duì)象,其中包含圖像文件中的圖形。我們將這個(gè)Surface
對(duì)象存儲(chǔ)在playerImage
中。
改變精靈的大小
在第 20 行,我們將使用pygame.transform
模塊中的一個(gè)新函數(shù)。pygame.transform.scale()
函數(shù)可以縮小或放大精靈。第一個(gè)參數(shù)是一個(gè)帶有圖像的Surface
對(duì)象。第二個(gè)參數(shù)是一個(gè)元組,表示第一個(gè)參數(shù)中圖像的新寬度和高度。scale()
函數(shù)返回一個(gè)帶有以新尺寸繪制的圖像的Surface
對(duì)象。在本章的程序中,當(dāng)玩家吃更多櫻桃時(shí),我們將使玩家精靈拉伸變大。我們將原始圖像存儲(chǔ)在playerImage
變量中,而拉伸后的圖像存儲(chǔ)在playerStretchedImage
變量中。
在第 21 行,我們?cè)俅握{(diào)用load()
來創(chuàng)建一個(gè)帶有櫻桃圖像的Surface
對(duì)象。確保player.png和cherry.png文件與spritesAndSounds.py文件在同一個(gè)文件夾中;否則,pygame
將無法找到它們并報(bào)錯(cuò)。
設(shè)置音樂和聲音
接下來需要加載聲音文件。pygame
中有兩個(gè)用于聲音的模塊。pygame.mixer
模塊可以在游戲過程中播放短聲音效果。pygame.mixer.music
模塊可以播放背景音樂。
添加聲音文件
調(diào)用pygame.mixer.Sound()
構(gòu)造函數(shù)來創(chuàng)建一個(gè)pygame.mixer.Sound
對(duì)象(簡(jiǎn)稱為Sound
對(duì)象)。這個(gè)對(duì)象有一個(gè)play()
方法,當(dāng)調(diào)用時(shí)會(huì)播放聲音效果。
# Set up the music.
pickUpSound = pygame.mixer.Sound('pickup.wav')
pygame.mixer.music.load('background.mid')
pygame.mixer.music.play(-1, 0.0)
musicPlaying = True
第 39 行調(diào)用pygame.mixer.music.load()
來加載背景音樂,第 40 行調(diào)用pygame.mixer.music.play()
來開始播放它。第一個(gè)參數(shù)告訴pygame
在第一次播放后播放背景音樂的次數(shù)。因此,傳遞5
會(huì)導(dǎo)致pygame
播放背景音樂六次。在這里,我們傳遞參數(shù)-1
,這是一個(gè)特殊值,使背景音樂永遠(yuǎn)重復(fù)播放。
play()
的第二個(gè)參數(shù)是開始播放聲音文件的時(shí)間點(diǎn)。傳遞0.0
將從開頭播放背景音樂。傳遞2.5
將從開頭開始播放背景音樂 2.5 秒。
最后,musicPlaying
變量具有一個(gè)布爾值,告訴程序是否應(yīng)該播放背景音樂和聲音效果。給玩家選擇在沒有聲音的情況下運(yùn)行程序是很好的。
切換聲音的開關(guān)
按 M 鍵可以打開或關(guān)閉背景音樂。如果musicPlaying
設(shè)置為True
,則當(dāng)前正在播放背景音樂,我們應(yīng)該通過調(diào)用pygame.mixer.music.stop()
來停止它。如果musicPlaying
設(shè)置為False
,則當(dāng)前沒有播放背景音樂,我們應(yīng)該通過調(diào)用play()
來開始播放。第 79 到 84 行使用if
語(yǔ)句來實(shí)現(xiàn)這一點(diǎn):
if event.key == K_m:if musicPlaying:pygame.mixer.music.stop()else:pygame.mixer.music.play(-1, 0.0)musicPlaying = not musicPlaying
無論音樂是否正在播放,我們都希望切換musicPlaying
中的值。切換布爾值意味著將值設(shè)置為其當(dāng)前值的相反值。musicPlaying = not musicPlaying
這一行將變量設(shè)置為False
,如果它當(dāng)前為True
,或者如果它當(dāng)前為False
,則將其設(shè)置為True
。想象一下切換就像你打開或關(guān)閉燈開關(guān)時(shí)發(fā)生的事情:切換燈開關(guān)會(huì)將其設(shè)置為相反的設(shè)置。
在窗口上繪制玩家
請(qǐng)記住,存儲(chǔ)在playerStretchedImage
中的值是一個(gè)Surface
對(duì)象。第 110 行使用blit()
將玩家的精靈繪制到窗口的Surface
對(duì)象上(存儲(chǔ)在windowSurface
中):
# Draw the block onto the surface.windowSurface.blit(playerStretchedImage, player)
blit()
方法的第二個(gè)參數(shù)是一個(gè)Rect
對(duì)象,指定在Surface
對(duì)象上繪制精靈的位置。程序使用存儲(chǔ)在player
中的Rect
對(duì)象,它跟蹤玩家在窗口中的位置。
檢查碰撞
這段代碼與以前的程序中的代碼類似,但有幾行新代碼:
if player.colliderect(food):foods.remove(food)player = pygame.Rect(player.left, player.top,player.width + 2, player.height + 2)playerStretchedImage = pygame.transform.scale(playerImage,(player.width, player.height))if musicPlaying:pickUpSound.play()
當(dāng)玩家精靈吃掉櫻桃之一時(shí),其大小增加兩個(gè)像素的高度和寬度。在第 116 行,一個(gè)比舊的Rect
對(duì)象大兩個(gè)像素的新Rect
對(duì)象將被分配為player
的新值。
雖然Rect
對(duì)象表示玩家的位置和大小,但玩家的圖像存儲(chǔ)在playerStretchedImage
中,作為Surface
對(duì)象。在第 117 行,程序通過調(diào)用scale()
創(chuàng)建一個(gè)新的拉伸圖像。
拉伸圖像通常會(huì)使圖像略微失真。如果你不斷地重新拉伸已經(jīng)拉伸過的圖像,失真會(huì)迅速累積。但通過每次將原始圖像拉伸到新的尺寸——通過傳遞playerImage
,而不是playerStretchedImage
,作為scale()
的第一個(gè)參數(shù)——你只會(huì)使圖像失真一次。
最后,第 119 行調(diào)用存儲(chǔ)在pickUpSound
變量中的Sound
對(duì)象上的play()
方法。但只有當(dāng)musicPlaying
設(shè)置為True
時(shí)才會(huì)這樣做(這意味著聲音已打開)。
在窗口上繪制櫻桃
在以前的程序中,你調(diào)用pygame.draw.rect()
函數(shù)為foods
列表中存儲(chǔ)的每個(gè)Rect
對(duì)象繪制一個(gè)綠色的正方形。然而,在這個(gè)程序中,你想要繪制櫻桃精靈。調(diào)用blit()
方法并傳遞存儲(chǔ)在foodImage
中的Surface
對(duì)象,上面繪制了櫻桃圖像:
# Draw the food.for food in foods:windowSurface.blit(foodImage, food)
food
變量包含foods
中的每個(gè)Rect
對(duì)象,在每次for
循環(huán)中告訴blit()
方法在哪里繪制foodImage
。
總結(jié)
你已經(jīng)為你的游戲添加了圖像和聲音。這些稱為精靈的圖像看起來比以前的程序中使用的簡(jiǎn)單繪制的形狀要好得多。精靈可以被縮放(即拉伸)到更大或更小的尺寸,因此我們可以以任何我們想要的尺寸顯示精靈。本章介紹的游戲還有一個(gè)背景并播放聲音效果。
現(xiàn)在我們知道如何創(chuàng)建窗口,顯示精靈,繪制基本圖形,收集鍵盤和鼠標(biāo)輸入,播放聲音,并實(shí)現(xiàn)碰撞檢測(cè),我們準(zhǔn)備在pygame
中創(chuàng)建一個(gè)圖形游戲。第 21 章將所有這些元素結(jié)合起來,打造我們迄今為止最先進(jìn)的游戲。
二十一、有聲音和圖像的《躲避者》游戲
原文:
inventwithpython.com/invent4thed/chapter21.html
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
前面的四章介紹了 pygame
模塊,并演示了如何使用它的許多功能。在本章中,我們將利用這些知識(shí)創(chuàng)建一個(gè)名為《躲避者》的圖形游戲。
本章涵蓋的主題
-
pygame.FULLSCREEN
標(biāo)志 -
move_ip() Rect
方法 -
實(shí)現(xiàn)作弊碼
-
修改《躲避者》游戲
在《躲避者》游戲中,玩家控制一個(gè)精靈(玩家角色),必須躲避從屏幕頂部掉落的一大堆壞人。玩家能夠躲避壞人的時(shí)間越長(zhǎng),他們的得分就會(huì)越高。
只是為了好玩,我們還將在這個(gè)游戲中添加一些作弊模式。如果玩家按住 X 鍵,每個(gè)壞人的速度都會(huì)降低到超慢的速度。如果玩家按住 Z 鍵,壞人將會(huì)改變方向,向上而不是向下移動(dòng)。
基本 pygame 數(shù)據(jù)類型的回顧
在我們開始制作《躲避者》之前,讓我們回顧一下 pygame
中使用的一些基本數(shù)據(jù)類型:
pygame.Rect
Rect
對(duì)象表示矩形空間的位置和大小。位置由 Rect
對(duì)象的 topleft
屬性(或 topright
、bottomleft
和 bottomright
屬性)確定。這些角屬性是 x 和 y 坐標(biāo)的整數(shù)元組。大小由 width
和 height
屬性確定,這些屬性是指示矩形有多長(zhǎng)或多高的整數(shù)像素。Rect
對(duì)象有一個(gè) colliderect()
方法,用于檢查它們是否與另一個(gè) Rect
對(duì)象發(fā)生碰撞。
pygame.Surface
Surface
對(duì)象是有色像素區(qū)域。Surface
對(duì)象表示一個(gè)矩形圖像,而 Rect
對(duì)象只表示一個(gè)矩形空間和位置。Surface
對(duì)象有一個(gè) blit()
方法,用于將一個(gè) Surface
對(duì)象上的圖像繪制到另一個(gè) Surface
對(duì)象上。pygame.display.set_mode()
函數(shù)返回的 Surface
對(duì)象是特殊的,因?yàn)樵谠?Surface
對(duì)象上繪制的任何東西在調(diào)用 pygame.display.update()
時(shí)會(huì)顯示在用戶的屏幕上。
pygame.event.Event
pygame.event
模塊在用戶提供鍵盤、鼠標(biāo)或其他輸入時(shí)生成 Event
對(duì)象。pygame.event.get()
函數(shù)返回這些 Event
對(duì)象的列表。您可以通過檢查其 type
屬性來確定 Event
對(duì)象的類型。QUIT
、KEYDOWN
和 MOUSEBUTTONUP
是一些事件類型的示例(有關(guān)所有事件類型的完整列表,請(qǐng)參見“處理事件”第 292 頁(yè))。
pygame.font.Font
pygame.font
模塊使用 Font
數(shù)據(jù)類型,表示 pygame
中文本使用的字體。傳遞給 pygame.font.SysFont()
的參數(shù)是字體名稱的字符串(通常傳遞 None
作為字體名稱以獲取默認(rèn)系統(tǒng)字體)和字體大小的整數(shù)。
pygame.time.Clock
pygame.time
模塊中的 Clock
對(duì)象有助于防止我們的游戲運(yùn)行得比玩家能看到的更快。Clock
對(duì)象有一個(gè) tick()
方法,可以傳遞我們希望游戲運(yùn)行的每秒幀數(shù)(FPS)。FPS 越高,游戲運(yùn)行得越快。
《躲避者》的示例運(yùn)行
當(dāng)您運(yùn)行這個(gè)程序時(shí),游戲?qū)?huì)看起來像圖 21-1。
圖 21-1:《躲避者》游戲的屏幕截圖
《躲避者》的源代碼
在一個(gè)新文件中輸入以下代碼,并將其保存為 dodger.py。您可以從 www.nostarch.com/inventwithpython/
下載代碼、圖像和聲音文件。將圖像和聲音文件放在與 dodger.py 相同的文件夾中。
如果在輸入此代碼后出現(xiàn)錯(cuò)誤,請(qǐng)使用在線 diff 工具將你輸入的代碼與本書代碼進(jìn)行比較,網(wǎng)址為 www.nostarch.com/inventwithpython#diff
。
dodger.py
import pygame, random, sys
from pygame.locals import *WINDOWWIDTH = 600
WINDOWHEIGHT = 600
TEXTCOLOR = (0, 0, 0)
BACKGROUNDCOLOR = (255, 255, 255)
FPS = 60
BADDIEMINSIZE = 10
BADDIEMAXSIZE = 40
BADDIEMINSPEED = 1
BADDIEMAXSPEED = 8
ADDNEWBADDIERATE = 6
PLAYERMOVERATE = 5def terminate():pygame.quit()sys.exit()def waitForPlayerToPressKey():while True:for event in pygame.event.get():if event.type == QUIT:terminate()if event.type == KEYDOWN:if event.key == K_ESCAPE: # Pressing ESC quits.terminate()returndef playerHasHitBaddie(playerRect, baddies):for b in baddies:if playerRect.colliderect(b['rect']):return Truereturn Falsedef drawText(text, font, surface, x, y):textobj = font.render(text, 1, TEXTCOLOR)textrect = textobj.get_rect()textrect.topleft = (x, y)surface.blit(textobj, textrect)# Set up pygame, the window, and the mouse cursor.
pygame.init()
mainClock = pygame.time.Clock()
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
pygame.display.set_caption('Dodger')
pygame.mouse.set_visible(False)# Set up the fonts.
font = pygame.font.SysFont(None, 48)# Set up sounds.
gameOverSound = pygame.mixer.Sound('gameover.wav')
pygame.mixer.music.load('background.mid')# Set up images.
playerImage = pygame.image.load('player.png')
playerRect = playerImage.get_rect()
baddieImage = pygame.image.load('baddie.png')# Show the "Start" screen.
windowSurface.fill(BACKGROUNDCOLOR)
drawText('Dodger', font, windowSurface, (WINDOWWIDTH / 3),(WINDOWHEIGHT / 3))
drawText('Press a key to start.', font, windowSurface,(WINDOWWIDTH / 3) - 30, (WINDOWHEIGHT / 3) + 50)
pygame.display.update()
waitForPlayerToPressKey()topScore = 0
while True:# Set up the start of the game.baddies = []score = 0playerRect.topleft = (WINDOWWIDTH / 2, WINDOWHEIGHT - 50)moveLeft = moveRight = moveUp = moveDown = FalsereverseCheat = slowCheat = FalsebaddieAddCounter = 0pygame.mixer.music.play(-1, 0.0)while True: # The game loop runs while the game part is playing.score += 1 # Increase score.for event in pygame.event.get():if event.type == QUIT:terminate()if event.type == KEYDOWN:if event.key == K_z:reverseCheat = Trueif event.key == K_x:slowCheat = Trueif event.key == K_LEFT or event.key == K_a:moveRight = FalsemoveLeft = Trueif event.key == K_RIGHT or event.key == K_d:moveLeft = FalsemoveRight = Trueif event.key == K_UP or event.key == K_w:moveDown = FalsemoveUp = Trueif event.key == K_DOWN or event.key == K_s:moveUp = FalsemoveDown = Trueif event.type == KEYUP:if event.key == K_z:reverseCheat = Falsescore = 0if event.key == K_x:slowCheat = Falsescore = 0if event.key == K_ESCAPE:terminate()if event.key == K_LEFT or event.key == K_a:moveLeft = Falseif event.key == K_RIGHT or event.key == K_d:moveRight = Falseif event.key == K_UP or event.key == K_w:moveUp = Falseif event.key == K_DOWN or event.key == K_s:moveDown = Falseif event.type == MOUSEMOTION:# If the mouse moves, move the player to the cursor.playerRect.centerx = event.pos[0]playerRect.centery = event.pos[1]# Add new baddies at the top of the screen, if needed.if not reverseCheat and not slowCheat:baddieAddCounter += 1if baddieAddCounter == ADDNEWBADDIERATE:baddieAddCounter = 0baddieSize = random.randint(BADDIEMINSIZE, BADDIEMAXSIZE)newBaddie = {'rect': pygame.Rect(random.randint(0,WINDOWWIDTH - baddieSize), 0 - baddieSize,baddieSize, baddieSize),'speed': random.randint(BADDIEMINSPEED,BADDIEMAXSPEED),'surface':pygame.transform.scale(baddieImage,(baddieSize, baddieSize)),}baddies.append(newBaddie)# Move the player around.if moveLeft and playerRect.left > 0:playerRect.move_ip(-1 * PLAYERMOVERATE, 0)if moveRight and playerRect.right < WINDOWWIDTH:playerRect.move_ip(PLAYERMOVERATE, 0)if moveUp and playerRect.top > 0:playerRect.move_ip(0, -1 * PLAYERMOVERATE)if moveDown and playerRect.bottom < WINDOWHEIGHT:playerRect.move_ip(0, PLAYERMOVERATE)# Move the baddies down.for b in baddies:if not reverseCheat and not slowCheat:b['rect'].move_ip(0, b['speed'])elif reverseCheat:b['rect'].move_ip(0, -5)elif slowCheat:b['rect'].move_ip(0, 1)# Delete baddies that have fallen past the bottom.for b in baddies[:]:if b['rect'].top > WINDOWHEIGHT:baddies.remove(b)# Draw the game world on the window.windowSurface.fill(BACKGROUNDCOLOR)# Draw the score and top score.drawText('Score: %s' % (score), font, windowSurface, 10, 0)drawText('Top Score: %s' % (topScore), font, windowSurface,10, 40)# Draw the player's rectangle.windowSurface.blit(playerImage, playerRect)# Draw each baddie.for b in baddies:windowSurface.blit(b['surface'], b['rect'])pygame.display.update()# Check if any of the baddies have hit the player.if playerHasHitBaddie(playerRect, baddies):if score > topScore:topScore = score # Set new top score.breakmainClock.tick(FPS)# Stop the game and show the "Game Over" screen.pygame.mixer.music.stop()gameOverSound.play()drawText('GAME OVER', font, windowSurface, (WINDOWWIDTH / 3),(WINDOWHEIGHT / 3))drawText('Press a key to play again.', font, windowSurface,(WINDOWWIDTH / 3) - 80, (WINDOWHEIGHT / 3) + 50)pygame.display.update()waitForPlayerToPressKey()gameOverSound.stop()
導(dǎo)入模塊
Dodger 游戲?qū)肓伺c之前的 pygame
程序相同的模塊:pygame
、random
、sys
和 pygame.locals
。
import pygame, random, sys
from pygame.locals import *
pygame.locals
模塊包含了 pygame
使用的幾個(gè)常量變量,比如事件類型(QUIT
,KEYDOWN
等)和鍵盤按鍵(K_ESCAPE
,K_LEFT
等)。通過使用 from pygame.locals import *
語(yǔ)法,你可以在源代碼中直接使用 QUIT
而不是 pygame.locals.QUIT
。
設(shè)置常量變量
第 4 到 7 行設(shè)置了窗口尺寸、文本顏色和背景顏色的常量:
WINDOWWIDTH = 600
WINDOWHEIGHT = 600
TEXTCOLOR = (0, 0, 0)
BACKGROUNDCOLOR = (255, 255, 255)
我們使用常量變量是因?yàn)樗鼈儽任覀兪謩?dòng)輸入的值更具描述性。例如,windowSurface.fill(BACKGROUNDCOLOR)
這一行比 windowSurface.fill((255, 255, 255))
更容易理解。
你可以通過改變常量變量來輕松改變游戲。通過改變第 4 行的 WINDOWWIDTH
,你會(huì)自動(dòng)改變代碼中所有使用 WINDOWWIDTH
的地方。如果你使用的是值 600
,那么你需要在代碼中每次出現(xiàn) 600
的地方都進(jìn)行修改。改變常量的值一次會(huì)更容易。
在第 8 行,你設(shè)置了 FPS
的常量,即每秒幀數(shù),你希望游戲運(yùn)行的幀數(shù)。
FPS = 60
幀 是通過游戲循環(huán)的單次迭代繪制的屏幕。你將 FPS
傳遞給第 186 行的 mainClock.tick()
方法,以便函數(shù)知道暫停程序的時(shí)間。這里 FPS
設(shè)置為 60
,但你可以將 FPS
更改為更高的值以使游戲運(yùn)行更快,或者更改為更低的值以減慢游戲速度。
第 9 到 13 行設(shè)置了更多的壞蛋下落的常量變量:
BADDIEMINSIZE = 10
BADDIEMAXSIZE = 40
BADDIEMINSPEED = 1
BADDIEMAXSPEED = 8
ADDNEWBADDIERATE = 6
壞蛋的寬度和高度將在 BADDIEMINSIZE
和 BADDIEMAXSIZE
之間。壞蛋在屏幕上下落的速度將在 BADDIEMINSPEED
和 BADDIEMAXSPEED
之間,每次游戲循環(huán)迭代的像素?cái)?shù)。并且每經(jīng)過 ADDNEWBADDIERATE
次游戲循環(huán)迭代,一個(gè)新的壞蛋將被添加到窗口頂部。
最后,PLAYERMOVERATE
存儲(chǔ)了玩家角色在游戲循環(huán)的每次迭代中在窗口中移動(dòng)的像素?cái)?shù)(如果角色正在移動(dòng)):
PLAYERMOVERATE = 5
通過增加這個(gè)數(shù)字,你可以增加角色移動(dòng)的速度。
定義函數(shù)
你將為這個(gè)游戲創(chuàng)建幾個(gè)函數(shù)。terminate()
和 waitForPlayerToPressKey()
函數(shù)將分別結(jié)束和暫停游戲,playerHasHitBaddie()
函數(shù)將跟蹤玩家與壞蛋的碰撞,drawText()
函數(shù)將在屏幕上繪制得分和其他文本。
結(jié)束和暫停游戲
pygame
模塊要求你同時(shí)調(diào)用 pygame.quit()
和 sys.exit()
來結(jié)束游戲。第 16 到 18 行將它們都放入一個(gè)名為 terminate()
的函數(shù)中。
def terminate():pygame.quit()sys.exit()
現(xiàn)在你只需要調(diào)用 terminate()
而不是同時(shí)調(diào)用 pygame.quit()
和 sys.exit()
。
有時(shí)你會(huì)希望暫停程序,直到玩家按下一個(gè)鍵,比如在游戲開始時(shí)出現(xiàn) Dodger 標(biāo)題文本或者在結(jié)束時(shí)顯示 Game Over 時(shí)。第 20 到 24 行創(chuàng)建了一個(gè)名為 waitForPlayerToPressKey()
的新函數(shù):
def waitForPlayerToPressKey():while True:for event in pygame.event.get():if event.type == QUIT:terminate()
在這個(gè)函數(shù)內(nèi)部,有一個(gè)無限循環(huán),只有在接收到 KEYDOWN
或 QUIT
事件時(shí)才會(huì)中斷。在循環(huán)開始時(shí),pygame.event.get()
返回一個(gè) Event
對(duì)象列表供檢查。
如果玩家在程序等待玩家按鍵時(shí)關(guān)閉了窗口,pygame
將生成一個(gè) QUIT
事件,在第 23 行通過 event.type
進(jìn)行檢查。如果玩家退出,Python 將在第 24 行調(diào)用 terminate()
函數(shù)。
如果游戲收到KEYDOWN
事件,它應(yīng)首先檢查是否按下了 ESC 鍵:
if event.type == KEYDOWN:if event.key == K_ESCAPE: # Pressing ESC quits.terminate()return
如果玩家按下 ESC,則程序應(yīng)該終止。如果不是這種情況,那么執(zhí)行將跳過第 27 行的if
塊,直接到return
語(yǔ)句,退出waitForPlayerToPressKey()
函數(shù)。
如果沒有生成QUIT
或KEYDOWN
事件,代碼將繼續(xù)循環(huán)。由于循環(huán)什么也不做,這將使游戲看起來像已經(jīng)凍結(jié),直到玩家按下鍵。
跟蹤壞人碰撞
如果玩家的角色與壞人之一發(fā)生碰撞,則playerHasHitBaddie()
函數(shù)將返回True
:
def playerHasHitBaddie(playerRect, baddies):for b in baddies:if playerRect.colliderect(b['rect']):return Truereturn False
baddies
參數(shù)是壞人字典數(shù)據(jù)結(jié)構(gòu)的列表。這些字典中的每一個(gè)都有一個(gè)’rect’鍵,該鍵的值是表示壞人大小和位置的Rect
對(duì)象。
playerRect
也是一個(gè)Rect
對(duì)象。Rect
對(duì)象有一個(gè)名為colliderect()
的方法,如果Rect
對(duì)象與傳遞給它的Rect
對(duì)象發(fā)生碰撞,則返回True
。否則,colliderect()
返回False
。
第 31 行的for
循環(huán)遍歷baddies
列表中的每個(gè)壞人字典。如果任何這些壞人與玩家的角色發(fā)生碰撞,則playerHasHitBaddie()
返回True
。如果代碼成功遍歷baddies
列表中的所有壞人而沒有檢測(cè)到碰撞,則playerHasHitBaddie()
返回False
。
向窗口繪制文本
在窗口上繪制文本涉及一些步驟,我們通過drawText()
來完成。這樣,當(dāng)我們想要在屏幕上顯示玩家得分或游戲結(jié)束文本時(shí),只需要調(diào)用一個(gè)函數(shù)。
def drawText(text, font, surface, x, y):textobj = font.render(text, 1, TEXTCOLOR)textrect = textobj.get_rect()textrect.topleft = (x, y)surface.blit(textobj, textrect)
首先,第 37 行的render()
方法調(diào)用創(chuàng)建了一個(gè)Surface
對(duì)象,以特定字體呈現(xiàn)文本。
接下來,您需要知道Surface
對(duì)象的大小和位置。您可以使用get_rect() Surface
方法獲取包含此信息的Rect
對(duì)象。
從第 38 行的get_rect()
返回的Rect
對(duì)象中復(fù)制了Surface
對(duì)象的寬度和高度信息。第 39 行通過為其topleft
屬性設(shè)置一個(gè)新的元組值來更改Rect
對(duì)象的位置。
最后,第 40 行將渲染文本的Surface
對(duì)象繪制到傳遞給drawText()
函數(shù)的Surface
對(duì)象上。在pygame
中顯示文本比簡(jiǎn)單調(diào)用print()
函數(shù)需要更多步驟。但是,如果將此代碼放入名為drawText()
的單個(gè)函數(shù)中,那么您只需要調(diào)用此函數(shù)即可在屏幕上顯示文本。
初始化 pygame 和設(shè)置窗口
現(xiàn)在常量變量和函數(shù)已經(jīng)完成,我們將開始調(diào)用設(shè)置窗口和時(shí)鐘的pygame
函數(shù):
# Set up pygame, the window, and the mouse cursor.
pygame.init()
mainClock = pygame.time.Clock()
第 43 行通過調(diào)用pygame.init()
函數(shù)設(shè)置了pygame
。第 44 行創(chuàng)建了一個(gè)pygame.time.Clock()
對(duì)象,并將其存儲(chǔ)在mainClock
變量中。這個(gè)對(duì)象將幫助我們防止程序運(yùn)行得太快。
第 45 行創(chuàng)建了一個(gè)用于窗口顯示的新Surface
對(duì)象:
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
請(qǐng)注意,pygame.display.set_mode()
只傳遞了一個(gè)參數(shù):一個(gè)元組。pygame.display.set_mode()
的參數(shù)不是兩個(gè)整數(shù),而是一個(gè)包含兩個(gè)整數(shù)的元組。您可以通過傳遞一個(gè)包含WINDOWWIDTH
和WINDOWHEIGHT
常量變量的元組來指定此Surface
對(duì)象(和窗口)的寬度和高度。
pygame.display.set_mode()
函數(shù)有第二個(gè)可選參數(shù)。您可以傳遞pygame.FULLSCREEN
常量以使窗口填滿整個(gè)屏幕??匆幌聦?duì)第 45 行的修改:
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),pygame.FULLSCREEN)
WINDOWWIDTH
和WINDOWHEIGHT
參數(shù)仍然用于窗口的寬度和高度,但圖像將被拉伸以適應(yīng)屏幕。嘗試在全屏模式和非全屏模式下運(yùn)行程序。
第 46 行將窗口的標(biāo)題設(shè)置為字符串’Dodger’:
pygame.display.set_caption('Dodger')
此標(biāo)題將顯示在窗口頂部的標(biāo)題欄中。
在 Dodger 中,鼠標(biāo)光標(biāo)不應(yīng)該可見。您希望鼠標(biāo)能夠移動(dòng)玩家角色在屏幕上移動(dòng),但鼠標(biāo)光標(biāo)會(huì)妨礙角色圖像。我們可以用一行代碼使鼠標(biāo)不可見:
pygame.mouse.set_visible(False)
調(diào)用pygame.mouse.set_visible(False)
告訴pygame
使光標(biāo)不可見。
設(shè)置字體、聲音和圖像對(duì)象
由于我們?cè)谶@個(gè)程序中在屏幕上顯示文本,我們需要為文本提供一個(gè)Font
對(duì)象給pygame
模塊使用。第 50 行通過調(diào)用pygame.font.SysFont()
創(chuàng)建了一個(gè)Font
對(duì)象:
# Set up the fonts.
font = pygame.font.SysFont(None, 48)
傳遞None
使用默認(rèn)字體。傳遞48
給字體一個(gè) 48 點(diǎn)的大小。
接下來,我們將創(chuàng)建Sound
對(duì)象并設(shè)置背景音樂:
# Set up sounds.
gameOverSound = pygame.mixer.Sound('gameover.wav')
pygame.mixer.music.load('background.mid')
pygame.mixer.Sound()
構(gòu)造函數(shù)創(chuàng)建一個(gè)新的Sound
對(duì)象,并將對(duì)此對(duì)象的引用存儲(chǔ)在gameOverSound
變量中。在您自己的游戲中,您可以創(chuàng)建任意數(shù)量的Sound
對(duì)象,每個(gè)對(duì)象都有不同的聲音文件。
pygame.mixer.music.load()
函數(shù)加載一個(gè)聲音文件用于背景音樂。這個(gè)函數(shù)不返回任何對(duì)象,一次只能加載一個(gè)背景音樂文件。背景音樂將在游戲期間持續(xù)播放,但Sound
對(duì)象只會(huì)在玩家撞到壞人而輸?shù)粲螒驎r(shí)播放。
您可以為這個(gè)游戲使用任何 WAV 或 MIDI 文件。一些聲音文件可以從本書的網(wǎng)站www.nostarch.com/inventwithpython/
下載。您也可以為這個(gè)游戲使用自己的聲音文件,只要您將文件命名為gameover.wav和background.mid,或者更改第 53 和 54 行使用的字符串以匹配您想要的文件名。
接下來,您將加載用于玩家角色和壞人的圖像文件:
# Set up images.
playerImage = pygame.image.load('player.png')
playerRect = playerImage.get_rect()
baddieImage = pygame.image.load('baddie.png')
角色的圖像存儲(chǔ)在player.png中,壞人的圖像存儲(chǔ)在baddie.png中。所有壞人看起來都一樣,所以你只需要一個(gè)圖像文件。您可以從本書的網(wǎng)站www.nostarch.com/inventwithpython/
下載這些圖像。
顯示開始畫面
游戲剛開始時(shí),Python 應(yīng)該在屏幕上顯示 Dodger 標(biāo)題。您還希望告訴玩家他們可以通過按任意鍵開始游戲。這個(gè)畫面出現(xiàn)是為了讓玩家在運(yùn)行程序后有時(shí)間準(zhǔn)備開始玩。
在 63 和 64 行,我們編寫代碼調(diào)用drawText()
函數(shù):
# Show the "Start" screen.
windowSurface.fill(BACKGROUNDCOLOR)
drawText('Dodger', font, windowSurface, (WINDOWWIDTH / 3),(WINDOWHEIGHT / 3))
drawText('Press a key to start.', font, windowSurface,(WINDOWWIDTH / 3) - 30, (WINDOWHEIGHT / 3) + 50)
pygame.display.update()
waitForPlayerToPressKey()
我們將向此函數(shù)傳遞五個(gè)參數(shù):
-
您希望出現(xiàn)的文本字符串
-
您希望字符串出現(xiàn)的字體
-
文本將被渲染到的
Surface
對(duì)象 -
在
Surface
對(duì)象上的 x 坐標(biāo),用于繪制文本 -
在
Surface
對(duì)象上的 y 坐標(biāo),用于繪制文本
這可能看起來是一個(gè)很多參數(shù)的函數(shù)調(diào)用,但請(qǐng)記住,每次調(diào)用此函數(shù)調(diào)用將替換五行代碼。這縮短了程序,并使查找錯(cuò)誤變得更容易,因?yàn)橐獧z查的代碼更少。
waitForPlayerToPressKey()
函數(shù)通過循環(huán)暫停游戲,直到生成KEYDOWN
事件。然后執(zhí)行中斷循環(huán),程序繼續(xù)運(yùn)行。
開始游戲
現(xiàn)在所有函數(shù)都已定義,我們可以開始編寫主游戲代碼。第 68 行及以后將調(diào)用我們之前定義的函數(shù)。程序首次運(yùn)行時(shí),topScore
變量的值為0
。每當(dāng)玩家輸?shù)粲螒虿⑶业梅执笥诋?dāng)前最高分時(shí),最高分將被替換為這個(gè)更大的分?jǐn)?shù)。
topScore = 0
while True:
從第 69 行開始的無限循環(huán)在技術(shù)上不是游戲循環(huán)。游戲循環(huán)處理游戲運(yùn)行時(shí)的事件和繪制窗口。相反,這個(gè)while
循環(huán)在每次玩家開始新游戲時(shí)迭代。當(dāng)玩家輸?shù)粲螒虿⑶矣螒蛑刂脮r(shí),程序的執(zhí)行會(huì)循環(huán)回到第 69 行。
一開始,您還希望將baddies
設(shè)置為空列表:
# Set up the start of the game.baddies = []score = 0
baddies
變量是一個(gè)包含以下鍵的字典對(duì)象列表:
'rect’描述了壞人的位置和大小的Rect
對(duì)象。
'speed’壞人下落的速度。這個(gè)整數(shù)表示每次游戲循環(huán)迭代的像素。
'surface’擁有縮放的壞人圖像繪制在上面的Surface
對(duì)象。這是繪制到pygame.display.set_mode()
返回的Surface
對(duì)象的Surface
。
第 72 行將玩家的分?jǐn)?shù)重置為0
。
玩家的起始位置在屏幕中央,距離底部 50 像素,由第 73 行設(shè)置:
playerRect.topleft = (WINDOWWIDTH / 2, WINDOWHEIGHT - 50)
第 73 行元組的第一個(gè)項(xiàng)目是左邊緣的 x 坐標(biāo),第二個(gè)項(xiàng)目是頂邊緣的 y 坐標(biāo)。
接下來,我們?cè)O(shè)置玩家移動(dòng)和作弊的變量:
moveLeft = moveRight = moveUp = moveDown = FalsereverseCheat = slowCheat = FalsebaddieAddCounter = 0
移動(dòng)變量moveLeft
、moveRight
、moveUp
和moveDown
都設(shè)置為False
。reverseCheat
和slowCheat
變量也設(shè)置為False
。只有當(dāng)玩家按住 Z 和 X 鍵啟用這些作弊時(shí),它們才會(huì)被設(shè)置為True
。
baddieAddCounter
變量是一個(gè)計(jì)數(shù)器,告訴程序何時(shí)在屏幕頂部添加一個(gè)新的壞人。baddieAddCounter
的值每次游戲循環(huán)迭代時(shí)增加 1。(這類似于“添加新食物方塊”中的代碼在第 295 頁(yè)。)
當(dāng)baddieAddCounter
等于ADDNEWBADDIERATE
時(shí),baddieAddCounter
重置為0
,并在屏幕頂部添加一個(gè)新的壞人。(這個(gè)檢查稍后在第 130 行進(jìn)行。)
背景音樂在第 77 行開始播放,調(diào)用了pygame.mixer.music.play()
函數(shù):
pygame.mixer.music.play(-1, 0.0)
因?yàn)榈谝粋€(gè)參數(shù)是-1
,pygame
會(huì)無限重復(fù)播放音樂。第二個(gè)參數(shù)是一個(gè)浮點(diǎn)數(shù),表示音樂開始播放的秒數(shù)。傳遞0.0
意味著音樂從頭開始播放。
游戲循環(huán)
游戲循環(huán)的代碼不斷更新游戲世界的狀態(tài),改變玩家和壞人的位置,處理由pygame
生成的事件,并在屏幕上繪制游戲世界。所有這些都會(huì)在幾十次每秒發(fā)生,使游戲?qū)崟r(shí)運(yùn)行。
第 79 行是主游戲循環(huán)的開始:
while True: # The game loop runs while the game part is playing.score += 1 # Increase score.
第 80 行在游戲循環(huán)的每次迭代中增加玩家的分?jǐn)?shù)。玩家能夠在不失去的情況下走得越久,他們的分?jǐn)?shù)就越高。循環(huán)只有在玩家輸?shù)粲螒蚧蛲顺龀绦驎r(shí)才會(huì)退出。
處理鍵盤事件
程序?qū)⑻幚硭姆N類型的事件:QUIT
、KEYDOWN
、KEYUP
和MOUSEMOTION
。
第 82 行是事件處理代碼的開始:
for event in pygame.event.get():if event.type == QUIT:terminate()
它調(diào)用pygame.event.get()
,返回一個(gè)Event
對(duì)象列表。每個(gè)Event
對(duì)象表示自上次調(diào)用pygame.event.get()
以來發(fā)生的事件。代碼檢查Event
對(duì)象的type
屬性,看看它是什么類型的事件,然后相應(yīng)地處理它。
如果Event
對(duì)象的type
屬性等于QUIT
,那么用戶已經(jīng)關(guān)閉了程序。QUIT
常量變量是從pygame.locals
模塊導(dǎo)入的。
如果事件的類型是KEYDOWN
,玩家已經(jīng)按下了一個(gè)鍵:
if event.type == KEYDOWN:if event.key == K_z:reverseCheat = Trueif event.key == K_x:slowCheat = True
第 87 行檢查事件是否描述了按下Z
鍵,條件為event.key == K_z
。如果條件為True
,Python 將reverseCheat
變量設(shè)置為True
以激活反向作弊。類似地,第 89 行檢查是否按下X
鍵以激活減速作弊。
第 91 到 102 行檢查事件是否由玩家按下箭頭或 WASD 鍵生成。這段代碼類似于前幾章的與鍵盤相關(guān)的代碼。
如果事件的類型是KEYUP
,玩家已經(jīng)釋放了一個(gè)鍵:
if event.type == KEYUP:if event.key == K_z:reverseCheat = Falsescore = 0if event.key == K_x:slowCheat = Falsescore = 0
第 105 行檢查玩家是否釋放了 Z 鍵,這將停用反向作弊。在這種情況下,第 106 行將reverseCheat
設(shè)置為False
,第 107 行將分?jǐn)?shù)重置為0
。分?jǐn)?shù)重置是為了阻止玩家使用作弊。
第 108 行到第 110 行對(duì) X 鍵和慢速作弊做了同樣的事情。釋放 X 鍵時(shí),slowCheat
設(shè)置為False
,玩家的分?jǐn)?shù)重置為0
。
在游戲進(jìn)行期間,玩家可以隨時(shí)按 ESC 鍵退出:
if event.key == K_ESCAPE:terminate()
第 111 行通過檢查event.key == K_ESCAPE
來確定釋放的鍵是否是 ESC。如果是,第 112 行調(diào)用terminate()
函數(shù)退出程序。
第 114 行到第 121 行檢查玩家是否停止按住箭頭或 WASD 鍵之一。在這種情況下,代碼將相應(yīng)的移動(dòng)變量設(shè)置為False
。這類似于第 19 章和第 20 章程序中的移動(dòng)代碼。
處理鼠標(biāo)移動(dòng)
現(xiàn)在你已經(jīng)處理了鍵盤事件,讓我們處理可能生成的任何鼠標(biāo)事件。《躲避球》游戲如果玩家點(diǎn)擊了鼠標(biāo)按鈕,不會(huì)有任何反應(yīng),但是當(dāng)玩家移動(dòng)鼠標(biāo)時(shí)會(huì)有反應(yīng)。這給玩家在游戲中控制角色的兩種方式:鍵盤或鼠標(biāo)。
MOUSEMOTION
事件在鼠標(biāo)移動(dòng)時(shí)生成:
if event.type == MOUSEMOTION:# If the mouse moves, move the player to the cursor.playerRect.centerx = event.pos[0]playerRect.centery = event.pos[1]
type
設(shè)置為MOUSEMOTION
的Event
對(duì)象還有一個(gè)名為pos
的屬性,用于存儲(chǔ)鼠標(biāo)事件的位置。pos
屬性存儲(chǔ)了鼠標(biāo)光標(biāo)在窗口中移動(dòng)的 x 和 y 坐標(biāo)的元組。如果事件的類型是MOUSEMOTION
,玩家的角色將移動(dòng)到鼠標(biāo)光標(biāo)的位置。
第 125 行和第 126 行將玩家角色的中心 x 和 y 坐標(biāo)設(shè)置為鼠標(biāo)光標(biāo)的 x 和 y 坐標(biāo)。
添加新的壞蛋
在游戲循環(huán)的每次迭代中,代碼將baddieAddCounter
變量增加一:
# Add new baddies at the top of the screen, if needed.if not reverseCheat and not slowCheat:baddieAddCounter += 1
只有在作弊未啟用時(shí)才會(huì)發(fā)生。請(qǐng)記住,只要按住 Z 和 X 鍵,reverseCheat
和slowCheat
就會(huì)設(shè)置為True
。在按住 Z 和 X 鍵時(shí),baddieAddCounter
不會(huì)增加。因此,新的壞蛋不會(huì)出現(xiàn)在屏幕頂部。
當(dāng)baddieAddCounter
達(dá)到ADDNEWBADDIERATE
中的值時(shí),是時(shí)候在屏幕頂部添加一個(gè)新的壞蛋了。首先,將baddieAddCounter
重置為0
:
if baddieAddCounter == ADDNEWBADDIERATE:baddieAddCounter = 0baddieSize = random.randint(BADDIEMINSIZE, BADDIEMAXSIZE)newBaddie = {'rect': pygame.Rect(random.randint(0,WINDOWWIDTH - baddieSize), 0 - baddieSize,baddieSize, baddieSize),'speed': random.randint(BADDIEMINSPEED,BADDIEMAXSPEED),'surface':pygame.transform.scale(baddieImage,(baddieSize, baddieSize)),}
第 132 行生成了壞蛋的像素大小。大小將是BADDIEMINSIZE
和BADDIEMAXSIZE
之間的隨機(jī)整數(shù),這些常量分別在第 9 行和第 10 行設(shè)置為10
和40
。
第 133 行是創(chuàng)建新壞蛋數(shù)據(jù)結(jié)構(gòu)的地方。請(qǐng)記住,baddies
的數(shù)據(jù)結(jié)構(gòu)只是一個(gè)帶有鍵'rect'
、'speed'
和'surface'
的字典。'rect'
鍵保存對(duì)存儲(chǔ)壞蛋位置和大小的Rect
對(duì)象的引用。對(duì)pygame.Rect()
構(gòu)造函數(shù)的調(diào)用有四個(gè)參數(shù):區(qū)域頂部邊緣的 x 坐標(biāo)、區(qū)域左邊緣的 y 坐標(biāo)、像素寬度和像素高度。
壞蛋需要出現(xiàn)在窗口頂部的隨機(jī)位置,因此將random.randint(0, WINDOWWIDTH - baddieSize)
傳遞給壞蛋左邊緣的 x 坐標(biāo)。之所以傳遞WINDOWWIDTH - baddieSize
而不是WINDOWWIDTH
,是因?yàn)槿绻麎牡暗淖筮吘壧坑?#xff0c;那么壞蛋的一部分將超出窗口邊緣,不會(huì)在屏幕上可見。
壞蛋的底邊應(yīng)該位于窗口頂邊的上方。窗口頂邊的 y 坐標(biāo)是0
。為了將壞蛋的底邊放在那里,將頂邊設(shè)置為0 - baddieSize
。
壞蛋的寬度和高度應(yīng)該相同(圖像是一個(gè)正方形),因此將baddieSize
傳遞給第三個(gè)和第四個(gè)參數(shù)。
壞人在屏幕上移動(dòng)的速度設(shè)置在'speed'
鍵中。將其設(shè)置為BADDIEMINSPEED
和BADDIEMAXSPEED
之間的隨機(jī)整數(shù)。
然后,在第 138 行,將新創(chuàng)建的壞人數(shù)據(jù)結(jié)構(gòu)添加到壞人數(shù)據(jù)結(jié)構(gòu)列表中:
baddies.append(newBaddie)
程序使用這個(gè)列表來檢查玩家是否與任何壞人發(fā)生了碰撞,并確定在窗口上繪制壞人的位置。
移動(dòng)玩家角色和壞人
四個(gè)移動(dòng)變量moveLeft
、moveRight
、moveUp
和moveDown
在pygame
生成KEYDOWN
和KEYUP
事件時(shí)分別設(shè)置為True
和`False。
如果玩家的角色向左移動(dòng),并且玩家角色的左邊緣大于0
(即窗口的左邊緣),那么playerRect
應(yīng)該向左移動(dòng):
# Move the player around.if moveLeft and playerRect.left > 0:playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
move_ip()
方法將Rect
對(duì)象的位置水平或垂直移動(dòng)一定數(shù)量的像素。move_ip()
的第一個(gè)參數(shù)是將Rect
對(duì)象向右移動(dòng)的像素?cái)?shù)(要向左移動(dòng),傳遞一個(gè)負(fù)整數(shù))。第二個(gè)參數(shù)是將Rect
對(duì)象向下移動(dòng)的像素?cái)?shù)(要向上移動(dòng),傳遞一個(gè)負(fù)整數(shù))。例如,playerRect.move_ip(10, 20)
將使Rect
對(duì)象向右移動(dòng) 10 個(gè)像素,向下移動(dòng) 20 個(gè)像素,playerRect.move_ip(-5, -15)
將使Rect
對(duì)象向左移動(dòng) 5 個(gè)像素,向上移動(dòng) 15 個(gè)像素。
move_ip()
末尾的ip代表“原地”。這是因?yàn)樵摲椒ǜ淖兞?code>Rect對(duì)象本身,而不是返回具有更改的新Rect
對(duì)象。還有一個(gè)move()
方法,它不會(huì)改變Rect
對(duì)象,而是在新位置創(chuàng)建并返回一個(gè)新的Rect
對(duì)象。
你總是會(huì)移動(dòng)playerRect
對(duì)象的像素?cái)?shù)為PLAYERMOVERATE
。要得到一個(gè)整數(shù)的負(fù)形式,將其乘以-1
。在第 142 行,由于PLAYERMOVERATE
中存儲(chǔ)了5
,表達(dá)式-1 * PLAYERMOVERATE
的值為-5
。因此,調(diào)用playerRect.move_ip(-1 * PLAYERMOVERATE, 0)
將使playerRect
的位置向左移動(dòng) 5 個(gè)像素。
第 143 到 148 行對(duì)其他三個(gè)方向進(jìn)行了相同的操作:右、上和下。
if moveRight and playerRect.right < WINDOWWIDTH:playerRect.move_ip(PLAYERMOVERATE, 0)if moveUp and playerRect.top > 0:playerRect.move_ip(0, -1 * PLAYERMOVERATE)if moveDown and playerRect.bottom < WINDOWHEIGHT:playerRect.move_ip(0, PLAYERMOVERATE)
在第 143 到 148 行的三個(gè)if
語(yǔ)句中,檢查其移動(dòng)變量是否設(shè)置為True
,并且玩家的Rect
對(duì)象的邊緣是否在窗口內(nèi)。然后調(diào)用move_ip()
來移動(dòng)Rect
對(duì)象。
現(xiàn)在,代碼循環(huán)遍歷baddies
列表中的每個(gè)壞人數(shù)據(jù)結(jié)構(gòu),使它們向下移動(dòng)一點(diǎn):
# Move the baddies down.for b in baddies:if not reverseCheat and not slowCheat:b['rect'].move_ip(0, b['speed'])
如果沒有激活任何作弊碼,那么壞人的位置向下移動(dòng)與其速度(存儲(chǔ)在'speed'
鍵中)相等的像素?cái)?shù)。
實(shí)現(xiàn)作弊碼
如果反向作弊被激活,那么壞人應(yīng)該向上移動(dòng) 5 個(gè)像素:
elif reverseCheat:b['rect'].move_ip(0, -5)
將move_ip()
的第二個(gè)參數(shù)傳遞為-5
將使Rect
對(duì)象向上移動(dòng) 5 個(gè)像素。
如果慢速作弊被激活,那么壞人仍然應(yīng)該向下移動(dòng),但速度為每次游戲循環(huán)迭代 1 個(gè)像素:
elif slowCheat:b['rect'].move_ip(0, 1)
當(dāng)慢速作弊被激活時(shí),壞人的正常速度(同樣存儲(chǔ)在壞人數(shù)據(jù)結(jié)構(gòu)的'speed'
鍵中)將被忽略。
移除壞人
任何掉到窗口底部以下的壞人都應(yīng)該從baddies
列表中移除。記住,不應(yīng)該在迭代列表時(shí)添加或移除列表項(xiàng)。不要使用for
循環(huán)迭代baddies
列表,而是使用baddies
列表的副本進(jìn)行迭代。要?jiǎng)?chuàng)建這個(gè)副本,使用空切片操作符[:]
:
# Delete baddies that have fallen past the bottom.for b in baddies[:]:
第 160 行的for
循環(huán)使用變量b
來遍歷baddies[:]
中的當(dāng)前項(xiàng)。如果壞人在窗口的底部以下,我們應(yīng)該將其移除,這在第 162 行中完成:
if b['rect'].top > WINDOWHEIGHT:baddies.remove(b)
b
字典是baddies[:]
列表中的當(dāng)前壞蛋數(shù)據(jù)結(jié)構(gòu)。列表中的每個(gè)壞蛋數(shù)據(jù)結(jié)構(gòu)都是一個(gè)帶有'rect'
鍵的字典,該鍵存儲(chǔ)一個(gè)Rect
對(duì)象。因此,b['rect']
是壞蛋的Rect
對(duì)象。最后,top
屬性是矩形區(qū)域頂部邊緣的 y 坐標(biāo)。請(qǐng)記住,y 坐標(biāo)向下增加。因此,b['rect'].top > WINDOWHEIGHT
將檢查壞蛋的頂部邊緣是否在窗口底部以下。如果這個(gè)條件為True
,那么第 162 行將從baddies
列表中刪除壞蛋數(shù)據(jù)結(jié)構(gòu)。
繪制窗口
在更新所有數(shù)據(jù)結(jié)構(gòu)之后,應(yīng)使用pygame
的圖像函數(shù)繪制游戲世界。因?yàn)橛螒蜓h(huán)每秒執(zhí)行多次,當(dāng)壞蛋和玩家在新位置繪制時(shí),它們看起來就像是平穩(wěn)移動(dòng)的。
在繪制任何其他內(nèi)容之前,第 165 行填充整個(gè)屏幕以擦除先前繪制的任何內(nèi)容:
# Draw the game world on the window.windowSurface.fill(BACKGROUNDCOLOR)
請(qǐng)記住,windowSurface
中的Surface
對(duì)象很特殊,因?yàn)樗怯?code>pygame.display.set_mode()返回的。因此,在該Surface
對(duì)象上繪制的任何內(nèi)容都將在調(diào)用pygame.display.update()
后出現(xiàn)在屏幕上。
繪制玩家得分
第 168 和 169 行在窗口的左上角渲染了當(dāng)前得分和最高得分的文本。
# Draw the score and top score.drawText('Score: %s' % (score), font, windowSurface, 10, 0)drawText('Top Score: %s' % (topScore), font, windowSurface,10, 40)
'Score: %s' % (score)
表達(dá)式使用字符串插值將score
變量的值插入字符串中。這個(gè)字符串、存儲(chǔ)在font
變量中的Font
對(duì)象、用于繪制文本的Surface
對(duì)象,以及文本應(yīng)放置的 x 和 y 坐標(biāo)都被傳遞給drawText()
方法,該方法將處理對(duì)render()
和blit()
方法的調(diào)用。
對(duì)于最高得分,做同樣的事情。將40
作為 y 坐標(biāo)傳遞,而不是0
,這樣最高得分的文本就會(huì)出現(xiàn)在當(dāng)前得分的文本下方。
繪制玩家角色和壞蛋
關(guān)于玩家的信息保存在兩個(gè)不同的變量中。playerImage
是一個(gè)包含玩家角色圖像的所有彩色像素的Surface
對(duì)象。playerRect
是一個(gè)存儲(chǔ)玩家角色大小和位置的Rect
對(duì)象。
blit()
方法在windowSurface
上繪制玩家角色的圖像(在playerImage
中)在playerRect
的位置:
# Draw the player's rectangle.windowSurface.blit(playerImage, playerRect)
第 175 行的for
循環(huán)在windowSurface
對(duì)象上繪制每個(gè)壞蛋:
# Draw each baddie.for b in baddies:windowSurface.blit(b['surface'], b['rect'])
baddies
列表中的每個(gè)項(xiàng)目都是一個(gè)字典。字典的'surface'
和'rect'
鍵包含了帶有壞蛋圖像的Surface
對(duì)象和帶有位置和大小信息的Rect
對(duì)象。
現(xiàn)在,所有內(nèi)容都已經(jīng)繪制到windowSurface
上,我們需要更新屏幕,以便玩家可以看到其中的內(nèi)容:
pygame.display.update()
通過調(diào)用update()
將這個(gè)Surface
對(duì)象繪制到屏幕上。
檢查碰撞
第 181 行檢查玩家是否與任何壞蛋發(fā)生碰撞,調(diào)用playerHasHitBaddie()
。如果玩家的角色與baddies
列表中的任何一個(gè)壞蛋發(fā)生碰撞,則此函數(shù)將返回True
。否則,該函數(shù)返回False
。
# Check if any of the baddies have hit the player.if playerHasHitBaddie(playerRect, baddies):if score > topScore:topScore = score # Set new top score.break
如果玩家的角色撞到了壞蛋,并且當(dāng)前得分高于最高得分,那么第 182 和 183 行將更新最高得分。程序的執(zhí)行會(huì)在第 184 行跳出游戲循環(huán),并移動(dòng)到第 189 行,結(jié)束游戲。
為了防止計(jì)算機(jī)盡可能快地運(yùn)行游戲循環(huán)(這對(duì)玩家來說太快了),調(diào)用mainClock.tick()
來暫停游戲很短的時(shí)間:
mainClock.tick(FPS)
這個(gè)暫停時(shí)間將足夠長(zhǎng),以確保每秒大約進(jìn)行40
次(存儲(chǔ)在FPS
變量?jī)?nèi)部的值)游戲循環(huán)迭代。
游戲結(jié)束畫面
當(dāng)玩家失敗時(shí),游戲停止播放背景音樂,并播放“游戲結(jié)束”音效:
# Stop the game and show the "Game Over" screen.pygame.mixer.music.stop()gameOverSound.play()
第 189 行調(diào)用pygame.mixer.music
模塊中的stop()
函數(shù)來停止背景音樂。第 190 行調(diào)用gameOverSound
中存儲(chǔ)的Sound
對(duì)象的play()
方法。
然后,第 192 行和第 193 行調(diào)用drawText()
函數(shù)將“游戲結(jié)束”文本繪制到windowSurface
對(duì)象上:
drawText('GAME OVER', font, windowSurface, (WINDOWWIDTH / 3),(WINDOWHEIGHT / 3))drawText('Press a key to play again.', font, windowSurface,(WINDOWWIDTH / 3) - 80, (WINDOWHEIGHT / 3) + 50)pygame.display.update()waitForPlayerToPressKey()
第 194 行調(diào)用update()
來將這個(gè)Surface
對(duì)象繪制到屏幕上。在顯示這個(gè)文本后,游戲會(huì)停止,直到玩家按下鍵,調(diào)用waitForPlayerToPressKey()
函數(shù)。
玩家按下鍵后,程序執(zhí)行從第 195 行的waitForPlayerToPressKey()
調(diào)用返回。根據(jù)玩家按鍵的時(shí)間長(zhǎng)短,可能會(huì)播放“游戲結(jié)束”音效。為了在新游戲開始之前停止這個(gè)音效,第 197 行調(diào)用gameOverSound.stop()
:
gameOverSound.stop()
我們的圖形游戲就到這里了!
修改躲避者游戲
你可能會(huì)發(fā)現(xiàn)游戲太容易或太難。幸運(yùn)的是,游戲很容易修改,因?yàn)槲覀兓〞r(shí)間使用常量變量而不是直接輸入值?,F(xiàn)在,我們只需要修改常量變量中設(shè)置的值就可以改變游戲。
例如,如果你想讓游戲總體運(yùn)行速度變慢,可以將第 8 行的FPS
變量更改為較小的值,比如20
。這將使壞人和玩家角色移動(dòng)得更慢,因?yàn)橛螒蜓h(huán)每秒只執(zhí)行20
次,而不是40
次。
如果你只想減慢壞人的速度而不是玩家的速度,那么將BADDIEMAXSPEED
更改為較小的值,比如4
。這將使所有壞人在游戲循環(huán)中的每次迭代之間移動(dòng) 1(BADDIEMINSPEED
中的值)到 4 個(gè)像素,而不是 1 到 8 個(gè)像素。
如果你想讓游戲有更少但更大的壞人,而不是許多較小的壞人,那么將ADDNEWBADDIERATE
增加到12
,BADDIEMINSIZE
增加到40
,BADDIEMAXSIZE
增加到80
。現(xiàn)在,壞人每 12 次游戲循環(huán)添加一次,而不是每 6 次,所以壞人的數(shù)量將減少一半。但為了保持游戲的趣味性,壞人會(huì)更大。
保持基本游戲不變,你可以修改任何常量變量,從而顯著影響游戲的玩法。不斷嘗試新的常量變量值,直到找到最喜歡的值組合。
總結(jié)
與我們的文本游戲不同,躲避者看起來真的像一款現(xiàn)代電腦游戲。它有圖形和音樂,并且使用鼠標(biāo)。雖然pygame
提供函數(shù)和數(shù)據(jù)類型作為構(gòu)建塊,但是你作為程序員將它們組合在一起,創(chuàng)造出有趣的互動(dòng)游戲。
你可以做到這一切,因?yàn)槟阒廊绾沃鸩街笇?dǎo)計(jì)算機(jī)做事,一行一行地。通過使用計(jì)算機(jī)的語(yǔ)言,你可以讓它為你進(jìn)行數(shù)字計(jì)算和繪圖。這是一項(xiàng)有用的技能,我希望你會(huì)繼續(xù)學(xué)習(xí)更多關(guān)于 Python 編程的知識(shí)。(還有很多東西要學(xué)!)
現(xiàn)在開始發(fā)揮你的想象力,創(chuàng)造屬于自己的游戲。祝你好運(yùn)!
上一頁(yè):第 20 章 - 使用聲音和圖像