13歲Python開發者寫給青少年的多人遊戲程式設計(上)

justyoung發表於2014-11-30

這篇部落格的作者是一名13歲的Python開發者Julian Meyer。你可以在Google+Twitter上找到他。

我確定,你一定曾和你的朋友們一起玩過線上多人遊戲。但是你是否想過這些遊戲的內部是怎樣實現的呢,遊戲是怎樣在計算機中執行的呢?

在這個教程中,你將通過編寫一個簡單的遊戲來學習有關多人遊戲程式設計。與此同時,你也將學習到物件導向程式設計的思想。

在這個教程中,你將會使用Python語言和它的PyGame模組。如果你剛接觸Python或PyGame模組,你應該先閱讀一下這篇文章,將告訴你關於PyGame的基礎知識。

著手開始吧

首先,你需要安裝PyGame模組。你可以在這個連結here上下載一個mac版的PyGame模組安裝程式。如果你的系統是Mac OS 10.7或以上,請下載Lion版的安裝程式,其它的,則下載Snow Leopard版本。

你也可以按以下的方法下載和安裝PyGame:

如果你使用MacPorts工具,那麼請用如下命令來安裝:sudo port install python2.7 py27-game

如果你使用Fink,那麼請使用如下命令:sudo fink install python27 pygame-py27

如果你使用Homebrew和pip,那麼請在這個連結here中查詢下載和安裝PyGame的命令。

如果你使用的是windows作業系統,那麼你可以在這裡here找到PyGame的安裝程式。

提示:如果你在上面的安裝教程中遇到問題,那麼請確保你在系統上安裝了32位版本的Python。如果你使用的是64位的系統,那麼你應該使用python2.7-32來執行Python。

最後,在這裡下載我們這個專案所需的檔案download the resources for this project,(訪問這個連結需要梯子),這些檔案包括遊戲所需的影象和聲音。

我們的“遊戲規則”

我們給教程中將要製作的遊戲取名為“Boxes”。也許,你在學校中已經和你的朋友們在紙上玩過這個遊戲了。

也許你對遊戲規則並不那麼熟悉,在這裡,就先介紹一下游戲規則:

1.遊戲的棋盤包含7×7個網格點,如果你將這些點用線段連起來,將得到6×6個立方格。

2.輪到每個玩家玩時,玩家將垂直或水平相鄰的兩個點用線段連線起來。

3. 如果一個玩家在網格中連線一條線後,網格中便組成了一個新格子,那麼這個玩家就擁有這個新格子,並獲得1分。

4 在遊戲的最後,擁有格子數最多或分數最高的玩家就是勝利者。

雖然遊戲的規則非常簡單,但這個遊戲玩起來卻非常有趣,特別是當你無聊的時候。但是,如果能線上上和其他玩家玩這個遊戲,是不是會更有趣呢?

 

物件導向程式設計簡介

在我們開始編寫遊戲之前,先討論一下你將在本教程中用到的物件導向程式設計思想。

物件導向程式設計,也被稱為OOP,它是一個基於物件的程式設計方式。物件是由許多資料和與這些資料相關的邏輯組成。例如,你有一個“狗”物件,那麼這個物件就包含了一些資料(例如,狗的名字、它最大的樂趣等),以及相關邏輯(例如,使狗發出叫聲的指令)。

物件是由叫做“類”的模板例項化而成的,類定義了物件所包含的資料,以及這個物件能做的事情。物件的資料和它能做的事情分別稱為物件的屬性和方法。

方法即是一些函式,你可通過呼叫函式使物件完成某一任務。例如你可將car.drive()這行程式碼,理解為是告訴“汽車(car)”這個物件“開車(drive)”。屬性是一些屬於物件的變數。繼續上一個例子,你的汽車“car”也許會有一個名為汽油(gas)的屬性,程式碼car.gas = 100的意思是將汽車的汽油量設定為100.

剛才所說的兩個程式碼操作的是一個已經存在的物件。回想我們剛才提到的,汽車(car)類是一個模板,它定義了怎樣例項化一個汽車(car)物件,這個定義包括了物件的屬性和方法。在對方法的定義中,你會看到物件內部的方法操作自己的程式碼。例如,你會看到這樣的程式碼:self.gas=100,它不同於car.gas=100,self.gas=100的意思是汽車(car)物件告訴自己,將自己的汽油量(gas)設定為100。

OOP包含了許多內容,但是以上介紹的基礎知識已經足夠我們的教程了。我們用程式碼將Boxes遊戲描述成許多物件的互動。這些物件都包含了我們在類中對其定義的若干屬性和方法。當你在編寫程式碼時,應注意,你寫的類程式碼是從類的內部操作自身,還是操作其它外部物件。

 

編寫一個簡單的物件導向的遊戲

有許多物件導向的框架可被用於我們的遊戲設計。我們給Boxes遊戲設計了兩個物件,一個物件負責遊戲的客戶端,另一個物件負責遊戲的伺服器,現在,讓我們來建立一個客戶端的主類,這個類的程式碼將在使用者啟動遊戲時執行。

在製作每一個遊戲之前,我喜歡先為這個遊戲程式建立一個資料夾。當你解壓剛才下載的專案壓縮檔案時,將會看到一個名為boxes的資料夾。你需要將你的原始檔和影象檔案一起放在這個資料夾中。

在這個檔案目錄下,使用你最喜歡的編輯器建立一個名為boxes.py的檔案(如果你沒有最喜歡的編輯器,那麼我推薦你在mac上使用TextEdit,或者在Windows上使用Notepad)。然後在這個檔案中使用import語句:

這條語句匯入了PyGame模組,我們在後續編碼中,將使用這個模組。在進入下一步之前,你應該首先測試一下,這個模組是否已經正確匯入並可以使用了。開啟終端,並用cd命令,進入我們的專案資料夾,然後在終端裡輸入python boxes.py命令,例如,在我的機器上應輸入這樣的命令:

如果你能成功執行這個命令,就說明PyGame模組已經正確的安裝在你的電腦上了。我們就可以進入下一步教程了。

提示:如果你執行上述命令時,收到“No module named pygame”的錯誤提示,則說明你沒有安裝PyGame,或者你安裝的PyGame版本和你係統中的Python版本不相容。例如,如果你使用MacPorts來安裝Python 2.7和PyGame,那麼你將使用這個命令來安裝他們:port install python2.7 py27-game PyGame,安裝好之後,你需要保證在終端裡呼叫2.7版本的Python:python2.7。如果執行上述程式碼得到這個錯誤:

就說明你需要執行32位模式的Python,例如:python2.7-32

接下來,像定義每一個類一樣,在boxes.py檔案中新增類定義程式碼:

這段程式碼的第一行告訴編譯器,你將建立一個名叫BoxesGame的新類。第二行定義了一個名叫__init__的方法。init兩邊的雙下劃線暗示著這是一個特殊的方法名字。事實上,這個名稱__init__確定了這個類的構造方法,這個方法將在你建立或例項化一個類的物件時呼叫。

現在,你將在init方法中編寫初始化PyGame的程式碼。將以下的程式碼緊接在原來程式碼中的註釋部分,#put something here…:

請注意輸入程式碼的縮排格式,即剛才輸入的程式碼都要和“#put

something here…”這段程式碼左對齊。你可以在這個連結中檢視更多

關於Python程式碼縮排的內容:Python Indentation.

接下來我們一段一段解釋剛才新增的程式碼:

1. 首先,你初始化了PyGame和兩個變數width、height,這兩個變數是用來設定我們遊戲窗體的大小的。

2. 接著,用width和height變數設定窗體的寬和高。這段程式碼也設定了窗體的標題。

3. 最後,你初始化了PyGame的時鐘,這個時鐘將會用來追蹤遊戲中的時間。

接下來,我們新增update()方法,這個方法每隔一段時間更新一次遊戲,包括重繪介面和接收使用者輸入。將以下的程式碼新增在__init__方法之後就能實現這些功能(update程式碼的左縮排必須和__init__相同):

這是一個簡單的迴圈方法,這個方法定期清除窗體中的內容並檢查使用者是否想退出遊戲。稍後,你會在這個方法中新增更多的內容。目前,你執行這個Python檔案並不會見到什麼效果,因為現在你做的僅是定義了一個名為BoxesGame的類。你還需要建立這個類的物件,然後使用這個物件執行遊戲!

現在,我們已經有了update方法,讓我們新增執行遊戲主類的方法吧。之後,你會在這個遊戲中新增一些基本的圖片,例如繪製遊戲中的棋盤。

在原始檔的末尾新增這些程式碼來執行我們編寫的遊戲(程式碼的左縮排必須與檔案的左縮排相同):

這三行程式碼體現了物件導向程式設計的一個優點:真正讓程式執行的程式碼其實只有三行。

至此,整個原始檔的內容應該是這樣的:

就這樣,是不是很簡單?現在,讓我們來執行這個遊戲:

正如你看到的那樣,遊戲執行的結果就是看到一個令人非常印象深刻的黑色介面。

也許你現在並不理解這些程式碼,但是遊戲的編寫就像是一個戰略過程。把自己想象成一個建築設計師來編寫遊戲。你剛剛就為你的建築打下了一個堅固的地基。每個雄偉的建築都有一個良好的奠基,所以在你開始編寫遊戲之前,需要首先做好規劃。

讓我們新增另一個方法。如果你不記得什麼是方法的話,那麼你可以再看看教程中的“物件導向程式設計簡介”這部分內容。

 

在遊戲介面中繪製棋盤和線段

PyGame將窗體的左上角的座標定義為(0,0)。所以我們為Boxes中的網格點定義一個座標系統,其中(0,0)表示左上角的點,(6,6)表示右下角的點:

需要用某種方法來表示遊戲中所有可能的線段。在遊戲中,我們有垂直和水平的兩種線段。讓我們考慮一下,如何用一個列表來表示所有的線段集合,這是一個可以表示所有垂直和水平線段集合的方法:

從程式設計的角度來說,一個列表也被稱為一個陣列。當你的列表元素也是列表時,這個列表就被稱為二維陣列,例如水平和垂直線段的集合。

舉一個例子,如果要表示從(0,0)點到(1,1)點水平方向上的路線,那麼,就應該在“horizontal lines”這個列表中選擇第0行,第0列的元素來表示。

請注意,“horizontal lines”列表有6行7列,而“vertical lines”有7行6列。(這裡按照作者的程式碼,horizontal lines應該是7行6列,vertical lines是6行7列)

將下面兩行程式碼新增到__init__程式碼中,來定義“horizontal lines”和“vertical lines”這兩個陣列:

[valuePerItem for x in y],這個語句可以快速建立一個陣列。上述程式碼在建立陣列的同時還將陣列的元素初始化為False。False代表一個空的區域。

至此,你就有了表示棋盤的方法了,接下來我們來看看怎樣用程式碼畫出這個棋盤。

首先,新建一個名為initGraphics()的方法。這個方法將被__init___方法呼叫,但為了使你的程式碼結構保持清晰,我們將載入圖片的程式碼封裝到其它方法中。在__init__方法之前,新增這個程式碼:

正如你看到的那樣,我們有三個垂直的線條影象:一個普通(空)線條,一個已被畫過(佔用)的線條和一個懸浮效果線條。將這些垂直的線條影象旋轉90度,就可以表示水平的線條了。線條的影象儲存在你下載的專案資原始檔夾裡,並且它們必須和你的python檔案同處一個目錄下。

你已經有了一個載入影象的方法,但是你需要呼叫它。猜一猜應該在哪新增呼叫的程式碼?

當你有了答案時,點選下面的“show”按鈕,看看你是否答對了。

在__init__方法的末尾新增這個程式碼:

接下來,你應該新增繪製棋盤的程式碼。若要迴圈整個棋盤上的x座標和y座標,你就必須在一個for迴圈中再巢狀一個for迴圈。每一個for迴圈可以迴圈x或y方向上的所有點。在__init__方法之後新增這段程式碼:

這段程式碼分別迴圈了垂直的和水平的線段組成的列表,並檢查每個線段是否被點選選中了。self.boardv[y][x]和self.boardh[y][x]返回true或false取決於這條線段是否被玩家選中了。

現在執行這個程式仍然不會有任何作用。現在你完成的程式碼僅定義了在drawBoard這個方法被呼叫時,drawBoard所要做的事情。現在讓我們在update方法中新增對drawBoard的呼叫吧。在清空窗體的語句screen.fill(0)後新增下面的程式碼:

當然,作為一個優秀的程式設計師,請記住在你的程式碼中新增註釋,解釋你剛才寫的程式碼。

現在可以執行你剛寫的程式碼了,程式執行後,你將看到介面上出現了你繪製的網格:

每次我寫繪製圖片的程式碼時,我都會對這些程式碼做一點測試,因為這樣很有趣,而且也能發現一些BUG。在定義self.boardh和self.boardy的程式碼的後面新增這個程式碼:

執行修改後的程式碼,你將看到,從(5,3)到(5,4)這條水平的線將會亮起:

很酷對吧?現在可以刪除我們剛才新增的測試程式碼了。好,現在你已經成功完成棋盤的繪製了,這是我們遊戲程式設計中的難點之一。

新增其它型別的線條

下一步,你需要找到離滑鼠指標最近的一個線條,然後在那個位置繪製一個有懸浮效果的線條。

首先,在程式碼原始檔的頂部寫下這行程式碼,來匯入我們馬上用到的數學庫:

然後在pygame.display.flip()之前,新增以下這一大段程式碼:

哇,好長一段程式碼。我們一段一段來看看這些程式碼吧:

1 首先,你通過PyGame的內建函式來獲得滑鼠指標的位置。

2 接下來,在前面的程式碼中,我們將網格的大小設定成了64×64,所以我們可以計算獲得滑鼠指標在棋盤網格中的位置。

3 你需要檢查你的滑鼠指標是更接近方塊的上邊、下邊還是是方塊的左邊、右邊。因為你需要判斷使用者的滑鼠是懸浮在一條水平的線條上還是垂直的線條上。

4 通過is_horizontal這個變數,計算線條的新位置。

5 用boardh或boardv初始化board變數。

6 最後,你需要將懸浮效果的線畫到介面上,你必須考慮是畫水平的線還是垂直的線,以及這個線條畫在一個方塊的上邊、下邊、左邊或是右邊。你還必須檢查你畫的線條是否超出了棋盤的邊界。如果線條超出了邊界或線條已經被畫過了,那麼你就不需要在這樣的線條上新增懸浮效果。

執行這個程式,你會驚喜地發現,當你的滑鼠靠近一個線條時,線條就會亮起。

如果你像我一樣,現在就一定會激動得將滑鼠在螢幕上移來移去。現在就請盡情享受你的成果吧!

好,現在當使用者滑鼠移到網格上的某線條時,那條線就會亮起。但是,你所寫的不僅僅是一個一直移動滑鼠的遊戲。你還需要增加這樣一個功能:當滑鼠點選一個線條時,這個線條就表示被玩家畫過,也就是玩家擁有了這個線條。

要完成這個功能,你需要使用PyGame的內建函式:pygame.mouse.get_pressed()[0]。這個函式返回1或0取決於玩家是否點選了這個線條。在我告訴你如何實現這個功能前,你可以仔細想一想應該怎樣做呢?回想一下,我們剛才是怎樣使用if語句的,以及怎樣在介面上繪製網格的。

將這些程式碼直接新增到剛才那段程式碼的後面:

現在執行修改過的程式,如果你點選滑鼠,你將會線上條懸浮的地方畫上這個線條。正如你看到的,你的程式碼完成了這些工作:你放置的線條取決於滑鼠是否被單擊,以及線條是水平的還是垂直的。

這個程式碼有一個問題,如果你在棋盤網格的下方點選滑鼠,那麼我們寫的遊戲將崩潰。讓我們看看造成這個現象的原因吧。通常,當一個錯誤發生時,你能在終端上看到一個錯誤報告,這裡我們遇到的錯誤報告是這樣的:

這個錯誤是因為boardh陣列越界了。還記得那個isoutfobounds變數嗎?這個變數可以為我們解決陣列越界的問題,只要簡單地像下面這樣修改一行程式碼即可:

現在,如果你在棋盤網格外點選滑鼠,遊戲也不會崩潰。好的,你剛才就算做了除錯的工作!

在你實現遊戲的服務端邏輯程式碼前,我們先在客戶端新增一些最後的潤色程式碼。

最後的潤色

有一個問題一直困擾著我,那就是介面上網格線交叉部分的空缺。幸運的是,你可以很容易地用一個7×7大小的圖片來填補這個空缺。當然,你需要一個影象檔案,因此,我們現在就一口氣載入這張圖片以及專案中你需要使用的所有圖片吧。

在initGraphics()後新增這些程式碼:

現在,你載入了需要的圖片檔案,讓我們將載入的圖片繪製到這49個空缺點上吧。將以下的程式碼新增到drawBoard():

好的,現在我們來測試一下游戲吧。遊戲執行後,你將會看到一個更好看的網格棋盤。

接下來,讓我們在遊戲介面的下方放置一個平視顯示器(HUD)吧。首先,你需要新建一個drawHUD()方法。

這段程式碼是在遊戲介面的背景上繪製得分的皮膚。

讓我說明一下PyGame處理字型的三個步驟:

1 首先,你需要設定一個字型以及這個字型的大小。

2 接下來,你呼叫font.render(“your text here”),使你輸入的文字以你設定的字型呈現。

3 然後,將這些字像圖片那樣繪製到遊戲的介面上。

現在,你已經知道怎樣在介面上放置文字了,我們就可以繪製HUD的另一部分:“Your Turn”提示文字。在drawHUD()方法的尾部新增以下的程式碼:

同樣在pygame.init()的後面新增這行程式碼:

剛才新增的程式碼建立了字型,將字型的顏色設定成白色,然後將這些字繪製到遊戲介面上。在執行遊戲之前,將這行程式碼新增到self.drawBoard()的後面:

執行這個程式後,你會看到“Your Turn”這個文字出現在遊戲介面的下方。如果你仔細看遊戲介面的下方,你會發現精細的背景質地。

這很棒不是嗎,但是你仍然需要在“Your Turn”之後新增一個指示圖示,來提醒玩家輪到他們了。

在做這之前,你需要讓遊戲知道,這個遊戲輪到誰了。所以你需要在__init__程式碼的尾部新增這行程式碼。

將這行程式碼新增到drawHUD()的尾部,以在介面上繪製指示圖片。

現在執行遊戲後,你將會看到綠色的指示器。好了,你可以把這個任務從你的未完成事項中去除了。

接下來,讓我們建立每個玩家的分數文字。使用如下的程式碼初始化兩個玩家的分數變數,將這些程式碼新增在__init__的末尾:

這裡,你需要新增一些其他你會在本階段使用的變數。還記得怎樣新增文字嗎?現在你將重複寫一些剛才寫過的程式碼,但是使用的是不同大小的字型。將這些程式碼新增到drawHUD()的尾部:

執行程式,檢查一下你的成果吧。

現在你已經完成了HUD的功能。現在還需要做一些工作來完善客戶端,原諒我吧……

接下來,我們新增一個簡單的變數來表示玩家所擁有的網格線。這些變數能讓你跟蹤玩家所擁有的方格。你需要使用這個變數正確地給玩家所擁有的方塊上色並記下玩家得分。請記住,擁有最多方格的玩家將獲得遊戲的勝利!

首先,在__init__的末尾初始化一個陣列:

和之前畫網格線的方法一樣,迴圈二維陣列在螢幕上繪製玩家所擁有的網格。在類的底部新增這個方法。

這個方法判斷每一個給定的方塊是否需要著色,如果需要,方法將給方塊畫上指定的顏色(每一個玩家都有自己的顏色)。

你還需要給遊戲增加一個勝利和失敗的介面。下面的方法就能完成這個功能,將它新增到類的底部:

當然,現在你還沒有辦法在遊戲中觸發這個介面。在下一部分的教程裡,當你實現了遊戲的服務端後,你就可以檢視這個介面了。

請記住,在你新增完這些介面後,遊戲的服務端就可以任意操縱客戶端了。除了服務端和客戶端之間的通訊問題需要一點處理外,從現在開始,你不需要對客戶端進行改動了。

但是,為了確保我們剛才編寫的方法是正確的,我們在__init__方法的尾部呼叫finished()。你將看到一個向上面那幅圖一樣的game over的畫面。

接下來要做什麼呢?

這是到目前為止,本教程的原始碼可在這裡找到source code

恭喜你!你已經完成了一個整潔而漂亮的遊戲客戶端。當然,雖然你出色地完成了遊戲的客戶端程式碼,但是並沒有實現任何遊戲的邏輯,所以我們的開發工作並沒有結束。

現在你需要進入我們教程的第二部分,教程的第二部分主要講述遊戲的服務端,隨著教程,你將開始製作真正的多玩家遊戲。

相關文章