多人游戏架构
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。