添加計分到你的 Python 遊戲
這是仍在進行中的關於使用 Pygame 模塊來在 Python 3 在創建電腦遊戲的第十一部分。先前的文章是:
- 通過構建一個簡單的擲骰子遊戲去學習怎麼用 Python 編程
- 使用 Python 和 Pygame 模塊構建一個遊戲框架
- 如何在你的 Python 遊戲中添加一個玩家
- 用 Pygame 使你的遊戲角色移動起來
- 如何向你的 Python 遊戲中添加一個敵人
- 在 Pygame 遊戲中放置平台
- 在你的 Python 遊戲中模擬引力
- 為你的 Python 平台類遊戲添加跳躍功能
- 使你的 Python 遊戲玩家能夠向前和向後跑
- 在你的 Python 平台類遊戲中放一些獎勵
如果你已經跟隨這一系列很久,那麼已經學習了使用 Python 創建一個視頻遊戲所需的所有基本語法和模式。然而,它仍然缺少一個至關重要的組成部分。這一組成部分不僅僅對用 Python 編程遊戲重要;不管你探究哪個計算機分支,你都必需精通:作為一個程序員,通過閱讀一種語言的或庫的文檔來學習新的技巧。
幸運的是,你正在閱讀本文的事實表明你熟悉文檔。為了使你的平台類遊戲更加美觀,在這篇文章中,你將在遊戲屏幕上添加得分和生命值顯示。不過,教你如何找到一個庫的功能以及如何使用這些新的功能的這節課程並沒有多神秘。
在 Pygame 中顯示得分
現在,既然你有了可以被玩家收集的獎勵,那就有充分的理由來記錄分數,以便你的玩家看到他們收集了多少獎勵。你也可以跟蹤玩家的生命值,以便當他們被敵人擊中時會有相應結果。
你已經有了跟蹤分數和生命值的變數,但是這一切都發生在後台。這篇文章教你在遊戲期間在遊戲屏幕上以你選擇的一種字體來顯示這些統計數字。
閱讀文檔
大多數 Python 模塊都有文檔,即使那些沒有文檔的模塊,也能通過 Python 的幫助功能來進行最小的文檔化。Pygame 的主頁面 鏈接了它的文檔。不過,Pygame 是一個帶有很多文檔的大模塊,並且它的文檔不像在 Opensource.com 上的文章一樣,以同樣易理解的(和友好的、易解釋的、有用的)敘述風格來撰寫的。它們是技術文檔,並且列出在模塊中可用的每個類和函數,各自要求的輸入類型等等。如果你不適應參考代碼組件描述,這可能會令人不知所措。
在煩惱於庫的文檔前,第一件要做的事,就是來想想你正在嘗試達到的目標。在這種情況下,你想在屏幕上顯示玩家的得分和生命值。
在你確定你需要的結果後,想想它需要什麼的組件。你可以從變數和函數的方面考慮這一點,或者,如果你還沒有自然地想到這一點,你可以進行一般性思考。你可能意識到需要一些文本來顯示一個分數,你希望 Pygame 在屏幕上繪製這些文本。如果你仔細思考,你可能會意識到它與在屏幕上渲染一個玩家、獎勵或一個平台並多麼大的不同。
從技術上講,你可以使用數字圖形,並讓 Pygame 顯示這些數字圖形。它不是達到你目標的最容易的方法,但是如果它是你唯一知道的方法,那麼它是一個有效的方法。不過,如果你參考 Pygame 的文檔,你看到列出的模塊之一是 font
,這是 Pygame 使得在屏幕上來使列印文本像輸入文字一樣容易的方法。
解密技術文檔
font
文檔頁面以 pygame.font.init()
開始,它列出了用於初始化字體模塊的函數。它由 pygame.init()
自動地調用,你已經在代碼中調用了它。再強調一次,從技術上講,你已經到達一個足夠好的點。雖然你尚不知道如何做,你知道你能夠使用 pygame.font
函數來在屏幕上列印文本。
然而,如果你閱讀更多一些,你會找到這裡還有一種更好的方法來列印字體。pygame.freetype
模塊在文檔中的描述方式如下:
pygame.freetype
模塊是pygame.fontpygame
模塊的一個替代品,用於載入和渲染字體。它有原函數的所有功能,外加很多新的功能。
在 pygame.freetype
文檔頁面的下方,有一些示例代碼:
import pygame
import pygame.freetype
你的代碼應該已經導入了 Pygame,不過,請修改你的 import
語句以包含 Freetype 模塊:
import pygame
import sys
import os
import pygame.freetype
在 Pygame 中使用字體
從 font
模塊的描述中可以看出,顯然 Pygame 使用一種字體(不管它的你提供的或內置到 Pygame 的默認字體)在屏幕上渲染字體。滾動瀏覽 pygame.freetype
文檔來找到 pygame.freetype.Font
函數:
pygame.freetype.Font
從支持的字體文件中創建一個新的字體實例。
Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font
pygame.freetype.Font.name
符合規則的字體名稱。
pygame.freetype.Font.path
字體文件路徑。
pygame.freetype.Font.size
在渲染中使用的默認點大小
這描述了如何在 Pygame 中構建一個字體「對象」。把屏幕上的一個簡單對象視為一些代碼屬性的組合對你來說可能不太自然,但是這與你構建英雄和敵人精靈的方式非常類似。你需要一個字體文件,而不是一個圖像文件。在你有一個字體文件後,你可以在你的代碼中使用 pygame.freetype.Font
函數來創建一個字體對象,然後使用該對象來在屏幕上渲染文本。
因為並不是世界上的每個人的電腦上都有完全一樣的字體,因此將你選擇的字體與你的遊戲捆綁在一起是很重要的。要捆綁字體,首先在你的遊戲文件夾中創建一個新的目錄,放在你為圖像而創建的文件目錄旁邊。稱其為 fonts
。
即使你的計算機操作系統隨附了幾種字體,但是將這些字體給予其他人是非法的。這看起來很奇怪,但法律就是這樣運作的。如果想與你的遊戲一起隨附一種字體,你必需找到一種開源或知識共享的字體,以允許你隨遊戲一起提供該字體。
專門提供自由和合法字體的網站包括:
當你找到你喜歡的字體後,下載下來。解壓縮 ZIP 或 TAR 文件,並移動 .ttf
或 .otf
文件到你的項目目錄下的 fonts
文件夾中。
你沒有安裝字體到你的計算機上。你只是放置字體到你遊戲的 fonts
文件夾中,以便 Pygame 可以使用它。如果你想,你可以在你的計算機上安裝該字體,但是沒有必要。重要的是將字體放在你的遊戲目錄中,這樣 Pygame 可以「描繪」字體到屏幕上。
如果字體文件的名稱複雜且帶有空格或特殊字元,只需要重新命名它即可。文件名稱是完全任意的,並且對你來說,文件名稱越簡單,越容易將其鍵入你的代碼中。
現在告訴 Pygame 你的字體。從文檔中你知道,當你至少提供了字體文件路徑給 pygame.freetype.Font
時(文檔明確指出所有其餘屬性都是可選的),你將在返回中獲得一個字體對象:
Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font
創建一個稱為 myfont
的新變數來充當你在遊戲中字體,並放置 Font
函數的結果到這個變數中。這個示例中使用 amazdoom.ttf
字體,但是你可以使用任何你想使用的字體。在你的設置部分放置這些代碼:
font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"fonts","amazdoom.ttf")
font_size = tx
myfont = pygame.freetype.Font(font_path, font_size)
在 Pygame 中顯示文本
現在你已經創建一個字體對象,你需要一個函數來繪製你想繪製到屏幕上的文本。這和你在你的遊戲中繪製背景和平台是相同的原理。
首先,創建一個函數,並使用 myfont
對象來創建一些文本,設置顏色為某些 RGB 值。這必須是一個全局函數;它不屬於任何具體的類:
def stats(score,health):
myfont.render_to(world, (4, 4), "Score:"+str(score), WHITE, None, size=64)
myfont.render_to(world, (4, 72), "Health:"+str(health), WHITE, None, size=64)
當然,你此刻已經知道,如果它不在主循環中,你的遊戲將不會發生任何事,所以在文件的底部添加一個對你的 stats
函數的調用:
for e in enemy_list:
e.move()
stats(player.score,player.health) # draw text
pygame.display.flip()
嘗試你的遊戲。
當玩家收集獎勵品時,得分會上升。當玩家被敵人擊中時,生命值下降。成功!
![Keeping score in Pygame](/data/attachment/album/202002/01/154912bd1b921t52ligbt3.jpg "Keeping score in Pygame")
不過,這裡有一個問題。當一個玩家被敵人擊中時,健康度會一路下降,這是不公平的。你剛剛發現一個非致命的錯誤。非致命的錯誤是這些在應用程序中小問題,(通常)不會阻止應用程序啟動或甚至導致停止工作,但是它們要麼沒有意義,要麼會惹惱用戶。這裡是如何解決這個問題的方法。
修復生命值計數
當前生命值系統的問題是,敵人接觸玩家時,Pygame 時鐘的每一次滴答,健康度都會減少。這意味著一個緩慢移動的敵人可能在一次遭遇中將一個玩家降低健康度至 -200 ,這不公平。當然,你可以給你的玩家一個 10000 的起始健康度得分,而不用擔心它;這可以工作,並且可能沒有人會注意。但是這裡有一個更好的方法。
當前,你的代碼偵查出一個玩家和一個敵人發生碰撞的時候。生命值問題的修復是檢測兩個獨立的事件:什麼時候玩家和敵人碰撞,並且,在它們碰撞後,什麼時候它們停止碰撞。
首先,在你的玩家類中,創建一個變數來代表玩家和敵人碰撞在一起:
self.frame = 0
self.health = 10
self.damage = 0
在你的 Player
類的 update
函數中,移除這塊代碼塊:
for enemy in enemy_hit_list:
self.health -= 1
#print(self.health)
並且在它的位置,只要玩家當前沒有被擊中,檢查碰撞:
if self.damage == 0:
for enemy in enemy_hit_list:
if not self.rect.contains(enemy):
self.damage = self.rect.colliderect(enemy)
你可能會在你刪除的語句塊和你剛剛添加的語句塊之間看到相似之處。它們都在做相同的工作,但是新的代碼更複雜。最重要的是,只有當玩家當前沒有被擊中時,新的代碼才運行。這意味著,當一個玩家和敵人碰撞時,這些代碼運行一次,而不是像以前那樣一直發生碰撞。
新的代碼使用兩個新的 Pygame 函數。self.rect.contains
函數檢查一個敵人當前是否在玩家的邊界框內,並且當它是 true
時, self.rect.colliderect
設置你的新的 self.damage
變數為 1,而不管它多少次是 true
。
現在,即使被一個敵人擊中 3 秒,對 Pygame 來說仍然看作一次擊中。
我通過通讀 Pygame 的文檔而發現了這些函數。你沒有必要一次閱讀完全部的文檔,並且你也沒有必要閱讀每個函數的每個單詞。不過,花費時間在你正在使用的新的庫或模塊的文檔上是很重要的;否則,你極有可能在重新發明輪子。不要花費一個下午的時間來嘗試修改拼接一個解決方案到一些東西,而這些東西已經被你正在使用的框架的所解決。閱讀文檔,知悉函數,並從別人的工作中獲益!
最後,添加另一個代碼語句塊來偵查出什麼時候玩家和敵人不再接觸。然後直到那時,才從玩家減少一個生命值。
if self.damage == 1:
idx = self.rect.collidelist(enemy_hit_list)
if idx == -1:
self.damage = 0 # set damage back to 0
self.health -= 1 # subtract 1 hp
注意,只有當玩家被擊中時,這個新的代碼才會被觸發。這意味著,在你的玩家在你的遊戲世界正在探索或收集獎勵時,這個代碼不會運行。它僅當 self.damage
變數被激活時運行。
當代碼運行時,它使用 self.rect.collidelist
來查看玩家是否仍然接觸在你敵人列表中的敵人(當其未偵查到碰撞時,collidelist
返回 -1)。在它沒有接觸敵人時,是該處理 self.damage
的時機:通過設置 self.damage
變數回到 0 來使其無效,並減少一點生命值。
現在嘗試你的遊戲。
得分反應
現在,你有一個來讓你的玩家知道它們分數和生命值的方法,當你的玩家達到某些里程碑時,你可以確保某些事件發生。例如,也許這裡有一個特殊的恢復一些生命值的獎勵項目。也許一個到達 0 生命值的玩家不得不從一個關卡的起始位置重新開始。
你可以在你的代碼中檢查這些事件,並且相應地操縱你的遊戲世界。你已經知道該怎麼做,所以請瀏覽文檔來尋找新的技巧,並且獨立地嘗試這些技巧。
這裡是到目前為止所有的代碼:
#!/usr/bin/env python3
# draw a world
# add a player and player control
# add player movement
# add enemy and basic collision
# add platform
# add gravity
# add jumping
# add scrolling
# add loot
# add score
# GNU All-Permissive License
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved. This file is offered as-is,
# without any warranty.
import pygame
import sys
import os
import pygame.freetype
'''
Objects
'''
class Platform(pygame.sprite.Sprite):
# x location, y location, img width, img height, img file
def __init__(self,xloc,yloc,imgw,imgh,img):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load(os.path.join('images',img)).convert()
self.image.convert_alpha()
self.rect = self.image.get_rect()
self.rect.y = yloc
self.rect.x = xloc
class Player(pygame.sprite.Sprite):
'''
Spawn a player
'''
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.movex = 0
self.movey = 0
self.frame = 0
self.health = 10
self.damage = 0
self.collide_delta = 0
self.jump_delta = 6
self.score = 1
self.images = []
for i in range(1,9):
img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert()
img.convert_alpha()
img.set_colorkey(ALPHA)
self.images.append(img)
self.image = self.images[0]
self.rect = self.image.get_rect()
def jump(self,platform_list):
self.jump_delta = 0
def gravity(self):
self.movey += 3.2 # how fast player falls
if self.rect.y > worldy and self.movey >= 0:
self.movey = 0
self.rect.y = worldy-ty
def control(self,x,y):
'''
control player movement
'''
self.movex += x
self.movey += y
def update(self):
'''
Update sprite position
'''
self.rect.x = self.rect.x + self.movex
self.rect.y = self.rect.y + self.movey
# moving left
if self.movex < 0:
self.frame += 1
if self.frame > ani*3:
self.frame = 0
self.image = self.images[self.frame//ani]
# moving right
if self.movex > 0:
self.frame += 1
if self.frame > ani*3:
self.frame = 0
self.image = self.images[(self.frame//ani)+4]
# collisions
enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
if self.damage == 0:
for enemy in enemy_hit_list:
if not self.rect.contains(enemy):
self.damage = self.rect.colliderect(enemy)
if self.damage == 1:
idx = self.rect.collidelist(enemy_hit_list)
if idx == -1:
self.damage = 0 # set damage back to 0
self.health -= 1 # subtract 1 hp
loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False)
for loot in loot_hit_list:
loot_list.remove(loot)
self.score += 1
print(self.score)
plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
for p in plat_hit_list:
self.collide_delta = 0 # stop jumping
self.movey = 0
if self.rect.y > p.rect.y:
self.rect.y = p.rect.y+ty
else:
self.rect.y = p.rect.y-ty
ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
for g in ground_hit_list:
self.movey = 0
self.rect.y = worldy-ty-ty
self.collide_delta = 0 # stop jumping
if self.rect.y > g.rect.y:
self.health -=1
print(self.health)
if self.collide_delta < 6 and self.jump_delta < 6:
self.jump_delta = 6*2
self.movey -= 33 # how high to jump
self.collide_delta += 6
self.jump_delta += 6
class Enemy(pygame.sprite.Sprite):
'''
Spawn an enemy
'''
def __init__(self,x,y,img):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load(os.path.join('images',img))
self.movey = 0
#self.image.convert_alpha()
#self.image.set_colorkey(ALPHA)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.counter = 0
def move(self):
'''
enemy movement
'''
distance = 80
speed = 8
self.movey += 3.2
if self.counter >= 0 and self.counter <= distance:
self.rect.x += speed
elif self.counter >= distance and self.counter <= distance*2:
self.rect.x -= speed
else:
self.counter = 0
self.counter += 1
if not self.rect.y >= worldy-ty-ty:
self.rect.y += self.movey
plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
for p in plat_hit_list:
self.movey = 0
if self.rect.y > p.rect.y:
self.rect.y = p.rect.y+ty
else:
self.rect.y = p.rect.y-ty
ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
for g in ground_hit_list:
self.rect.y = worldy-ty-ty
class Level():
def bad(lvl,eloc):
if lvl == 1:
enemy = Enemy(eloc[0],eloc[1],'yeti.png') # spawn enemy
enemy_list = pygame.sprite.Group() # create enemy group
enemy_list.add(enemy) # add enemy to group
if lvl == 2:
print("Level " + str(lvl) )
return enemy_list
def loot(lvl,tx,ty):
if lvl == 1:
loot_list = pygame.sprite.Group()
loot = Platform(200,ty*7,tx,ty, 'loot_1.png')
loot_list.add(loot)
if lvl == 2:
print(lvl)
return loot_list
def ground(lvl,gloc,tx,ty):
ground_list = pygame.sprite.Group()
i=0
if lvl == 1:
while i < len(gloc):
ground = Platform(gloc[i],worldy-ty,tx,ty,'ground.png')
ground_list.add(ground)
i=i+1
if lvl == 2:
print("Level " + str(lvl) )
return ground_list
def platform(lvl,tx,ty):
plat_list = pygame.sprite.Group()
ploc = []
i=0
if lvl == 1:
ploc.append((20,worldy-ty-128,3))
ploc.append((300,worldy-ty-256,3))
ploc.append((500,worldy-ty-128,4))
while i < len(ploc):
j=0
while j <= ploc[i][2]:
plat = Platform((ploc[i][0]+(j*tx)),ploc[i][1],tx,ty,'ground.png')
plat_list.add(plat)
j=j+1
print('run' + str(i) + str(ploc[i]))
i=i+1
if lvl == 2:
print("Level " + str(lvl) )
return plat_list
def stats(score,health):
myfont.render_to(world, (4, 4), "Score:"+str(score), SNOWGRAY, None, size=64)
myfont.render_to(world, (4, 72), "Health:"+str(health), SNOWGRAY, None, size=64)
'''
Setup
'''
worldx = 960
worldy = 720
fps = 40 # frame rate
ani = 4 # animation cycles
clock = pygame.time.Clock()
pygame.init()
main = True
BLUE = (25,25,200)
BLACK = (23,23,23 )
WHITE = (254,254,254)
SNOWGRAY = (137,164,166)
ALPHA = (0,255,0)
world = pygame.display.set_mode([worldx,worldy])
backdrop = pygame.image.load(os.path.join('images','stage.png')).convert()
backdropbox = world.get_rect()
player = Player() # spawn player
player.rect.x = 0
player.rect.y = 0
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10
forwardx = 600
backwardx = 230
eloc = []
eloc = [200,20]
gloc = []
tx = 64 #tile size
ty = 64 #tile size
font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"fonts","amazdoom.ttf")
font_size = tx
myfont = pygame.freetype.Font(font_path, font_size)
i=0
while i <= (worldx/tx)+tx:
gloc.append(i*tx)
i=i+1
enemy_list = Level.bad( 1, eloc )
ground_list = Level.ground( 1,gloc,tx,ty )
plat_list = Level.platform( 1,tx,ty )
loot_list = Level.loot(1,tx,ty)
'''
Main loop
'''
while main == True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit(); sys.exit()
main = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT or event.key == ord('a'):
print("LEFT")
player.control(-steps,0)
if event.key == pygame.K_RIGHT or event.key == ord('d'):
print("RIGHT")
player.control(steps,0)
if event.key == pygame.K_UP or event.key == ord('w'):
print('jump')
if event.type == pygame.KEYUP:
if event.key == pygame.K_LEFT or event.key == ord('a'):
player.control(steps,0)
if event.key == pygame.K_RIGHT or event.key == ord('d'):
player.control(-steps,0)
if event.key == pygame.K_UP or event.key == ord('w'):
player.jump(plat_list)
if event.key == ord('q'):
pygame.quit()
sys.exit()
main = False
# scroll the world forward
if player.rect.x >= forwardx:
scroll = player.rect.x - forwardx
player.rect.x = forwardx
for p in plat_list:
p.rect.x -= scroll
for e in enemy_list:
e.rect.x -= scroll
for l in loot_list:
l.rect.x -= scroll
# scroll the world backward
if player.rect.x <= backwardx:
scroll = backwardx - player.rect.x
player.rect.x = backwardx
for p in plat_list:
p.rect.x += scroll
for e in enemy_list:
e.rect.x += scroll
for l in loot_list:
l.rect.x += scroll
world.blit(backdrop, backdropbox)
player.gravity() # check gravity
player.update()
player_list.draw(world) #refresh player position
enemy_list.draw(world) # refresh enemies
ground_list.draw(world) # refresh enemies
plat_list.draw(world) # refresh platforms
loot_list.draw(world) # refresh loot
for e in enemy_list:
e.move()
stats(player.score,player.health) # draw text
pygame.display.flip()
clock.tick(fps)
via: https://opensource.com/article/20/1/add-scorekeeping-your-python-game
作者:Seth Kenlon 選題:lujun9972 譯者:robsean 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive