編程派codingpy
編程派堅信,這個時代人人都學點編程,不論對自身成長還是日常工作,都将有巨大益處。因此,選擇了通過本公号與大家分享最易學的編程語言 Python 的教程和資源,希望對你有幫助。
今天分享的是 Python 翻譯組 最新譯文,原文來自real python,是一篇比較詳細的 PyGame 遊戲開發入門指南。
譯者:haiyuqiao,華中科技大學(在讀研究生),正在使用 Python 做數據分析。Fighting from now!校對:EarlGrey,編程派主頁君。
以下是正文,一起來學習吧。
下文中下劃線文字,可閱讀原文後點擊訪問。另外,本文代碼略有删減,完整代碼可閱讀原文。
PyGame是 SDL 庫的 Python 包裝器(wrapper)。SDL 是一個跨平台庫,支持訪問計算機多媒體硬件(聲音、視頻、輸入等)。SDL 非常強大,但美中不足的是,它是基于 C 語言的,而 C 語言比較難懂,因此我們采用 PyGame 。
在本教程中,我們将介紹 PyGame 的基本邏輯和沖突檢測,以及如何在屏幕上繪圖和将外部文件導入到遊戲中。
提示:教程假定你對 Python 的語法、文件結構和面向對象的程序設計已經有了基本的了解。
準備工作
打開PyGame下載頁面,根據你的操作系統和 Python 版本下載合适的 PyGame 安裝包。如果你使用的是 Python 3,那麼請下載 1.9.2 版.
EarlGrey:在下載頁面找不到 1.9.2 版的下載鍊接,但是
pip install
可以安裝。新建一個 .py 文件,然後輸入以下代碼:
與其他 Python 程序一樣,我們首先導入想要使用的模塊。這裡,我們将導入
pygame
和pygame.locals
,後續我們将使用其中的一些常量。最後一行會初始化所有導入的 PyGame 模塊,在做其他操作之前必須執行調用該函數。基礎對象
屏幕對象
首先,我們需要一張畫布,我們稱之為“屏幕”,它是我們繪畫的平台。為了創建一個屏幕,我們需要調用
pygame.display
中的set_mode
方法,然後向set_mode()
傳遞包含屏幕窗口寬度和高度的元組(本教程中使用 800x600 尺寸)。screen = pygame.display.set_mode((800,600))
運行上述代碼,将會彈出一個窗口,然後當程序退出後又立即消失。一點都不酷嘛,對吧?下一節,我們将介紹遊戲的主循環,它将确保隻有在我們給它正确的輸入時程序才會退出。
遊戲主循環
遊戲主循環/事件循環是所有操作發生的地方。在遊戲過程中,它不斷的更新遊戲狀态,渲染遊戲畫面和收集輸入指令。創建循環時,需要确保我們有辦法跳出循環,退出應用。為此,我們将同時介紹一些基本的用戶輸入指令。所有的用戶輸入(和我們稍稍後提到的其他事件)都會進入 PyGame 的事件隊列,通過調用
pygame.event.get()
可以訪問該隊列。這将返回一個包含隊列裡所有事件的列表,我們将循環這個列表,并根針對相應的事件類型做出反應。現在我們隻關心KEYDOWN
和QUIT
事件:
将上述代碼添加到之前的代碼下,并運行。你應該看到一個空的窗口。隻有你按下 ESC 鍵 或者觸發一個 QUIT 事,否則這個窗口不會消失。
Surface 和 Rects
Surface
和Rects
是 PyGame 中的基本構件。可以将 Surface 看作一張白紙,你可以在上面随意繪畫。我們的屏幕對象也是一個 Surface 。它們可以包含圖片。Rects 是 Surface 中矩形區域的表示。讓我們創建一個 50x50 像素的 Surface,然後給它塗色。由于屏幕是黑色的,所以我們使用白色。 我們然後調用
get_rect()
在 Surface上 得到一個矩形區域和 Surface 的 x 軸 和 y 軸。
Blit 和 Flip
僅僅隻是創建了 Surface 并不能在屏幕上看到它。為此我們需要将這個 Surface 繪制(Blit)到另一個 Surface 上。Blit 是一個專業術語,意思就是繪圖。你僅僅隻能從一個Surface Blit 到另一個Surface,我們的屏幕就是一個 Surface 對象。以下是我們如何将
surf
畫到屏幕上:# 這一行表示:将surf畫到屏幕 x:400.y:300的坐标上
screen.blit(surf,(400,300)) pygame.display.flip()
blit()
有兩個參數:要畫的 Surface 和 在源 Surface 上的坐标。此處我們使用屏幕的中心,但是當你運行代碼時,你會發現我們的surf
并沒有出現在屏幕的中心。這是因為blit()
是從左上角開始畫 surf 。注意在 blit 之後的
pygame.display.filp()
的調用。Flip将會更新自上次 flip 後的整個屏幕,兩次 flip 之間發生的修改都将在屏幕上顯示。沒有調用flip()
那就什麼也不會出現。Sprites
什麼是 Sprites ?從編程術語來講,Sprites 是屏幕上事物的二維表達。本質上來講,Sprite 就是一個圖片。Pygame 提供一個叫做 Sprites 的基礎類,它就是用來擴展的,可以包含想要在屏幕上呈現的對象一個或多個圖形表示。我們将會擴展Sprite 類,這樣可以使用它的内建方法。我們稱這個新的對象為
Player
。Plyaer
将擴展 Sprite,現在隻有兩個屬性:surf
和rect
。我們也會給surf
塗色(本教程使用白色),如之前 surface 例子,隻是現在 Surface 屬于 Player :
現在我們将上述代碼整合在一起:
EarlGrey:此處代碼省略。
運行上述代碼,你将會在屏幕中心看到一個白色的矩形:
如果将
screen.blit(player.surf,(400,300))
改成screen.blit(player.surf,player.rect)
,你覺得會發生什麼?修改之後,試着在控制台中打印player.rect
。rect
的前兩個屬性分别是rect
左上角的 x 和 y 軸坐标。當你将 rect 傳遞給 blit ,它将會根據這個坐标畫 surface 。我們後續将使用它控制 player 移動。用戶輸入
現在開始才是有趣的部分。我們要把 Player 變得可控制!之前我們提過,按鍵事件
pygame.event.get()
将把最新的事件從事件堆(event stack)中移除。Pygame 還有另外一個事件方法,pygame.event.get_pressed()
。get_pressed()
方法返回一個隊列,其中包含了所有按鍵事件組成的字典,我們将把它放在主循環中,這樣我們将在每一幀上的按鍵。pressed_keys = pygame.event.get_presssed()
現在我們将寫一個方法,接收上面那個字典,并且根據按下的鍵定義 sprite 的行為,代碼如下:
K_UP
、K_DOWN
、K_LEFT
、K_RIGHT
對應鍵盤上的上、下、左 右方向鍵。我們判斷這些鍵是否按下,如果它為真,那麼我們就朝相應的方向移動rect()
。Rects 有兩個内建的移動方法,此處我們使用 move in placemove_ip()
,因為我們希望移動 rect 并且不用複制它。将上述方法添加到
Player
類,将get_pressed()
調用放在主循環中。整體代碼現在應該是這樣的:EarlGrey:此處代碼省略。
現在你可以使用方向鍵移動矩陣塊了。也許你注意到了,你可以将矩形塊移出屏幕,這可能并不是你想要的。所以我們我們需要往 update 方法中添加一些邏輯,檢測矩形的坐标是否移出了 800x600 的屏幕邊界;如果出了邊界,那麼就将它放回在邊界上:
上述代碼沒有使用
move
方法,我們隻要修改 上下左右的相應坐标即可。現在我們添加一些敵人!
首先我們創建一個新的 sprite 類,命名為
Enemy
。依照創建 player 的格式創建:
以上有幾點需要說明。首先,當我們在 surface 上調用
get_rect
時,我們将核心屬性 x 設為 820 ,y 的坐标為一個随機數,由random.randint()
生成。在最終的代碼中,我們會在文件的開頭導入 Random 庫(
import random
)。為什麼選擇随機數?因為我們希望敵人從屏幕右邊(820)的随機位置(0-600)上出現。 我們還将使用random
設置敵人的速度屬性,這樣敵人就會有快有慢。敵人的
update()
方法沒有參數限制(我們不關心敵人的輸入),隻要讓它向着屏幕左邊以一定的速度移動就可以了。update 方法中的最後一個if
語句檢測敵人右側是否通過了屏幕左邊邊界(要确保它們不會一碰到屏幕的邊界就消失)。當他們通過屏幕的邊界後,我們調用 Sprite 的内建方法kill()
,從 sprite 組中删除它們,這樣它們就不會再被渲染出來。kill 不會釋放被它們占用的内存, 需要你确保你不再引用它們,以便 Python 的垃圾回收器回收。Groups
Pygame 提供的另一個很有用的對象是 Sprite 的 Groups。誠如其名,是 Sprite 的集合。為什麼我們要使用 sprite.Group 而不是列表呢? 因為 sprite.Group 有一些内建的方法,有助于解決沖突和更新問題。那現在就創建一個 Group,用來包含遊戲中的所有 Sprites 。創建完 Group 後,我們要将 Player 添加到裡面,因為它是我們目前唯一的 Sprite 。我們也可以為敵人創建一個 group 。 當我們調用 Sprite 的
kill()
方法時,sprite 将會從其所在的全部 group 中删除。enemies = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)
現在有了
all_sprites
的 group ,我們接着改變對象渲染方式,隻要渲染 group 中的所有對象即可:
現在,任何放到
all_sprites
中的對象都會被渲染出來。自定義事件
現在我們為敵人創建了一個 sprite.Group ,但是并沒有實際的敵人。那怎樣才能在屏幕上出現敵人呢?我們當然可以在剛開始的時候創建一堆的敵人,但是這樣遊戲玩不了幾秒。為此,我們創建一個自定義事件,它隔幾秒鐘就會觸發創建一批敵人。我們要監聽該事件,方式和監聽按鍵或退出事件一樣。創建自定義事件十分容易,隻要命名即可:
ADDENEMY = pygame.USEREVENT 1
這樣就可以了!現在,我們有了一個叫做
ADDENEMY
的事件,可以在主程序中監聽它。這裡我們隻需要注意一點,即自定義事件需要有一個獨特的值,要比USEREVENT
的值大,這就是我們為什麼設定它為USEREVENT 1
。這裡說明一點:自定義事件本質上就是整數常量。又因為比USEREVENT
小的數值已經被内置函數占據,所以創建的任何自定義事件都要比USEREVENT
大。定義好事件之後,我們需要将它插入事件隊列中。因為整個遊戲過程中都要創建它們,所以将設置一個計時器。可以通過 PyGame 的
time()
對象實現。pygame.time.set_timer(ADDENEMY,250)
這行代碼告訴 PyGame 每隔 250 毫秒(四分之一秒) 觸發一次
ADDENEMY
事件。這是在主遊戲循環之外執行的,不過在整個遊戲中都處于執行狀态。現在我們添加一些監聽事件的代碼:
謹記:
set_timer()
隻能用來将事件插入到 PyGame 事件隊列中,不做其他任何事情。現在我們會監聽
ADDENEMY
事件,當它觸發時,将創建一個Enemy
類的實例。然後我們将實例添加到enemies
這個 Sprite Group(後續用它來檢測沖突)和all_sprites
Group(這樣它會和其他對象一起渲染)。沖突
這才是 PyGame 的魅力所在!寫沖突代碼(collision code)很難,但是 PyGame 提供了很多沖突檢測方法,你可以在這裡查看其中一部分。本次教程使用 spritecollideany。
spritecollideany()
接受一個 Sprite 對象和一個 Sprite.Group ,檢測 Sprite 對象是否和 Sprite Group 中的其他 Sprites 沖突。這樣,我們可以拿 Player 和敵人所在的 Sprite Group 對比,檢測 player 是否被敵人擊中。代碼實現如下:
檢測 player 是否
enemies
中的 Sprites 沖突,如果發生沖突了,那麼調用player
Sprite 的kill()
方法。因為我們隻渲染了all_sprites
Group 中的 sprites ,kill()
方法将從其所在的全部 Groups 中移出 Sprite ,player
就不再出現,算是“殺死它了”。目前的完整代碼如下:EarlGrey:此處代碼省略。
測試一下!
圖片
現在遊戲可以玩了,但是長得挺醜的。接下來,我們将白色方塊變成有意思的圖片,讓遊戲看上去有遊戲的樣子。
前面的代碼示例中,我們使用了塗色的 Surface 對象表示遊戲裡的所有事物。雖然這樣有助于理解什麼是 Surface 和它如何工作,但是卻讓遊戲變得很醜!現在我們要給 player 和 enemy 添加一些圖片。我喜歡自己畫圖,我把 player 畫成小飛機,enemy 是導彈,這些可以從我的代碼庫中下載。歡迎你使用我的作品,自己畫或者者下載一些免費遊戲素材。
修改對象的構造函數
下面是我們現在的 player 構造函數:
新的構造函數将會是這個樣子的:
我們想用一張圖片替代 Surface 對象。我們将使用
pygame.image.load()
導入圖片的路徑。load()
方法将會返回一個 Surface 對象。我們然後在這個 Surface 對象上調用convert()
創建副本,這樣可以更快地将它畫在屏幕上。接下來,我們在圖片上調用
set_colorkey()
方法。set_colorkey
用于設置圖片的顔色,如果不設置 Pygame 會将圖片設置為透明。這裡我選用白色,因為和飛機的背景色一緻。RLEACCEL 是一個可選參數,它有助于 PyGame 在非加速顯示器上更快地渲染。最後,我們和之前一樣調用
rect()
對象:在圖片上調用get_rect()
。謹記:圖片仍然是一個 surface 對象,隻不過它上面畫了一張圖。
對 enemy 構造函數做同樣的操作:
現在的遊戲雖然和以前一樣,但是比之前漂亮多啦!但是我仍然覺得它少了點什麼東西。讓我們加點不斷漂浮的白雲,這樣會有飛機劃過藍天的感覺。為此,我們需要遵循之前用過的一些原則。首先,我們創建
cloud
對象,畫上白雲的照片,其update()
方法讓它不停地向着屏幕左邊移動。然後,我們需要添加一個自定義事件,每隔一段時間就生成白雲(我們還要将添加白雲到all_sprites
group)。白雲對象的實現如下:
上面的代碼看起來都很熟悉,下面這個事件創建代碼也一樣,我們将它放在 enemy 事件代碼下面:
ADDCLOUD = pygame.USEREVENT 2
pygame.time.set_timer(ADDCLOUD,1000)
現在為它們創建一個新的 Sprite.Group:
clouds = pygame.sprite.Group()
現在,在主循環中,我們需要開始監聽
ADDCLOUD
事件。下面的代碼:
将會變成這樣:
我們還要把 clouds 添加到
all_sprites
Group 和新的 clouds Group 中。我們這麼做,是因為我們使用all_sprites
來渲染,使用clouds
調用它們的 update 函數。也許你很好奇,為什麼我們将它不添加到enemies
Group中;畢竟我們調用的是一樣的 update 函數。原因在于,我們不想檢測白雲和飛機之間的沖突。我們的飛機要順暢地通過所有的雲層。現在剩下的就是調用 clouds Group 的update()
方法了!結語
大功告成!讓我們測試一下,效果應該是這樣的:
完整代碼可以在 GitHub 上的代碼庫獲取!希望本教程對你有用!
點此查看原文
Python 翻譯組是EarlGrey@編程派發起成立的一個專注于 Python 技術内容翻譯的小組,目前已有近 30 名 Python 技術愛好者加入。
翻譯組出品的内容(包括教程、文檔、書籍、視頻)将在編程派微信公衆号首發,歡迎各位 Python 愛好者推薦相關線索。
推薦線索,直接在編程派微信公衆号推文下留言即可。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!