在 Pygame 遊戲中放置平台

2019-05-26 20:36:00

在這個從零構建一個 Python 遊戲系列的第六部分中,為你的角色建立一些平台來旅行。

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

一個平台類遊戲需要平台。

Pygame 中,平台本身也是個妖精,正像你那個可玩的妖精。這一點是重要的,因為有個是物件的平台,可以使你的玩家妖精更容易與之互動。

建立平台有兩個主要步驟。首先,你必須給該物件編寫程式碼,然後,你必須對映出你希望該物件出現的位置。

編碼平台物件

要構建一個平台物件,你要建立一個名為 Platform 的類。它是一個妖精,正像你的 Player 妖精 一樣,帶有很多相同的屬性。

你的 Platform 類需要知道很多平台型別的資訊,它應該出現在遊戲世界的哪裡、它應該包含的什麼圖片等等。這其中很多資訊可能還尚不存在,這要看你為你的遊戲計劃了多少,但是沒有關係。正如直到移動你的遊戲角色那篇文章結束時,你都沒有告訴你的玩家妖精移動速度有多快,你不必事先告訴 Platform 每一件事。

在這系列中你所寫的指令碼的開頭附近,建立一個新的類。在這個程式碼範例中前三行是用於說明上下文,因此在注釋的下面新增程式碼:

import pygameimport sysimport os## 新程式碼如下: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.image.set_colorkey(ALPHA)    self.rect = self.image.get_rect()    self.rect.y = yloc    self.rect.x = xloc

當被呼叫時,這個類在某個 X 和 Y 位置上建立一個屏上物件,具有某種寬度和高度,並使用某種影象作為紋理。這與如何在屏上繪製出玩家或敵人非常類似。

平台的型別

下一步是繪製出你的平台需要出現的地方。

瓷磚方式

實現平台類遊戲世界有幾種不同的方法。在最初的橫向滾軸遊戲中,例如,馬里奧超級兄弟和刺蝟索尼克,這個技巧是使用“瓷磚”方式,也就是說有幾個代表地面和各種平台的塊,並且這些塊被重複使用來製作一個關卡。你只能有 8 或 12 種不同的塊,你可以將它們排列在螢幕上來建立地面、浮動的平台,以及你遊戲中需要的一切其它的事物。有人發現這是製作遊戲最容易的方法了,因為你只需要製作(或下載)一小組關卡素材就能建立很多不同的關卡。然而,這裡的程式碼需要一點數學知識。

Supertux, a tile-based video game

SuperTux ,一個基於瓷磚的電腦遊戲。

手工繪製方式

另一種方法是將每個素材作為一個整體影象。如果你喜歡為遊戲世界建立素材,那你會在用圖形應用程式構建遊戲世界的每個部分上花費很多時間。這種方法不需要太多的數學知識,因為所有的平台都是整體的、完整的物件,你只需要告訴 Python 將它們放在螢幕上的什麼位置。

每種方法都有優勢和劣勢,並且根據於你選擇使用的方式,程式碼稍有不同。我將覆蓋這兩方面,所以你可以在你的工程中使用一種或另一種,甚至兩者的混合。

關卡繪製

總的來說,繪製你的遊戲世界是關卡設計和遊戲程式設計中的一個重要的部分。這需要數學知識,但是沒有什麼太難的,而且 Python 擅長數學,它會有所幫助。

你也許發現先在紙張上設計是有用的。拿一張表格紙,並繪製一個方框來代表你的遊戲表單。在方框中繪製平台,並標記其每一個平台的 X 和 Y 坐標,以及它的寬度和高度。在方框中的實際位置沒有必要是精確的,你只要保持數位合理即可。譬如,假設你的螢幕是 720 畫素寬,那麼你不能在一個螢幕上放 8 塊 100 畫素的平台。

當然,不是你遊戲中的所有平台都必須容納在一個螢幕大小的方框裡,因為你的遊戲將隨著你的玩家行走而捲動。所以,可以繼續繪製你的遊戲世界到第一螢幕的右側,直到關卡結束。

如果你更喜歡精確一點,你可以使用方格紙。當設計一個瓷磚類的遊戲時,這是特別有用的,因為每個方格可以代表一個瓷磚。

Example of a level map

一個關卡地圖範例。

坐標系

你可能已經在學校中學習過笛卡爾坐標系。你學習的東西也適用於 Pygame,除了在 Pygame 中你的遊戲世界的坐標系的原點 0,0 是放置在你的螢幕的左上角而不是在中間,是你在地理課上用過的坐標是在中間的。

Example of coordinates in Pygame

在 Pygame 中的坐標範例。

X 軸起始於最左邊的 0,向右無限增加。Y 軸起始於螢幕頂部的 0,向下延伸。

圖片大小

如果你不知道你的玩家、敵人、平台是多大的,繪製出一個遊戲世界是毫無意義的。你可以在圖形程式中找到你的平台或瓷磚的尺寸。例如在 Krita 中,單擊“影象”選單,並選擇“屬性”。你可以在“屬性”視窗的最頂部處找到它的尺寸。

另外,你也可以建立一個簡單的 Python 指令碼來告訴你的一個影象的尺寸。開啟一個新的文字檔案,並輸入這些程式碼到其中:

#!/usr/bin/env python3from PIL import Imageimport os.pathimport sysif len(sys.argv) > 1:    print(sys.argv[1])else:    sys.exit('Syntax: identify.py [filename]')pic = sys.argv[1]dim = Image.open(pic)X   = dim.size[0]Y   = dim.size[1]print(X,Y)

儲存該文字檔案為 identify.py

要使用這個指令碼,你必須安裝一些額外的 Python 模組,它們包含了這個指令碼中新使用的關鍵字:

$ pip3 install Pillow --user

一旦安裝好,在你遊戲工程目錄中執行這個指令碼:

$ python3 ./identify.py images/ground.png(1080, 97)

在這個範例中,地面平台的圖形的大小是 1080 畫素寬和 97 畫素高。

平台塊

如果你選擇單獨地繪製每個素材,你必須建立想要插入到你的遊戲世界中的幾個平台和其它元素,每個素材都放在它自己的檔案中。換句話說,你應該讓每個素材都有一個檔案,像這樣:

One image file per object

每個物件一個圖形檔案。

你可以按照你希望的次數重複使用每個平台,只要確保每個檔案僅包含一個平台。你不能使用一個檔案包含全部素材,像這樣:

Your level cannot be one image file

你的關卡不能是一個圖形檔案。

當你完成時,你可能希望你的遊戲看起來像這樣,但是如果你在一個大檔案中建立你的關卡,你就沒有方法從背景中區分出一個平台,因此,要麼把物件繪製在它們自己的檔案中,要麼從一個更大的檔案中裁剪出它們,並儲存為單獨的副本。

注意: 如同你的其它素材,你可以使用 GIMP、Krita、MyPaint,或 Inkscape 來建立你的遊戲素材。

平台出現在每個關卡開始的螢幕上,因此你必須在你的 Level 類中新增一個 platform 函數。在這裡特例是地面平台,它重要到應該擁有它自己的一個組。通過把地面看作一組特殊型別的平台,你可以選擇它是否捲動,或它上面是否可以站立,而其它平台可以漂浮在它上面。這取決於你。

新增這兩個函數到你的 Level 類:

def ground(lvl,x,y,w,h):    ground_list = pygame.sprite.Group()    if lvl == 1:        ground = Platform(x,y,w,h,'block-ground.png')        ground_list.add(ground)    if lvl == 2:        print("Level " + str(lvl) )    return ground_listdef platform( lvl ):    plat_list = pygame.sprite.Group()    if lvl == 1:        plat = Platform(200, worldy-97-128, 285,67,'block-big.png')        plat_list.add(plat)        plat = Platform(500, worldy-97-320, 197,54,'block-small.png')        plat_list.add(plat)    if lvl == 2:        print("Level " + str(lvl) )           return plat_list

ground 函數需要一個 X 和 Y 位置,以便 Pygame 知道在哪裡放置地面平台。它也需要知道平台的寬度和高度,這樣 Pygame 知道地面延伸到每個方向有多遠。該函數使用你的 Platform 類來生成一個屏上物件,然後將這個物件新增到 ground_list 組。

platform 函數本質上是相同的,除了其有更多的平台。在這個範例中,僅有兩個平台,但是你可以想有多少就有多少。在進入一個平台後,在列出另一個前你必須新增它到 plat_list 中。如果你不新增平台到組中,那麼它將不出現在你的遊戲中。

提示: 很難想象你的遊戲世界的 0 是在頂部,因為在真實世界中發生的情況是相反的;當估計你有多高時,你不會從上往下測量你自己,而是從腳到頭頂來測量。

如果對你來說從“地面”上來構建你的遊戲世界更容易,將 Y 軸值表示為負數可能有幫助。例如,你知道你的遊戲世界的底部是 worldy 的值。因此 worldy 減去地面的高度(在這個範例中是 97)是你的玩家正常站立的位置。如果你的角色是 64 畫素高,那麼地面減去 128 正好是你的玩家的兩倍高。事實上,一個放置在 128 畫素處平台大約是相對於你的玩家的兩層樓高度。一個平台在 -320 處比三層樓更高。等等。

正像你現在可能所知的,如果你不使用它們,你的類和函數是沒有價值的。新增這些程式碼到你的設定部分(第一行只是上下文,所以新增最後兩行):

enemy_list  = Level.bad( 1, eloc )ground_list = Level.ground( 1,0,worldy-97,1080,97 )plat_list   = Level.platform( 1 )

並把這些行加到你的主迴圈(再一次,第一行僅用於上下文):

enemy_list.draw(world)  # 重新整理敵人ground_list.draw(world)  # 重新整理地面plat_list.draw(world)  # 重新整理平台

瓷磚平台

瓷磚類遊戲世界更容易製作,因為你只需要在前面繪製一些塊,就能在遊戲中一再使用它們建立每個平台。在像 OpenGameArt.org 這樣的網站上甚至有一套瓷磚供你來使用。

Platform 類與在前面部分中的類是相同的。

groundplatformLevel 類中,然而,必須使用迴圈來計算使用多少塊來建立每個平台。

如果你打算在你的遊戲世界中有一個堅固的地面,這種地面是很簡單的。你只需要從整個視窗的一邊到另一邊“克隆”你的地面瓷磚。例如,你可以建立一個 X 和 Y 值的列表來規定每個瓷磚應該放置的位置,然後使用一個迴圈來獲取每個值並繪製每一個瓷磚。這僅是一個範例,所以不要新增這到你的程式碼:

# Do not add this to your codegloc = [0,656,64,656,128,656,192,656,256,656,320,656,384,656]

不過,如果你仔細看,你可以看到所有的 Y 值是相同的,X 值以 64 的增量不斷地增加 —— 這就是瓷磚的大小。這種重複是精確地,是計算機擅長的,因此你可以使用一點數學邏輯來讓計算機為你做所有的計算:

新增這些到你的指令碼的設定部分:

gloc = []tx   = 64ty   = 64i=0while i <= (worldx/tx)+tx:    gloc.append(i*tx)    i=i+1ground_list = Level.ground( 1,gloc,tx,ty )

現在,不管你的視窗的大小,Python 會通過瓷磚的寬度分割遊戲世界的寬度,並建立一個陣列列表列出每個 X 值。這裡不計算 Y 值,因為在平的地面上這個從不會變化。

為了在一個函數中使用陣列,使用一個 while 迴圈,檢視每個條目並在適當的位置新增一個地面瓷磚:

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,'tile-ground.png')            ground_list.add(ground)            i=i+1    if lvl == 2:        print("Level " + str(lvl) )    return ground_list

除了 while 迴圈,這幾乎與在上面一部分中提供的瓷磚類平台的 ground 函數的程式碼相同。

對於移動的平台,原理是相似的,但是這裡有一些技巧可以使它簡單。

你可以通過它的起始畫素(它的 X 值)、距地面的高度(它的 Y 值)、繪製多少瓷磚來定義一個平台,而不是通過畫素繪製每個平台。這樣,你不必操心每個平台的寬度和高度。

這個技巧的邏輯有一點複雜,因此請仔細複製這些程式碼。有一個 while 迴圈巢狀在另一個 while 迴圈的內部,因為這個函數必須考慮每個陣列項的三個值來成功地建造一個完整的平台。在這個範例中,這裡僅有三個平台以 ploc.append 語句定義,但是你的遊戲可能需要更多,因此你需要多少就定義多少。當然,有一些不會出現,因為它們遠在螢幕外,但是一旦當你進行捲動時,它們將呈現在眼前。

def platform(lvl,tx,ty):    plat_list = pygame.sprite.Group()    ploc = []    i=0    if lvl == 1:        ploc.append((200,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,'tile.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

要讓這些平台出現在你的遊戲世界,它們必須出現在你的主迴圈中。如果你還沒有這樣做,新增這些行到你的主迴圈(再一次,第一行僅被用於上下文)中:

        enemy_list.draw(world)  # 重新整理敵人        ground_list.draw(world) # 重新整理地面        plat_list.draw(world)   # 重新整理平台

啟動你的遊戲,根據需要調整你的平台的放置位置。如果你看不見螢幕外產生的平台,不要擔心;你不久後就可以修復它。

到目前為止,這是遊戲的圖片和程式碼:

Pygame game

到目前為止,我們的 Pygame 平台。

#!/usr/bin/env python3# draw a world# add a player and player control# add player movement# add enemy and basic collision# add platform# 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 os'''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.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 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)        for enemy in enemy_hit_list:            self.health -= 1            print(self.health)        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)        for g in ground_hit_list:            self.health -= 1            print(self.health)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.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        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 += 1class 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,lloc):        print(lvl)    def ground(lvl,gloc,tx,ty):        ground_list = pygame.sprite.Group()        i=0        if lvl == 1:            while i < len(gloc):                print("blockgen:" + str(i))                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'''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)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 = 10 # how fast to moveeloc = []eloc = [200,20]gloc = []#gloc = [0,630,64,630,128,630,192,630,256,630,320,630,384,630]tx = 64 #tile sizety = 64 #tile sizei=0while i <= (worldx/tx)+tx:    gloc.append(i*tx)    i=i+1    print("block: " + str(i))enemy_list = Level.bad( 1, eloc )ground_list = Level.ground( 1,gloc,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'):                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'):                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 == ord('q'):                pygame.quit()                sys.exit()                main = False#    world.fill(BLACK)    world.blit(backdrop, backdropbox)    player.update()    player_list.draw(world) #refresh player position    enemy_list.draw(world)  # refresh enemies    ground_list.draw(world)  # refresh enemies    for e in enemy_list:        e.move()    pygame.display.flip()    clock.tick(fps)

(LCTT 譯註:到本文翻譯完為止,該系列已經近一年沒有繼續更新了~)