多人對戰架構
En Parlant~ 的多人對戰功能使用 WebSocket 中繼伺服器即時連接兩位玩家。系統不採用點對點連線——所有通訊都經過中繼伺服器。這讓網路架構保持簡單,並能在防火牆和 NAT 環境下穩定運作。
Player A (host) Relay Server Player B (joiner) | | | |--- create_game(name) ----------->| | |<-- game_created(code) -----------| | | |<--- join_game(code, name) -----| |<-- game_joined(name) ------------|--- game_joined(name) -------->| | | | |--- game_move(uci, times) ------->|--- game_move(uci, times) --->| |<-- game_move(uci, times) --------|<-- game_move(uci, times) ----| | | | |--- heartbeat ------------------>|--- peer_heartbeat ----------->|中繼伺服器是一個輕量的轉發層。它在記憶體中維護房間,在每個房間的兩位玩家之間路由事件,並處理清理工作。伺服器端沒有任何棋局邏輯或走步驗證——客戶端擁有完全的權威性。
- 前端(客戶端):socket.io-client——標準的 Socket.IO JavaScript 客戶端
- 後端(中繼):socketioxide——基於 Axum 建構的 Rust Socket.IO 伺服器
- **協定:**Socket.IO over WebSocket(必要時自動降級為 HTTP 長輪詢)
選擇 Socket.IO 而非原生 WebSocket,是因為它內建提供了自動重連、房間/命名空間管理以及結構化事件處理等功能。
1. 建立遊戲
Section titled “1. 建立遊戲”主持人點擊 Multiplayer 並輸入顯示名稱。客戶端發送一個帶有名稱的 create_game 事件。伺服器會:
- 產生一組唯一的 6 字元房間代碼
- 建立房間並將主持人加入為第一位玩家
- 回傳
game_created(code)讓主持人可以分享代碼
2. 加入遊戲
Section titled “2. 加入遊戲”加入者輸入房間代碼和顯示名稱。客戶端發送 join_game(code, name)。伺服器會:
- 根據代碼查找房間
- 將加入者加入為第二位玩家
- 向雙方發送
game_joined(name)——每位玩家都會收到對方的顯示名稱
兩位玩家都進入房間後:
- **主持人一律執白,加入者一律執黑。**這由加入順序決定,而非協商。
- 走步以 UCI 格式發送(例如
e2e4),並附帶雙方的時鐘時間。 - 中繼伺服器將每個
game_move事件轉發給對手。雙方玩家也會收到自己的走步作為確認。
4. 遊戲結束事件
Section titled “4. 遊戲結束事件”多種事件處理遊戲結束的情況:
- 認輸——轉發給對手以觸發認輸對話框
- 和棋提議——轉發給對手,對手可以接受或忽略
- 接受和棋——轉發給雙方玩家以和棋結束比賽
遊戲結束後,任一玩家可以發送 ready 事件表示想要再戰。當雙方都發送了 ready 後,客戶端重置棋盤並交換顏色(或維持不變——由客戶端決定)。
房間代碼為 6 個字元,格式化為 XX-XX-XX 以便口頭分享時方便閱讀。字元集排除了視覺上容易混淆的字元:
- 不使用
0或O(數字零與字母 O) - 不使用
1、I或L(數字一、大寫 I 與大寫 L)
這避免了當玩家透過語音聊天唸出代碼,或從朋友的訊息中手動輸入時,出現「那是零還是 O?」的問題。
多人連線需要偵測玩家何時斷開連線——無論是網路中斷、關閉應用程式,還是筆電進入休眠。En Parlant~ 為此使用了心跳機制:
- 每個客戶端每 5 秒向伺服器發送一次
heartbeat事件 - 伺服器確認心跳並以
peer_heartbeat轉發給對手 - 客戶端追蹤上次從對手收到
peer_heartbeat的時間 isPeerAlive(timeoutMs)函式檢查對手的最後一次心跳是否在可接受的閾值範圍內
這驅動了使用者介面中的連線狀態指示器。如果心跳停止到達,玩家會看到對手可能已斷線,可以選擇等待或離開遊戲。
中繼伺服器會自動移除閒置的房間以防止記憶體洩漏:
- 房間在 30 分鐘無活動後被視為閒置
- 清理任務每 60 秒執行一次,清除所有超過閒置閾值的房間
- 當房間被清理時,所有剩餘的連線都會被中斷
系統沒有持久化儲存。如果伺服器重啟,所有房間都會消失。這是刻意為之——中繼伺服器是無狀態且短暫的。進行中的遊戲需要重新開始,但這在實際使用中很少發生。
預設的中繼伺服器運行在 Fly.io 上,提供低延遲的 WebSocket 連線及自動 TLS。請參閱多人對戰伺服器設定指南了解如何運行自己的中繼伺服器。
開發期間測試多人對戰功能:
-
複製並執行中繼伺服器:
Terminal window git clone https://github.com/DarrellThomas/en-parlant-relay.gitcd en-parlant-relaycargo run伺服器會在連接埠 3210 上啟動。
-
在 En Parlant~ 中,將中繼伺服器 URL 改為
ws://localhost:3210。 -
開啟兩個應用程式實例(或一個應用程式加一個開發模式)來模擬雙方玩家。
走步、心跳以及所有事件的運作方式與生產環境的中繼伺服器完全相同——唯一的差異是連線 URL。