Linux中國

使用 Python 和 Asyncio 編寫在線多人遊戲(三)

在這個系列中,我們基於多人遊戲 貪吃蛇 來製作一個非同步的 Python 程序。上一篇文章聚焦於編寫遊戲循環上,而本系列第 1 部分則涵蓋了如何非同步化

4、製作一個完整的遊戲

4.1 工程概覽

在此部分,我們將回顧一個完整在線遊戲的設計。這是一個經典的貪吃蛇遊戲,增加了多玩家支持。你可以自己在 (http://snakepit-game.com) 親自試玩。源碼在 GitHub 的這個倉庫。遊戲包括下列文件:

  • server.py - 處理主遊戲循環和連接。
  • game.py - 主要的 Game 類。實現遊戲的邏輯和遊戲的大部分通信協議。
  • player.py - Player 類,包括每一個獨立玩家的數據和蛇的展現。這個類負責獲取玩家的輸入並相應地移動蛇。
  • datatypes.py - 基本數據結構。
  • settings.py - 遊戲設置,在注釋中有相關的說明。
  • index.html - 客戶端所有的 html 和 javascript代碼都放在一個文件中。

4.2 遊戲循環內窺

多人的貪吃蛇遊戲是個用於學習十分好的例子,因為它簡單。所有的蛇在每個幀中移動到一個位置,而且幀以非常低的頻率進行變化,這樣就可以讓你就觀察到遊戲引擎到底是如何工作的。因為速度慢,對於玩家的按鍵不會立馬響應。按鍵先是記錄下來,然後在一個遊戲循環迭代的最後計算下一幀時使用。

現代的動作遊戲幀頻率更高,而且通常服務端和客戶端的幀頻率是不相等的。客戶端的幀頻率通常依賴於客戶端的硬體性能,而服務端的幀頻率則是固定的。一個客戶端可能根據一個遊戲「嘀嗒」的數據渲染多個幀。這樣就可以創建平滑的動畫,這個受限於客戶端的性能。在這個例子中,服務端不僅傳輸物體的當前位置,也要傳輸它們的移動方向、速度和加速度。客戶端的幀頻率稱之為 FPS( 每秒幀數 frames per second ),服務端的幀頻率稱之為 TPS( 每秒滴答數 ticks per second )。在這個貪吃蛇遊戲的例子中,二者的值是相等的,在客戶端顯示的一幀是在服務端的一個「嘀嗒」內計算出來的。

我們使用類似文本模式的遊戲區域,事實上是 html 表格中的一個字元寬的小格。遊戲中的所有對象都是通過表格中的不同顏色字元來表示。大部分時候,客戶端將按鍵的碼發送至服務端,然後每個「滴答」更新遊戲區域。服務端一次更新包括需要更新字元的坐標和顏色。所以我們將所有遊戲邏輯放置在服務端,只將需要渲染的數據發送給客戶端。此外,我們通過替換通過網路發送的數據來減少遊戲被破解的概率。

4.3 它是如何運行的?

這個遊戲中的服務端出於簡化的目的,它和例子 3.2 類似。但是我們用一個所有服務端都可訪問的 Game 對象來代替之前保存了所有已連接 websocket 的全局列表。一個 Game 實例包括一個表示連接到此遊戲的玩家的 Player 對象的列表(在 self._players 屬性裡面),以及他們的個人數據和 websocket 對象。將所有遊戲相關的數據存儲在一個 Game 對象中,會方便我們增加多個遊戲房間這個功能——如果我們要增加這個功能的話。這樣,我們維護多個 Game 對象,每個遊戲開始時創建一個。

客戶端和服務端的所有交互都是通過編碼成 json 的消息來完成。來自客戶端的消息僅包含玩家所按下鍵碼對應的編號。其它來自客戶端消息使用如下格式:

[command, arg1, arg2, ... argN ]

來自服務端的消息以列表的形式發送,因為通常一次要發送多個消息 (大多數情況下是渲染的數據):

[[command, arg1, arg2, ... argN ], ... ]

在每次遊戲循環迭代的最後會計算下一幀,並且將數據發送給所有的客戶端。當然,每次不是發送完整的幀,而是發送兩幀之間的變化列表。

注意玩家連接上服務端後不是立馬加入遊戲。連接開始時是 觀望者 spectator 模式,玩家可以觀察其它玩家如何玩遊戲。如果遊戲已經開始或者上一個遊戲會話已經在屏幕上顯示 「game over」 (遊戲結束),用戶此時可以按下 「Join」(參與),來加入一個已經存在的遊戲,或者如果遊戲沒有運行(沒有其它玩家)則創建一個新的遊戲。後一種情況下,遊戲區域在開始前會被先清空。

遊戲區域存儲在 Game._field 這個屬性中,它是由嵌套列表組成的二維數組,用於內部存儲遊戲區域的狀態。數組中的每一個元素表示區域中的一個小格,最終小格會被渲染成 html 表格的格子。它有一個 Char 的類型,是一個 namedtuple ,包括一個字元和顏色。在所有連接的客戶端之間保證遊戲區域的同步很重要,所以所有遊戲區域的更新都必須依據發送到客戶端的相應的信息。這是通過 Game.apply_render() 來實現的。它接受一個 Draw 對象的列表,其用於內部更新遊戲區域和發送渲染消息給客戶端。

我們使用 namedtuple 不僅因為它表示簡單數據結構很方便,也因為用它生成 json 格式的消息時相對於 dict 更省空間。如果你在一個真實的遊戲循環中需要發送複雜的數據結構,建議先將它們序列化成一個簡單的、更短的格式,甚至打包成二進位格式(例如 bson,而不是 json),以減少網路傳輸。

Player 對象包括用 deque 對象表示的蛇。這種數據類型和 list 相似,但是在兩端增加和刪除元素時效率更高,用它來表示蛇很理想。它的主要方法是 Player.render_move(),它返回移動玩家的蛇至下一個位置的渲染數據。一般來說它在新的位置渲染蛇的頭部,移除上一幀中表示蛇的尾巴的元素。如果蛇吃了一個數字變長了,在相應的多個幀中尾巴是不需要移動的。蛇的渲染數據在主類的 Game.next_frame() 中使用,該方法中實現所有的遊戲邏輯。這個方法渲染所有蛇的移動,檢查每一個蛇前面的障礙物,而且生成數字和「石頭」。每一個「嘀嗒」,game_loop() 都會直接調用它來生成下一幀。

如果蛇頭前面有障礙物,在 Game.next_frame() 中會調用 Game.game_over()。它後通知所有的客戶端那個蛇死掉了 (會調用 player.render_game_over() 方法將其變成石頭),然後更新表中的分數排行榜。Player 對象的 alive 標記被置為 False,當渲染下一幀時,這個玩家會被跳過,除非他重新加入遊戲。當沒有蛇存活時,遊戲區域會顯示 「game over」 (遊戲結束)。而且,主遊戲循環會停止,設置 game.running 標記為 False。當某個玩家下次按下 「Join」 (加入)時,遊戲區域會被清空。

在渲染遊戲的每個下一幀時也會產生數字和石頭,它們是由隨機值決定的。產生數字或者石頭的概率可以在 settings.py 中修改成其它值。注意數字的產生是針對遊戲區域每一個活的蛇的,所以蛇越多,產生的數字就越多,這樣它們都有足夠的食物來吃掉。

4.4 網路協議

從客戶端發送消息的列表:

命令 參數 描述
new_player [name] 設置玩家的昵稱
join 玩家加入遊戲

從服務端發送消息的列表:

命令 參數 描述
handshake [id] 給一個玩家指定 ID
world [[(char, color), ...], ...] 初始化遊戲區域(世界地圖)
reset_world 清除實際地圖,替換所有字元為空格
render [x, y, char, color] 在某個位置顯示字元
p_joined [id, name, color, score] 新玩家加入遊戲
p_gameover [id] 某個玩家遊戲結束
p_score [id, score] 給某個玩家計分
top_scores [[name, score, color], ...] 更新排行榜

典型的消息交換順序:

客戶端 -> 服務端 服務端 -> 客戶端 服務端 -> 所有客戶端 備註
new_player 名字傳遞給服務端
handshake 指定 ID
world 初始化傳遞的世界地圖
top_scores 收到傳遞的排行榜
join 玩家按下「Join」,遊戲循環開始
reset_world 命令客戶端清除遊戲區域
render, render, ... 第一個遊戲「滴答」,渲染第一幀
(key code) 玩家按下一個鍵
render, render, ... 渲染第二幀
p_score 蛇吃掉了一個數字
render, render, ... 渲染第三幀
... 重複若干幀 ...
p_gameover 試著吃掉障礙物時蛇死掉了
top_scores 更新排行榜(如果需要更新的話)

5. 總結

說實話,我十分享受 Python 最新的非同步特性。新的語法做了改善,所以非同步代碼很容易閱讀。可以明顯看出哪些調用是非阻塞的,什麼時候發生 greenthread 的切換。所以現在我可以宣稱 Python 是非同步編程的好工具。

SnakePit 在 7WebPages 團隊中非常受歡迎。如果你在公司想休息一下,不要忘記給我們在 Twitter 或者 Facebook 留下反饋。

via: https://7webpages.com/blog/writing-online-multiplayer-game-with-python-and-asyncio-part-3/

作者:Kyrylo Subbotin 譯者:chunyang-wen 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

(題圖來自:wallpaperinhd.net


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國