跳到內容

多人對戰架構

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,是因為它內建提供了自動重連、房間/命名空間管理以及結構化事件處理等功能。

主持人點擊 Multiplayer 並輸入顯示名稱。客戶端發送一個帶有名稱的 create_game 事件。伺服器會:

  1. 產生一組唯一的 6 字元房間代碼
  2. 建立房間並將主持人加入為第一位玩家
  3. 回傳 game_created(code) 讓主持人可以分享代碼

加入者輸入房間代碼和顯示名稱。客戶端發送 join_game(code, name)。伺服器會:

  1. 根據代碼查找房間
  2. 將加入者加入為第二位玩家
  3. 向雙方發送 game_joined(name)——每位玩家都會收到對方的顯示名稱

兩位玩家都進入房間後:

  • **主持人一律執白,加入者一律執黑。**這由加入順序決定,而非協商。
  • 走步以 UCI 格式發送(例如 e2e4),並附帶雙方的時鐘時間。
  • 中繼伺服器將每個 game_move 事件轉發給對手。雙方玩家也會收到自己的走步作為確認。

多種事件處理遊戲結束的情況:

  • 認輸——轉發給對手以觸發認輸對話框
  • 和棋提議——轉發給對手,對手可以接受或忽略
  • 接受和棋——轉發給雙方玩家以和棋結束比賽

遊戲結束後,任一玩家可以發送 ready 事件表示想要再戰。當雙方都發送了 ready 後,客戶端重置棋盤並交換顏色(或維持不變——由客戶端決定)。

房間代碼為 6 個字元,格式化為 XX-XX-XX 以便口頭分享時方便閱讀。字元集排除了視覺上容易混淆的字元:

  • 不使用 0O(數字零與字母 O)
  • 不使用 1IL(數字一、大寫 I 與大寫 L)

這避免了當玩家透過語音聊天唸出代碼,或從朋友的訊息中手動輸入時,出現「那是零還是 O?」的問題。

多人連線需要偵測玩家何時斷開連線——無論是網路中斷、關閉應用程式,還是筆電進入休眠。En Parlant~ 為此使用了心跳機制:

  1. 每個客戶端每 5 秒向伺服器發送一次 heartbeat 事件
  2. 伺服器確認心跳並以 peer_heartbeat 轉發給對手
  3. 客戶端追蹤上次從對手收到 peer_heartbeat 的時間
  4. isPeerAlive(timeoutMs) 函式檢查對手的最後一次心跳是否在可接受的閾值範圍內

這驅動了使用者介面中的連線狀態指示器。如果心跳停止到達,玩家會看到對手可能已斷線,可以選擇等待或離開遊戲。

中繼伺服器會自動移除閒置的房間以防止記憶體洩漏:

  • 房間在 30 分鐘無活動後被視為閒置
  • 清理任務每 60 秒執行一次,清除所有超過閒置閾值的房間
  • 當房間被清理時,所有剩餘的連線都會被中斷

系統沒有持久化儲存。如果伺服器重啟,所有房間都會消失。這是刻意為之——中繼伺服器是無狀態且短暫的。進行中的遊戲需要重新開始,但這在實際使用中很少發生。

預設的中繼伺服器運行在 Fly.io 上,提供低延遲的 WebSocket 連線及自動 TLS。請參閱多人對戰伺服器設定指南了解如何運行自己的中繼伺服器。

開發期間測試多人對戰功能:

  1. 複製並執行中繼伺服器:

    Terminal window
    git clone https://github.com/DarrellThomas/en-parlant-relay.git
    cd en-parlant-relay
    cargo run

    伺服器會在連接埠 3210 上啟動。

  2. 在 En Parlant~ 中,將中繼伺服器 URL 改為 ws://localhost:3210

  3. 開啟兩個應用程式實例(或一個應用程式加一個開發模式)來模擬雙方玩家。

走步、心跳以及所有事件的運作方式與生產環境的中繼伺服器完全相同——唯一的差異是連線 URL。