在這個遊戲中,玩家通過控制屏幕中的精靈,躲開前方隨機出現的敵人。玩家躲避敵人的時間越久,得到的分數就越高。
爲了好玩,我們還會爲遊戲加入一些作弊模式。如果玩家按下了X鍵,每一個敵人的速度就會降低到超慢。如果玩家按下了Z鍵,敵人就會反轉方向,沿着屏幕向上移動而不是往下落。
目錄
(一)回顧pygame的基本數據類型
- pygame.Rect:Rect對象表示一個矩形空間的位置和大小。位置可以通過Rect對象的topleft(或者topright、bottomleft和bottomright)屬性來確定。這些屬性是表示X座標和Y座標的整數的一個元組。矩形的大小可以通過width屬性和height屬性來決定,這些屬性表示矩形區域的長和高是多少像素。Rect對象有一個colliderect()方法,用來檢查矩形是否和其他Rect對象有碰撞。
- pygame.Surface:Surface對象是帶顏色的像素的區域。Surface對象表示一個矩形圖像,而Rect對象只表示一個矩形的空間和位置。Surface對象有一個blit()方法,它將一個Surface對象上的圖像繪製到另一個Surface對象之上。由pygame.display.set_mode()函數返回的Surface對象是特殊的,因爲當調用pygame.display.update()函數時,在該Surface對象上繪製的任何物體都會顯示在用戶屏幕上。
- pygame.event.Event:當用戶提供鍵盤、鼠標或其他類型的輸入時,pygame.event模塊會創建Event對象。pygame.event.get()函數返回這些Event對象的一個列表。可以通過查看Event對象的type屬性,來查看事件的類型。QUIT、KEYDOWN和MOUSEBUTTONUP是一些事件類型的示例。
- pygame.font.Font:pygame.font模塊擁有一個Font數據類型,用於表示pygame中的文本的字體。傳遞給pygame.font.SysFont()函數的參數是表示字體名稱的一個字符串以及表示字體大小的一個整數。然而,通常傳遞None作爲字體名稱以獲取默認系統字體。
- pygame.time.Clock:pygame.time模塊中的Clock對象有助於避免程序運行得過快。Clock對象有一個tick()方法,它接收的參數表示想要遊戲運行速度是每秒多少幀(FPS)。FPS越高,遊戲運行越快。
(二)導入模塊
import pygame, random, sys
from pygame.locals import *
pygame.locals模塊提供了幾個供pygame使用的常量,如事件類型(QUIT和KEYDOWN等)和鍵盤按鍵(K_ESCAPE和K_LEFT)等。通過使用from pygame.locals import *語句,我們可以在源代碼中只輸入QUIT,而不必輸入pygame.locals.QUIT。*代表所有pygame.locals導出的對象名稱。
(三)創建常量
WINDOWWIDTH = 600
WINDOWHEIGHT = 600
TEXTCOLOR = (0, 0, 0)
BACKGROUNDCOLOR = (255, 255, 255)
以上代碼爲窗口大小、文本顏色和背景顏色設置了常量。將常量賦值給變量,要比直接輸入常量更具有描述性,而且也便於修改。
接下來我們爲FPS設置了常量,這是想要讓遊戲按照每秒多少幀的速度運行:
FPS = 60
幀(frame是在遊戲循環的單次迭代中所繪製的一個屏幕)。
後面跟代碼中,我們將FPS傳遞給mainClock.tick方法,以便函數可以知道程序要暫停多久。
下面我們設置了更多的常量,用來描述落下的敵人。
BADDIEMINSIZE = 10
BADDIEMAXSIZE = 40
BADDIEMINSPEED = 1
BADDIEMAXSPEED = 8
ADDNEWBADDIERATE = 6
敵人的寬度和高度均在BADDIEMINSIZE和BADVIEMAXSIZE之間。在遊戲循環的每次迭代中,敵人從屏幕上落下的速率在每秒BADDIEMINSPEED到BADDIEMINSPEED多個像素之間。遊戲循環每經過ADDNEWBADDIERATE次迭代之後,將在窗口的頂部增加一個新的敵人。
如果玩家的角色是移動的,在遊戲循環的每次迭代中,PLAYERMOVERATE將保存玩家的角色在窗口中移動的像素數。
PLAYERMOVERATE = 5
通過增大這個數字,就可以加快角色的移動速度。
(四)定義函數
我們將爲這個遊戲創建幾個函數。terminate()和waitForPlayerToPressKey()函數將分別終止和暫停程序,playerHasHitBaddie()函數將會記錄玩家和敵人的碰撞,drawText()函數將會把得分和其他的文本繪製到屏幕上。
1)終止和暫停遊戲
def terminate():
pygame.quit()
sys.exit()
把pygame.quit()和sys.exit()放到一個函數裏面。
有時,我們想要暫停遊戲,直到玩家按下一個鍵再繼續遊戲。這裏體現在Dodger標題文本剛出現的時候,需要玩家隨意按下一個鍵來開始遊戲,或者在出現Game Over且遊戲結束的時候。我們創建了一個waitForPlayerToPressKey()函數:
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()
return
在這個函數中有一個無限循環,只有當接收到一個KEYDOWN或QUIT事件時,纔會跳出該循環。否則代碼將包初循環,看上去就像是凍結了,直到玩家按下一個鍵。
2)記錄和敵人的碰撞
如果玩家的角色和一個敵人碰撞,playerHasHitBaddie()函數將返回True。
def playerHasHitBaddie(playerRect, baddies):
for b in baddies:
if playerRect.colliderect(b['rect']):
return True
return False
baddies參數是“baddie”字典數據結構的一個列表。其中每一個字典都有rect鍵,該鍵的值是表示敵人大小和位置的一個Rect對象。
playerRect也是一個Rect對象。playerRect對象和baddies列表中的Rect對象如果發生碰撞,則該方法返回True。
如果未發生碰撞,該函數返回False。
3)將文本繪製到窗口
在窗口繪製文本包含了幾個步驟,我們把這些步驟放到drawText()函數中去。當我們呢需要在屏幕上顯示文本的時候,只需要調用drawText()函數即可:
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)
首先調用render()方法創建了一個Surface對象,文本以特定的字體渲染其上。
接下來調用Surface對象的get_rect()方法來獲取Surface對象的大小和位置,返回一個Rect對象的大小和位置。
然後重新設置這個Rect對象的topleft屬性,來改變它的位置。
最後,將其上已經繪製了文本的Surface,繪製到了作爲參數傳遞給drawText()函數的Surface上。
(五)初始化pygame並設置窗口
# 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)
初始化、以及創建一個pygame.time.Clock()對象、創建Surface對象、設置窗口的標題,和前面的相同,這裏就不贅述了。
pygame.display.set_mode()函數的第2個參數是可選的。我們可以傳遞pygame.FULLSCREEN常量,使得窗口占據整個屏幕,而不是顯示爲一個小窗口。即修改爲:
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT),pygame.FULLSCREEN)
在Dodger中,鼠標光標應該是不可見的。我們想要鼠標能夠在屏幕上移動玩家的角色,但是,鼠標的光標可能會擋住角色的圖像,只需要一行代碼,就可以讓鼠標不可見:
pygame.mouse.set_visible(False)
告訴pygame,讓鼠標光標不可見。
(六)設置Font、Sound和Image對象
# Set up the fonts.
font = pygame.font.SysFont(None, 48)
# Set up sounds.
gameOverSound = pygame.mixer.Sound('pickup.wav')
pygame.mixer.music.load('www.mp3')
# Set up images.
playerImage = pygame.image.load('player.png')
playerRect = playerImage.get_rect()
baddieImage = pygame.image.load('cherry.png')
調用pygame.font.SysFont()函數創建了一個Font對象,跟隨系統字體,設置字體的大小爲48.
加載遊戲背景音樂和玩家碰到敵人時播放的音樂。
最後加載圖像文件,使用get_rect()方法獲取Rect對象的大小和位置,賦值給playerRect對象。
(七)顯示開始界面
# 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
topScore變量用來記錄最高分,後面代碼中將這個最高分顯示在屏幕上。
(八)遊戲循環
遊戲循環的代碼通過修改玩家和敵人的位置、處理pygame生成的事件以及在屏幕上繪製遊戲世界,來不斷地更新遊戲世界地狀態。所有這些事情會在1秒鐘內發生很多次,這使得遊戲“實時”地運行。
每進行一次while循環,score就會增加1,score增加地頻率也就是遊戲循環地頻率。
while True:
# Set up the start of the game.
baddies = []
score = 0
playerRect.topleft = (WINDOWWIDTH / 2, WINDOWHEIGHT - 50)
moveLeft = moveRight = moveUp = moveDown = False
reverseCheat = slowCheat = False
baddieAddCounter = 0
pygame.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 = True
if event.key == K_x:
slowCheat = True
if event.key == K_LEFT or event.key == K_a:
moveRight = False
moveLeft = True
if event.key == K_RIGHT or event.key == K_d:
moveLeft = False
moveRight = True
if event.key == K_UP or event.key == K_w:
moveDown = False
moveUp = True
if event.key == K_DOWN or event.key == K_s:
moveUp = False
moveDown = True
if event.type == KEYUP:
if event.key == K_z:
reverseCheat = False
score = 0
if event.key == K_x:
slowCheat = False
score = 0
if event.key == K_ESCAPE:
terminate()
if event.key == K_LEFT or event.key == K_a:
moveLeft = False
if event.key == K_RIGHT or event.key == K_d:
moveRight = False
if event.key == K_UP or event.key == K_w:
moveUp = False
if event.key == K_DOWN or event.key == K_s:
moveDown = False
if 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 += 1
if baddieAddCounter == ADDNEWBADDIERATE:
baddieAddCounter = 0
baddieSize = 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.
break
mainClock.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()
playerRect爲玩家圖像的位置和大小的Rect對象。
1)事件處理
有4中不同的事件要處理:QUIT、KEYDOWN、KEYUP、和MOUSEMOTION。
處理鍵盤事件
for event in pygame.event.get():
if event.type == QUIT:
terminate()
if event.type == KEYDOWN:
if event.key == K_z:
reverseCheat = True
if event.key == K_x:
slowCheat = True
if event.key == K_LEFT or event.key == K_a:
moveRight = False
moveLeft = True
if event.key == K_RIGHT or event.key == K_d:
moveLeft = False
moveRight = True
if event.key == K_UP or event.key == K_w:
moveDown = False
moveUp = True
if event.key == K_DOWN or event.key == K_s:
moveUp = False
moveDown = True
if event.type == KEYUP:
if event.key == K_z:
reverseCheat = False
score = 0
if event.key == K_x:
slowCheat = False
score = 0
if event.key == K_ESCAPE:
terminate()
if event.key == K_LEFT or event.key == K_a:
moveLeft = False
if event.key == K_RIGHT or event.key == K_d:
moveRight = False
if event.key == K_UP or event.key == K_w:
moveUp = False
if event.key == K_DOWN or event.key == K_s:
moveDown = False
KEYDOWN事件實現上下左右的移動,同時增加了Z和X鍵。
按Z鍵實現敵人反向運動,釋放Z鍵,將解除反向作弊模式。
按X鍵降低敵人移動速度,釋放X鍵,敵人移動速度 回覆正常。
處理鼠標事件
如果玩家點擊鼠標按鍵,Dodger不會做任何事情,但是當玩家移動鼠標的時候,遊戲會做出相應。這就使得玩家在遊戲中可以有兩種方法來控制玩家角色:鼠標和鍵盤。
if event.type == MOUSEMOTION:
# If the mouse moves, move the player to the cursor.
playerRect.centerx = event.pos[0]
playerRect.centery = event.pos[1]
MOUSEMOTION類型的Event對象,也有一個名爲pos的屬性,表示鼠標事件的位置。pos屬性保存了一個元組,是鼠標光標在窗口中移動到的位置的X座標和Y座標。如果事件的類型是MOUSEMOTION,玩家的角色將移動到鼠標光標的位置。
2)增加新的敵人
# Add new baddies at the top of the screen, if needed.
if not reverseCheat and not slowCheat:
baddieAddCounter += 1
if baddieAddCounter == ADDNEWBADDIERATE:
baddieAddCounter = 0
baddieSize = 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)
在遊戲循環的每一次迭代中,變量baddieAddCounter都會加1.
只有作弊模式未啓用纔會這麼做。記住,只要分別按下Z鍵和X鍵,就會把reverseCheat和slowCheat設置爲True。
當按下X或者Z鍵時,baddieAddCounter就不會增加。因此也不會有新的敵人會出現在屏幕的頂部。
當baddieAddCounter等於ADDNEWBADDIERATE中的值時,就會在屏幕增加一個新的敵人。ADDNEWBADDIERATE變量在程序最開始的地方進行的定義,表示敵人增加的速率。
3)移動玩家角色和敵人
當pygame觸發了KEYDOWN 和KEYUP事件時,把4個移動變量moveLeft、moveRight、moveUp和moveDown分別設置爲True和False。
move_ip()方法將會把Rect對象的位置水平地或垂直地移動多個像素。move_ip()的第1個參數是要將Rect對象向右移動多少個像素(如果要向左移動,就給這個參數傳入負值)。第2個參數是要將Rect對象向下移動多少個像素(要向上移動,就給這個參數傳入負值)。
# 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)
實現作弊模式
elif reverseCheat:
b['rect'].move_ip(0, -5)
elif slowCheat:
b['rect'].move_ip(0, 1)
如果激活了反向作弊模式,那麼敵人應該以5個像素的速度向上移動。
如果沒有激活任何的作弊模式,那麼向下移動敵人位置的像素數目就等於它的速度(該值存儲在'speed'鍵中)。
刪除敵人
任何跌落到窗口底邊之下的敵人都應該從baddies列表中刪除。記住,當遍歷一個列表時,不能增加或刪除元素以修改列表中的內容。所以for循環遍歷的不是baddies列表,而是這個baddies列表的一個副本。
# Delete baddies that have fallen past the bottom.
for b in baddies[:]:
if b['rect'].top > WINDOWHEIGHT:
baddies.remove(b)
4)繪製窗口
# 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.
break
mainClock.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()
要注意的是,最後調用waitForPlayerToPressKey()函數等待的過程中,要結束播放"Game Over"。所以調用stop()方法來實現。
以上就是我們的圖形化遊戲。
(九)修改Dodger遊戲
你可能覺得這個遊戲太簡單或者太難。但是這個遊戲很容易修改,因爲我們使用的是常量,而不是直接輸入值。現在,我們要修改遊戲的話,我們只需要修改在常量中設置的值。
- 降低遊戲整體運行速度:減小FPS變量;
- 降低敵人速度:減小BADDIEMAXSPEED.
當基本的遊戲保持相同時,可以修改任何的常量,以便對遊戲的行爲產生顯著的影響。不斷爲這些常量嘗試新的值,直到找到一套最喜歡的值。
參考:
- 菜鳥教程
- http://inventwithpython.com/invent4thed/chapter3.html
- 《Python遊戲編程快速上手》第四版,AI Sweigart著,李強 譯
- https://www.pygame.org/docs/ref/locals.html