資訊內容
用Scratch+Python做一個聯網游戲
Scratch2有一個擴展功能,可以讓Scratch和第三方應用通過http連接起來,實現訪問網站數據、控制硬件等應用。我們今天就使用這個擴展,連接到一個用Python實現的網絡服務,來做一個聯網游戲。希望大家能通過今天的學習了解一些網絡編程的基本知識,以及知道要做出一個比較復雜的系統要關心的方面。
我們先用Scratch做一個大富翁游戲:有兩個以上的游戲角色(這個例子里是一只貓和一只狗)分別由兩臺電腦控制。角色輪流滾動色子走動,走動的步數由骰子的點數隨機決定。走動途中會碰到不同的魔術指令可以改變走動的路徑、加分等等。誰先走到終點就算贏。游戲畫面如下所示:
系統結構圖
有兩個玩家情況下的系統架構圖如下所示:
我們這里說的系統架構圖是一個計算機系統的設計圖。描述的對象包括直接構成系統的各個硬件和軟件組件,以及各個組件之間的連接和通訊方法。
系統架構圖是構建計算機系統實踐的基礎,與建筑師在開始建設前必須完成的設計圖紙一樣重要。現代的大型計算機系統就如同一個復雜龐大的建筑,完成它需要多方面的計算機網絡、硬件和軟件配合以及涉及大量的知識。如果正式搭建之前沒有想好系統架構和設計細節,往往會導致事倍功半,甚至在項目后期才發現無法解決的問題而不得不推倒重來。
大家編程的過程中如果涉及多個組件(或對象),特別是如果涉及網絡的編程,在開始寫程序之前請務必通過畫出系統架構圖來理清各組件之間的關系。
架構圖等計算機設計圖表是用來輔助我們理解、維護計算機系統以及方便我們和其他部門溝通交流的,對于圖表的格式,不同的單位可能會有不同的要求,總的來說只要方便相關的人員理解就行。比較著名的一種格式叫做統一建模語言(Unified Modeling Language, UML),可以進一步學習了解。
上面的架構圖說明了在有兩個玩家的情況下,網絡里會有:
3個硬件:
-
一臺服務器
-
兩臺個人電腦
兩臺個人電腦實際上是可以直接連接起來實現聯網對戰的。為什么需要一臺服務器那么復雜呢?原因是:如果其中一個玩家沒開電腦,沒有服務器的話,另一個玩家需要找一個新玩家的話就不是那么容易了。就算找到了新玩家,兩臺電腦之間還需要重新設置網絡參數以實現聯網。這對于沒有多少電腦知識只想簡簡單單玩個游戲的人來說非常困難。而服務器一般都是7*24小時穩定提供服務的,在游戲開發時設置好服務器地址后,任意一個玩家打開游戲就可以連上同一個服務器,再通過服務器尋找其他在線的玩家一起玩了。
5個軟件組件:
-
服務器里的Python服務端(實現聯網游戲的協作和數據共享)
-
每臺電腦上的Python客戶端(負責和服務器連接,是服務器和Scratch游戲之間溝通的橋梁)
-
每臺電腦上的Scratch游戲端(負責展示游戲畫面)
上面的架構圖還說明了不同部件之間的連接協議。這里的協議可以理解為部件之間溝通的一種語言。Scratch游戲不能直接和Python語言寫的客戶端溝通;Python語言寫的客戶端和服務端分布在網絡的兩端,它們之間的溝通也不像在同一臺電腦上溝通那么簡單(比如不能直接訪問文件),這就需要根據需求選擇溝通協議。
Scratch的擴展規定了只能使用http協議,所以第三方應用必須要使用http協議才能和Scratch游戲溝通。用Python可以很容易地寫一個支持http協議的應用。
Python客戶端和服務端可以選擇的連接協議就非常之多了。我們沒有選擇最方便的http協議是因為http協議只能單向請求,即只能等客戶端向服務端發起請求之后服務端才能給客戶端發送信息,服務端不能主動隨時向客戶端發送信息。我們的聯網游戲需要頻繁地雙向溝通,這種協議并不是太適合。
我們選擇了websocket作為python客戶端和服務端的溝通協議。Websocket的連接方式和http一樣,只需要服務端定義好連接接口,不需要在客戶端進行額外的網絡設置。連接后客戶端和服務端之間可以隨時互相發送消息了。
網絡基本知識
1.??Http
Http(超文本傳輸協議)應該是大家日常接觸到的最多的互聯網技術。你用瀏覽器打開任意一個網頁都會在地址欄里看到http這四個字母。
http是用來在網絡間傳輸諸如網頁、數據等信息的應用層協議。它最初用于瀏覽器和服務器之間的通信,現在也廣泛用于各種網絡組件之間通信。不懂http就幾乎等于不懂網絡編程。http遵循經典的客戶端-服務端模型,客戶端打開一個連接發出http請求,然后等待服務器端的響應。
2.??地址
每個http服務都要綁定到一個地址以接收請求,比如百度網站的地址是http://www.baidu.com。也就是說百度網站的服務器地址是www.baidu.com。這種地址叫做域名。可以理解為一個人在網絡上注冊的名字。注意這個名字和我們人的名字有點不同的是,它在互聯網上是獨一無二的。
還有一種地址叫做IP地址,比如183.232.231.173。IP地址就像人的身份證號。它是互聯網上每臺設備的獨一無二的身份識別碼。每個域名都要映射到一個IP地址上才能使用。互聯網是先有IP地址再有域名的。由于IP地址太難記了,人們發明了域名來方便人們記住不同的網站。
要知道一個網站的IP地址很簡單,在Windows菜單欄里輸入‘CMD’打開命令提示符,然后輸入ping和空格再加上網站的域名就可以了。比如下面的命令找到了百度網站的IP地址就是183.232.231.173:
3.??端口
一臺擁有IP地址的服務器可以提供許多服務,比如網站服務、文件傳輸服務FTP、郵件服務SMTP等,這些服務完全可以通過1個IP地址來實現。那么,服務器是怎樣區分不同的網絡服務呢?顯然不能只靠IP地址,因為IP?地址與網絡服務的關系是一對多的關系。實際上是通過“IP地址+端口號”來區分不同的服務的。為了在一臺設備上可以運行多個程序,人為的設計了端口(Port)的概念,類似的例子是公司內部的分機號碼。
一個網絡設備可以有65536個端口,0-1024之間多被操作系統占用,所以實際編程時一般采用1024以后的端口號。
要訪問同一個網站的不同端口可以簡單地在地址后面加冒號(:)和端口號,如訪問百度的80端口:http://www.baidu.com:80
HTTP服務的默認端口是80和443,也就是說打開http://www.baidu.com實際上是打開http://www.baidu.com:80或http://www.baidu.com:443,只不過瀏覽器很聰明地幫我們把補充了80或443端口。
????4.? Websocket
HTTP的一個局限性是只能從客戶端向服務端發起請求,服務端不能主動向客戶端發去消息。在有些時候,如一個玩家完成了一個動作之后,我們希望另一個玩家可以馬上收到消息繼續進行下一個動作,這種時候就需要游戲服務器主動向客戶端發送消息了。Websocket協議是一個簡單的解決方法,它用起來和HTTP類似,也是需要客戶端和服務端。
Scratch擴展的溝通協議
Scratch擴展的溝通協議有專門的規范。
首先,需要建立一個“.s2e”描述文件來定義擴展積木。該文件是json格式的。然后把該文件導入到Scratch里面生成擴展積木。導入方法是在scratch里按住鍵盤“Shift”鍵然后點擊“文件”,然后選擇“導入實驗性HTTP擴展功能”,接著選擇你的“.s2e”文件就可以了。導入成功后在“更多積木”那里就能看到自定義的積木了。
下面是一個“.s2e”文件例子:
導入后會在“更多積木”類別那里看到:?
第一行:"Extension Example"就是顯示的擴展積木模塊名稱。
第二行:12345是http客戶端的端號。這個擴展積木會訪問本地電腦的12345 http端口來通信,即http://localhost:12345/
第三行開始定義不同的積木。
第四行定義了一個執行命令積木,它在scratch里面的顯示名稱是“partner moved”,當該積木被執行的時候會發送一個http請求到http://localhost:12345/partnerMoved地址里。最后一個參數就是該請求的地址。
第五行定義了一個設置命令參數,它會把“set character to_”里的“_”傳輸出去。如執行“set character to 3”積木就會發一個http請求到http://localhost:12345/setChar/3 地址去。
第六行是一個從http服務端取得當前參數返回值的命令。第一個參數“r”代表他是一個“報告”指令,它可以取得“getCharacter”的值。注意這里取值并不是直接發一條getCharacter請求到http服務端,而是從最近的一次http://localhost:12345/poll請求里取出getCharacter指令的值。關于“/poll”請求請看下面詳細說明:
/poll 請求
Scratch每秒鐘大概發30條“/poll”請求到http服務端,并從“/poll”請求的回應報文那里獲取最新的參數值。http服務端要通知Scratch的任何信息或結果都需要通過“/poll”回應報文來傳送。每一對返回參數和返回值占一行,兩對之間通過“/n”行結束符分隔。返回參數和返回值之間要有一個空格。如下面的回應報文:
getCharacter abc
age 12
它返回兩個參數:getCharacter的值是abc,age的值是12。getCharacter的值可以通過上面的第六行定義的“
”積木獲取。
/reset_all 請求
在Scratch里點擊結束的紅球時會觸發發一個“/reset_all”請求到http服務端。可以把一些游戲重置或停止時需要做的東西放到這個請求的處理模塊里。但是我的試驗里有時候點綠旗也會觸發“/reset_all”請求,但并不是每次都會。所以如果需要在游戲沒有結束(不點擊紅球)時做一些重置動作的話,保險起見最好自己定義一個“reset”積木。
Scratch擴展的一個關鍵點或者說限制條件是:Scratch只能通過poll請求從http服務端獲取信息,而不能通過其他請求或者指令。換一種說法,http服務端如果想要發消息給Scratch端,只能把消息內容放在poll請求的回復里面。
我們要做的聯網游戲需要用到的Scratch擴展溝通協議知識就是上面這些。接下來介紹一些聯網游戲的關鍵部分。不同模塊之間的溝通次序。我們用一種叫“順序圖”的圖表來描述這種關系。
順序圖
順序圖一般用于說明一個系統之間如何交互來實現一個使用場景,它體現的是系統不同組件之間如何按照時間順序互相配合來完成一個任務。它是一個二維圖,縱向是時間軸,時間沿豎線向下延伸。橫向代表了協作中各個組件。
我們來看一個查看可用角色功能的順序圖:
“可用角色”這一信息保存在服務器端,所以每個游戲的界面(Scratch端)都要通過連接Python客戶端再連到服務器端去取得這個信息。順序圖的頂端是3個組件的名稱。每個組件都有一條縱向的生命線。生命線間用有方向的連線表示不同組件的協作。實線表示一個組件向另一個組件發起一個請求,箭頭方向表示請求的方向;虛線表示請求的結果返回。從上到下表示時間順序。上圖的意思是以下步驟按時間先后順序進行:
1.?? Scratch游戲程序向Python客戶端(在同一臺電腦上通過http協議)發起一個checkAvailChar(檢查可用角色)的請求。
2.?? Scratch游戲程序執行等待1秒鐘的指令。我們這里預期1秒內從網絡中得到可用角色的返回值。
3.?? Python客戶端(在網絡上通過websocket協議)向服務器端發起一個checkAvailChar(檢查可用角色)的請求。這一步驟是和步驟2同步進行的,我們沒辦法控制2和3的先后次序。
4.?? Python服務器端向客戶端返回步驟3的請求結果 - 可用角色。
5.?? Scratch游戲程序向Python客戶端發出poll請求。上面介紹過, 事實上Scratch游戲程序大概每秒鐘發出30條poll請求。只不過 Python客戶端只有在服務器端返回可用角色信息后才會把正確的值通過poll返回給Scratch游戲,在這之前Scratch游戲都只能取得一個不正確的值。
6.?? Python客戶端向Scratch端返回步驟5的請求結果 - 可用角色。只有客戶端已經從服務器端取得正確的可用角色值后,它才能向Scratch端返回正確的結果。否則就返回默認值。
7.?? Scratch游戲程序向Python客戶端發起一個setChar(設置角色)的請求。這個步驟是在‘步驟2 – 等待1秒’之后才會進行的。并且圖里省略了Scratch游戲讓玩家選擇角色的步驟,因為這個流程圖重點在于描述不同系統組件間的協作。
上面是一個簡單的順序圖介紹。順序圖對于編程人員非常重要,一個復雜的涉及多個組件的系統直接看程序的話通常很難理解。我寫這個小聯網游戲的過程中就有感覺,如果不看順序圖的話,經常是昨天寫的程序今天已經看得非常吃力了。同一個作者尚且如此,如果是不同部門、不同領域用不同語言寫的程序看起來就更加是天書一樣了。順序圖就提供了一種通用、簡單、直白的語言來描述不同組件之間的聯系。這也是UML的核心作用或者說目標 – 提供一種統一的設計溝通語言。
畫順序圖還有一個好處就是它讓人可以更早地在寫程序之前真正深入地理清核心系統的邏輯關系,可以盡早地發現一些比較嚴重的邏輯錯誤。計算機系統開發有一個重要規律是問題越早發現修改起來的需要的資源(費用)就越低。一個簡單的例子:一個設計錯誤如果在測試階段才發現的話往往要花費很多部門(設計、開發、測試)的精力才能修復;如果設計階段已經發現了就沒有開發、測試部門的重復勞動了。
順序圖可以按不同的功能分開來畫,比如上面的圖只包括了檢查可用角色的功能。并且它不需要包括系統的所有邏輯順序,一些顯而易見的只是系統一個小組件內部的步驟就不需要畫出來了。當然這個要靠團隊特別是設計師的經驗和能力來判斷了。對于初學者,所有涉及不同組件(比如分布在不同電腦的程序、或者使用不同語言寫的程序)的交互部分應該都畫出順序圖來。
畫順序圖包括其他設計圖的大原則是方便交流和理解系統就行,并不需要花大量時間畫得很精美。它并不是最終用戶看到的東西。
Python http 服務器
我們用python自帶的HTTPServer類來做一個簡單的可以接收http請求的服務器。這里的http服務器是位于Scratch和真正物理服務器中間的Python客戶端,即下面架構圖中的紅色框部分:
-
寫一個SimpleHTTPRequestHandler類來繼承python的BaseHTTPRequestHandler類:
from http.server import BaseHTTPRequestHandler
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): -
SimpleHTTPRequestHandler重寫父類BaseHTTPRequestHandler的do_GET方法來處理http請求:
def do_GET(self): -
do_GET方法根據請求地址的不同來返回不同的結果,比如收到‘/poll’請求就返回“hello“:
def do_GET(self):
??????? ifself.path == '/poll':
??????? ????self.send_response(200)
??????? ????self.end_headers()
? ? ? ? ? ? self.wfile.write('hello'.encode('UTF-8')) -
導入HTTPServer:
from http.server import HTTPServer -
實列化HTTPServer,它的地址是'localhost',端口是12345。'localhost'是一個特殊的地址,指向本地即自己,因為我們的scratch游戲和這個http服務器是放在同一臺電腦上的。
HTTPServer使用我們自己寫的SimpleHTTPRequestHandler來處理請求:
httpd = HTTPServer(('localhost', 12345), SimpleHTTPRequestHandler) -
讓這個服務器一直運行:
httpd.serve_forever() -
現在簡單的http服務器已經起來了。可以用瀏覽器測試打開http://localhost:12345/poll?,瀏覽器會顯示“hello”就表示成功了。
接下來我們就可以根據“Scratch擴展的溝通協議”來具體實現scratch游戲的python http 客戶端。記得把Scratch擴展配置文件 - “.s2e”文件里的“extensionPort”端口配成http服務器的端口,就可以實現Scratch和http服務器的通信了。?
更多關于python http服務的內容請參考:
https://docs.python.org/3/library/http.server.html
Websocket 客戶端和服務端
我們使用了Aymeric Augustin的基于python asyncio的websocket實現。它使得使用websocket異常簡單。下面是一個簡單的websocket客戶端,它連接到位于本機即“localhost”8765端口的websocket服務端,向它發送一句“Hello server”字符串:
import?asyncio
import?websockets
async?def?hello(uri):
??? async?with websockets.connect(uri) as websocket:
??????? await websocket.send("Hello server")
asyncio.get_event_loop().run_until_complete(
??? hello('ws://localhost:8765'))
下面是websocket服務端,它使用8765端口,當接到客戶端的消息后就發送一句“Hello client”字符串:
import asyncioimport websockets?async def echo(websocket, path):??? async for message in websocket:??????? await websocket.send("Hello client")?asyncio.get_event_loop().run_until_complete(??? websockets.serve(echo,'localhost',8765))asyncio.get_event_loop().run_forever()在我們的聯網游戲例子里,websocket客戶端就是Python客戶端,即下圖中紅框部分;而websocket服務端就是Python服務端,即下圖中藍框部分:
? ? ??
聲明:本文章由網友投稿作為教育分享用途,如有侵權原作者可通過郵件及時和我們聯系刪除
