新增計分到你的 Python 遊戲

2020-02-01 15:48:00

在本系列的第十一篇有關使用 Python Pygame 模組進行程式設計的文章中,顯示玩家獲得戰利品或受到傷害時的得分。

這是仍在進行中的關於使用 Pygame 模組來在 Python 3 在建立電腦遊戲的第十一部分。先前的文章是:

如果你已經跟隨這一系列很久,那麼已經學習了使用 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 pygameimport pygame.freetype

你的程式碼應該已經匯入了 Pygame,不過,請修改你的 import 語句以包含 Freetype 模組:

import pygameimport sysimport osimport 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) -> Fontpygame.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 = txmyfont = 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

不過,這裡有一個問題。當一個玩家被敵人擊中時,健康度會一路下降,這是不公平的。你剛剛發現一個非致命的錯誤。非致命的錯誤是這些在應用程式中小問題,(通常)不會阻止應用程式啟動或甚至導致停止工作,但是它們要麼沒有意義,要麼會惹惱使用者。這裡是如何解決這個問題的方法。

修復生命值計數

當前生命值系統的問題是,敵人接觸玩家時,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 pygameimport sysimport osimport 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 = xlocclass 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_listdef 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 = 960worldy = 720fps = 40 # frame rateani = 4  # animation cyclesclock = pygame.time.Clock()pygame.init()main = TrueBLUE  = (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 playerplayer.rect.x = 0player.rect.y = 0player_list = pygame.sprite.Group()player_list.add(player)steps = 10forwardx = 600backwardx = 230eloc = []eloc = [200,20]gloc = []tx = 64 #tile sizety = 64 #tile sizefont_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"fonts","amazdoom.ttf")font_size = txmyfont = pygame.freetype.Font(font_path, font_size)   i=0while i <= (worldx/tx)+tx:    gloc.append(i*tx)    i=i+1enemy_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)