在 Pygame 遊戲中放置平台
這是仍在進行中的關於使用 Pygame 模塊來在 Python 3 中創建電腦遊戲的系列文章的第六部分。先前的文章是:
- 通過構建一個簡單的擲骰子遊戲去學習怎麼用 Python 編程
- 使用 Python 和 Pygame 模塊構建一個遊戲框架
- 如何在你的 Python 遊戲中添加一個玩家
- 用 Pygame 使你的遊戲角色移動起來
- 如何向你的 Python 遊戲中添加一個敵人
一個平台類遊戲需要平台。
在 Pygame 中,平台本身也是個妖精,正像你那個可玩的妖精。這一點是重要的,因為有個是對象的平台,可以使你的玩家妖精更容易與之互動。
創建平台有兩個主要步驟。首先,你必須給該對象編寫代碼,然後,你必須映射出你希望該對象出現的位置。
編碼平台對象
要構建一個平台對象,你要創建一個名為 Platform
的類。它是一個妖精,正像你的 Player
妖精 一樣,帶有很多相同的屬性。
你的 Platform
類需要知道很多平台類型的信息,它應該出現在遊戲世界的哪裡、它應該包含的什麼圖片等等。這其中很多信息可能還尚不存在,這要看你為你的遊戲計划了多少,但是沒有關係。正如直到移動你的遊戲角色那篇文章結束時,你都沒有告訴你的玩家妖精移動速度有多快,你不必事先告訴 Platform
每一件事。
在這系列中你所寫的腳本的開頭附近,創建一個新的類。在這個代碼示例中前三行是用於說明上下文,因此在注釋的下面添加代碼:
import pygame
import sys
import 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](/data/attachment/album/201905/26/203707q14rerqrsjsaruuz.png "Supertux, a tile-based video game")
SuperTux ,一個基於瓷磚的電腦遊戲。
手工繪製方式
另一種方法是將每個素材作為一個整體圖像。如果你喜歡為遊戲世界創建素材,那你會在用圖形應用程序構建遊戲世界的每個部分上花費很多時間。這種方法不需要太多的數學知識,因為所有的平台都是整體的、完整的對象,你只需要告訴 Python 將它們放在屏幕上的什麼位置。
每種方法都有優勢和劣勢,並且根據於你選擇使用的方式,代碼稍有不同。我將覆蓋這兩方面,所以你可以在你的工程中使用一種或另一種,甚至兩者的混合。
關卡繪製
總的來說,繪製你的遊戲世界是關卡設計和遊戲編程中的一個重要的部分。這需要數學知識,但是沒有什麼太難的,而且 Python 擅長數學,它會有所幫助。
你也許發現先在紙張上設計是有用的。拿一張表格紙,並繪製一個方框來代表你的遊戲窗體。在方框中繪製平台,並標記其每一個平台的 X 和 Y 坐標,以及它的寬度和高度。在方框中的實際位置沒有必要是精確的,你只要保持數字合理即可。譬如,假設你的屏幕是 720 像素寬,那麼你不能在一個屏幕上放 8 塊 100 像素的平台。
當然,不是你遊戲中的所有平台都必須容納在一個屏幕大小的方框里,因為你的遊戲將隨著你的玩家行走而滾動。所以,可以繼續繪製你的遊戲世界到第一屏幕的右側,直到關卡結束。
如果你更喜歡精確一點,你可以使用方格紙。當設計一個瓷磚類的遊戲時,這是特別有用的,因為每個方格可以代表一個瓷磚。
![Example of a level map](/data/attachment/album/201905/26/203708r1rke761pdkp7dpd.png "Example of a level map")
一個關卡地圖示例。
坐標系
你可能已經在學校中學習過笛卡爾坐標系。你學習的東西也適用於 Pygame,除了在 Pygame 中你的遊戲世界的坐標系的原點 0,0
是放置在你的屏幕的左上角而不是在中間,是你在地理課上用過的坐標是在中間的。
![Example of coordinates in Pygame](/data/attachment/album/201905/26/203709tkfkbkonzvevsv1f.png "Example of coordinates in Pygame")
在 Pygame 中的坐標示例。
X 軸起始於最左邊的 0,向右無限增加。Y 軸起始於屏幕頂部的 0,向下延伸。
圖片大小
如果你不知道你的玩家、敵人、平台是多大的,繪製出一個遊戲世界是毫無意義的。你可以在圖形程序中找到你的平台或瓷磚的尺寸。例如在 Krita 中,單擊「圖像」菜單,並選擇「屬性」。你可以在「屬性」窗口的最頂部處找到它的尺寸。
另外,你也可以創建一個簡單的 Python 腳本來告訴你的一個圖像的尺寸。打開一個新的文本文件,並輸入這些代碼到其中:
#!/usr/bin/env python3
from PIL import Image
import os.path
import sys
if 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](/data/attachment/album/201905/26/203710p2xtt26t664t6a42.png "One image file per object")
每個對象一個圖形文件。
你可以按照你希望的次數重複使用每個平台,只要確保每個文件僅包含一個平台。你不能使用一個文件包含全部素材,像這樣:
![Your level cannot be one image file](/data/attachment/album/201905/26/203710ymiai7a6i6lugmab.png "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_list
def 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
類與在前面部分中的類是相同的。
ground
和 platform
在 Level
類中,然而,必須使用循環來計算使用多少塊來創建每個平台。
如果你打算在你的遊戲世界中有一個堅固的地面,這種地面是很簡單的。你只需要從整個窗口的一邊到另一邊「克隆」你的地面瓷磚。例如,你可以創建一個 X 和 Y 值的列表來規定每個瓷磚應該放置的位置,然後使用一個循環來獲取每個值並繪製每一個瓷磚。這僅是一個示例,所以不要添加這到你的代碼:
# Do not add this to your code
gloc = [0,656,64,656,128,656,192,656,256,656,320,656,384,656]
不過,如果你仔細看,你可以看到所有的 Y 值是相同的,X 值以 64 的增量不斷地增加 —— 這就是瓷磚的大小。這種重複是精確地,是計算機擅長的,因此你可以使用一點數學邏輯來讓計算機為你做所有的計算:
添加這些到你的腳本的設置部分:
gloc = []
tx = 64
ty = 64
i=0
while i <= (worldx/tx)+tx:
gloc.append(i*tx)
i=i+1
ground_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](/data/attachment/album/201905/26/203711c58sbz95ybpnxpdg.jpg "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 pygame
import sys
import 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 = 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.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 += 1
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,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 = 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)
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 # how fast to move
eloc = []
eloc = [200,20]
gloc = []
#gloc = [0,630,64,630,128,630,192,630,256,630,320,630,384,630]
tx = 64 #tile size
ty = 64 #tile size
i=0
while 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 譯註:到本文翻譯完為止,該系列已經近一年沒有繼續更新了~)
via: https://opensource.com/article/18/7/put-platforms-python-game
作者:Seth Kenlon 選題:lujun9972 譯者:robsean 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive