新聞中心
在這個(gè)系列中,我們基于多人游戲 貪吃蛇 來制作一個(gè)異步的 Python 程序。上一篇文章聚焦于編寫游戲循環(huán)上,而本系列第 1 部分則涵蓋了如何異步化。

創(chuàng)新互聯(lián)建站是一家集網(wǎng)站建設(shè),張灣企業(yè)網(wǎng)站建設(shè),張灣品牌網(wǎng)站建設(shè),網(wǎng)站定制,張灣網(wǎng)站建設(shè)報(bào)價(jià),網(wǎng)絡(luò)營銷,網(wǎng)絡(luò)優(yōu)化,張灣網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競爭力。可充分滿足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時(shí)我們時(shí)刻保持專業(yè)、時(shí)尚、前沿,時(shí)刻以成就客戶成長自我,堅(jiān)持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。
- 代碼戳這里
4、制作一個(gè)完整的游戲
4.1 工程概覽
在此部分,我們將回顧一個(gè)完整在線游戲的設(shè)計(jì)。這是一個(gè)經(jīng)典的貪吃蛇游戲,增加了多玩家支持。你可以自己在 (http://snakepit-game.com) 親自試玩。源碼在 GitHub 的這個(gè)倉庫。游戲包括下列文件:
- server.py - 處理主游戲循環(huán)和連接。
- game.py - 主要的 Game 類。實(shí)現(xiàn)游戲的邏輯和游戲的大部分通信協(xié)議。
- player.py - Player 類,包括每一個(gè)獨(dú)立玩家的數(shù)據(jù)和蛇的展現(xiàn)。這個(gè)類負(fù)責(zé)獲取玩家的輸入并相應(yīng)地移動(dòng)蛇。
- datatypes.py - 基本數(shù)據(jù)結(jié)構(gòu)。
- settings.py - 游戲設(shè)置,在注釋中有相關(guān)的說明。
- index.html - 客戶端所有的 html 和 javascript代碼都放在一個(gè)文件中。
4.2 游戲循環(huán)內(nèi)窺
多人的貪吃蛇游戲是個(gè)用于學(xué)習(xí)十分好的例子,因?yàn)樗唵?。所有的蛇在每個(gè)幀中移動(dòng)到一個(gè)位置,而且?guī)苑浅5偷念l率進(jìn)行變化,這樣就可以讓你就觀察到游戲引擎到底是如何工作的。因?yàn)樗俣嚷瑢τ谕婕业陌存I不會立馬響應(yīng)。按鍵先是記錄下來,然后在一個(gè)游戲循環(huán)迭代的最后計(jì)算下一幀時(shí)使用。
現(xiàn)代的動(dòng)作游戲幀頻率更高,而且通常服務(wù)端和客戶端的幀頻率是不相等的??蛻舳说膸l率通常依賴于客戶端的硬件性能,而服務(wù)端的幀頻率則是固定的。一個(gè)客戶端可能根據(jù)一個(gè)游戲“嘀嗒”的數(shù)據(jù)渲染多個(gè)幀。這樣就可以創(chuàng)建平滑的動(dòng)畫,這個(gè)受限于客戶端的性能。在這個(gè)例子中,服務(wù)端不僅傳輸物體的當(dāng)前位置,也要傳輸它們的移動(dòng)方向、速度和加速度。客戶端的幀頻率稱之為 FPS(每秒幀數(shù)(frames per second)),服務(wù)端的幀頻率稱之為 TPS(每秒滴答數(shù)(ticks per second))。在這個(gè)貪吃蛇游戲的例子中,二者的值是相等的,在客戶端顯示的一幀是在服務(wù)端的一個(gè)“嘀嗒”內(nèi)計(jì)算出來的。
我們使用類似文本模式的游戲區(qū)域,事實(shí)上是 html 表格中的一個(gè)字符寬的小格。游戲中的所有對象都是通過表格中的不同顏色字符來表示。大部分時(shí)候,客戶端將按鍵的碼發(fā)送至服務(wù)端,然后每個(gè)“滴答”更新游戲區(qū)域。服務(wù)端一次更新包括需要更新字符的坐標(biāo)和顏色。所以我們將所有游戲邏輯放置在服務(wù)端,只將需要渲染的數(shù)據(jù)發(fā)送給客戶端。此外,我們通過替換通過網(wǎng)絡(luò)發(fā)送的數(shù)據(jù)來減少游戲被破解的概率。
4.3 它是如何運(yùn)行的?
這個(gè)游戲中的服務(wù)端出于簡化的目的,它和例子 3.2 類似。但是我們用一個(gè)所有服務(wù)端都可訪問的 Game 對象來代替之前保存了所有已連接 websocket 的全局列表。一個(gè) Game 實(shí)例包括一個(gè)表示連接到此游戲的玩家的 Player 對象的列表(在 self._players 屬性里面),以及他們的個(gè)人數(shù)據(jù)和 websocket 對象。將所有游戲相關(guān)的數(shù)據(jù)存儲在一個(gè) Game 對象中,會方便我們增加多個(gè)游戲房間這個(gè)功能——如果我們要增加這個(gè)功能的話。這樣,我們維護(hù)多個(gè) Game 對象,每個(gè)游戲開始時(shí)創(chuàng)建一個(gè)。
客戶端和服務(wù)端的所有交互都是通過編碼成 json 的消息來完成。來自客戶端的消息僅包含玩家所按下鍵碼對應(yīng)的編號。其它來自客戶端消息使用如下格式:
- [command, arg1, arg2, ... argN ]
來自服務(wù)端的消息以列表的形式發(fā)送,因?yàn)橥ǔR淮我l(fā)送多個(gè)消息 (大多數(shù)情況下是渲染的數(shù)據(jù)):
- [[command, arg1, arg2, ... argN ], ... ]
在每次游戲循環(huán)迭代的最后會計(jì)算下一幀,并且將數(shù)據(jù)發(fā)送給所有的客戶端。當(dāng)然,每次不是發(fā)送完整的幀,而是發(fā)送兩幀之間的變化列表。
注意玩家連接上服務(wù)端后不是立馬加入游戲。連接開始時(shí)是觀望者(spectator)模式,玩家可以觀察其它玩家如何玩游戲。如果游戲已經(jīng)開始或者上一個(gè)游戲會話已經(jīng)在屏幕上顯示 “game over” (游戲結(jié)束),用戶此時(shí)可以按下 “Join”(參與),來加入一個(gè)已經(jīng)存在的游戲,或者如果游戲沒有運(yùn)行(沒有其它玩家)則創(chuàng)建一個(gè)新的游戲。后一種情況下,游戲區(qū)域在開始前會被先清空。
游戲區(qū)域存儲在 Game._field 這個(gè)屬性中,它是由嵌套列表組成的二維數(shù)組,用于內(nèi)部存儲游戲區(qū)域的狀態(tài)。數(shù)組中的每一個(gè)元素表示區(qū)域中的一個(gè)小格,最終小格會被渲染成 html 表格的格子。它有一個(gè) Char 的類型,是一個(gè) namedtuple ,包括一個(gè)字符和顏色。在所有連接的客戶端之間保證游戲區(qū)域的同步很重要,所以所有游戲區(qū)域的更新都必須依據(jù)發(fā)送到客戶端的相應(yīng)的信息。這是通過 Game.apply_render() 來實(shí)現(xiàn)的。它接受一個(gè) Draw 對象的列表,其用于內(nèi)部更新游戲區(qū)域和發(fā)送渲染消息給客戶端。
我們使用 namedtuple 不僅因?yàn)樗硎竞唵螖?shù)據(jù)結(jié)構(gòu)很方便,也因?yàn)橛盟?json 格式的消息時(shí)相對于 dict 更省空間。如果你在一個(gè)真實(shí)的游戲循環(huán)中需要發(fā)送復(fù)雜的數(shù)據(jù)結(jié)構(gòu),建議先將它們序列化成一個(gè)簡單的、更短的格式,甚至打包成二進(jìn)制格式(例如 bson,而不是 json),以減少網(wǎng)絡(luò)傳輸。
Player 對象包括用 deque 對象表示的蛇。這種數(shù)據(jù)類型和 list 相似,但是在兩端增加和刪除元素時(shí)效率更高,用它來表示蛇很理想。它的主要方法是 Player.render_move(),它返回移動(dòng)玩家的蛇至下一個(gè)位置的渲染數(shù)據(jù)。一般來說它在新的位置渲染蛇的頭部,移除上一幀中表示蛇的尾巴的元素。如果蛇吃了一個(gè)數(shù)字變長了,在相應(yīng)的多個(gè)幀中尾巴是不需要移動(dòng)的。蛇的渲染數(shù)據(jù)在主類的 Game.next_frame() 中使用,該方法中實(shí)現(xiàn)所有的游戲邏輯。這個(gè)方法渲染所有蛇的移動(dòng),檢查每一個(gè)蛇前面的障礙物,而且生成數(shù)字和“石頭”。每一個(gè)“嘀嗒”,game_loop() 都會直接調(diào)用它來生成下一幀。
如果蛇頭前面有障礙物,在 Game.next_frame() 中會調(diào)用 Game.game_over()。它后通知所有的客戶端那個(gè)蛇死掉了 (會調(diào)用 player.render_game_over() 方法將其變成石頭),然后更新表中的分?jǐn)?shù)排行榜。Player 對象的 alive 標(biāo)記被置為 False,當(dāng)渲染下一幀時(shí),這個(gè)玩家會被跳過,除非他重新加入游戲。當(dāng)沒有蛇存活時(shí),游戲區(qū)域會顯示 “game over” (游戲結(jié)束)。而且,主游戲循環(huán)會停止,設(shè)置 game.running 標(biāo)記為 False。當(dāng)某個(gè)玩家下次按下 “Join” (加入)時(shí),游戲區(qū)域會被清空。
在渲染游戲的每個(gè)下一幀時(shí)也會產(chǎn)生數(shù)字和石頭,它們是由隨機(jī)值決定的。產(chǎn)生數(shù)字或者石頭的概率可以在 settings.py 中修改成其它值。注意數(shù)字的產(chǎn)生是針對游戲區(qū)域每一個(gè)活的蛇的,所以蛇越多,產(chǎn)生的數(shù)字就越多,這樣它們都有足夠的食物來吃掉。
4.4 網(wǎng)絡(luò)協(xié)議
從客戶端發(fā)送消息的列表:
| 命令 | 參數(shù) | 描述 |
| new_player | [name] | 設(shè)置玩家的昵稱 |
| join | 玩家加入游戲 |
從服務(wù)端發(fā)送消息的列表:
| 命令 | 參數(shù) | 描述 |
| handshake | [id] | 給一個(gè)玩家指定 ID |
| world | [[(char, color), ...], ...] | 初始化游戲區(qū)域(世界地圖) |
| reset_world | 清除實(shí)際地圖,替換所有字符為空格 | |
| render | [x, y, char, color] | 在某個(gè)位置顯示字符 |
| p_joined | [id, name, color, score] | 新玩家加入游戲 |
| p_gameover | [id] | 某個(gè)玩家游戲結(jié)束 |
| p_score | [id, score] | 給某個(gè)玩家計(jì)分 |
| top_scores | [[name, score, color], ...] | 更新排行榜 |
典型的消息交換順序:
| 客戶端 -> 服務(wù)端 | 服務(wù)端 -> 客戶端 | 服務(wù)端 -> 所有客戶端 | 備注 |
| new_player | 名字傳遞給服務(wù)端 | ||
| handshake | 指定 ID | ||
| world | 初始化傳遞的世界地圖 | ||
| top_scores | 收到傳遞的排行榜 | ||
| join | 玩家按下“Join”,游戲循環(huán)開始 | ||
| reset_world | 命令客戶端清除游戲區(qū)域 | ||
| render, render, ... | 第一個(gè)游戲“滴答”,渲染第一幀 | ||
| (key code) | 玩家按下一個(gè)鍵 | ||
| render, render, ... | 渲染第二幀 | ||
| p_score | 蛇吃掉了一個(gè)數(shù)字 | ||
| render, render, ... | 渲染第三幀 | ||
| ... 重復(fù)若干幀 ... | |||
| p_gameover | 試著吃掉障礙物時(shí)蛇死掉了 | ||
| top_scores | 更新排行榜(如果需要更新的話) |
5. 總結(jié)
說實(shí)話,我十分享受 Python 最新的異步特性。新的語法做了改善,所以異步代碼很容易閱讀??梢悦黠@看出哪些調(diào)用是非阻塞的,什么時(shí)候發(fā)生 greenthread 的切換。所以現(xiàn)在我可以宣稱 Python 是異步編程的好工具。
SnakePit 在 7WebPages 團(tuán)隊(duì)中非常受歡迎。如果你在公司想休息一下,不要忘記給我們在 Twitter 或者 Facebook 留下反饋。
網(wǎng)站題目:使用Python和Asyncio編寫在線多人游戲(三)
網(wǎng)站路徑:http://www.dlmjj.cn/article/cdggped.html


咨詢
建站咨詢
