Rust和WebAssembly中的一款多人棋盘游戏

2020-06-27 16:06:14

Pont是Mindware Games的棋类游戏Qwirkle的在线实现,它是为我的父母写的,这样他们就可以在新冠肺炎呆在家里的时代和朋友和家人一起玩。

游戏被分成几个房间,用三个字的代码(上图中的喜怒无常的形状)来标识。在每个房间里,游戏分发棋子,执行游戏规则,并提供一个本地聊天窗口。

不同寻常的是,这是一个基于网络的多人游戏,没有任何Javascript:客户端和服务器都是用Rust编写的,它被编译成WebAssembly在浏览器上运行。(有一个Javascript填充程序来加载WebAssembly模块,但我不需要自己编写它)。

请记住,我不是一名Web开发人员,因此对于Web应用程序来说,这可能是一个奇怪的局外人架构。以下是该系统的外观:

系统对证书使用了let的加密:客户端和服务器之间的静态资产和WebSocket通信都是加密的。游戏服务器不能与Nginx代理安全通信,但如果有人在服务器上观看,我就会遇到更大的问题。

WASM捆绑包和PONT-SERVER可执行文件都是用Rust编写并在PONT存储库中管理的,它们都依赖于PONT-COMMON,它定义了游戏的基本类型和逻辑(例如,这样客户端和服务器都可以检查移动是否合法)。

客户端和服务器通过WebSocket进行通信,消息在PONT-COMMON中被强类型化为枚举,使用Serde序列化,然后打包成二进制WebSocket消息作为二进制WebSocket消息发送。

这两个大国之间正在进行一场非常礼貌的冷战(不相容?)。运行时,包只能在其中一个中工作

Futures、std::Future和Futures_util之间的一般混淆(Google搜索将使您进入文档的随机版本,这一事实加剧了这一情况)。

我最终使用了Smol运行时,因为它的依赖性相对较少,而且我意识到它不会试图拥有整个异步世界(不像Tokio和Async-STD)。

跨过这些障碍后,服务器架构相对简单,一组独立的任务异步运行,通过WebSocket与外部通信,内部通过无限的MPSC队列进行通信。

这里是一个服务器运行一个游戏(有两个玩家)的示例,外加一个刚刚连接的新客户端。每个矩形表示一个异步任务:

每个客户端连接一个异步任务,它在WebSocket连接和应用程序的内部队列之间传递消息。

这些任务每个都映射到一个Smol::Task。(作为一个小优化,第一个玩家的任务同时处理玩家通信和运行房间,这就是为什么它们在上图中都是蓝色的原因)。

服务器编译成5MB的静态二进制文件。整个系统托管在Digital Ocean提供的最小虚拟机上,这是一台每月5美元的机器。我期待着不可避免的黑客新闻DDOS,在那里我可以看到它的可扩展性!

客户端是2000行无框架的兴奋,它使用状态机模式来表示游戏的流程,接受来自服务器的消息,并相应地更新状态:例如,顶层状态从连接到创建或加入到玩。

游戏板被表示为一个SVG;其他一切都是标准的HTML元素。实际上,整个UI都是在index.htmland中按需预构建的。

客户有点精雕细琢:棋子在棋盘上移动时会有动画效果,还有一个可选的色盲模式,它会添加角落标记来指示颜色。

我使用直接的DOM操作(从web-sys机箱)来控制系统。这个练习让我领略了虚拟DOM的用处,但我不想引入框架的复杂性。(Svelte是否有Rust+wasm版本?)。

对于部署,我使用wasm-pack,并从与其他静态资产相同的服务器上提供生成的wasm blob。

客户端面临的主要挑战(当然)是处理跨浏览器兼容性:特别是Safari,它支持的功能较少,而且对触摸事件的处理也很时髦。

客户端仍然有点混乱,动画、UI输入和服务器事件都在争先恐后地破坏系统的不变量。例如,在动画运行时拖动板可能会使系统进入无效状态,这是一个令人讨厌的错误。

这并不令人惊讶:有状态UI很难,这解释了声明式方法的流行。在这一点上,客户端的功能是完整的,并且还没有完全崩溃,所以我不倾向于进行任何戏剧性的重构。

Rust作为一种语言仍然很棒,尽管有一些小问题。我已经在上面讨论了异步生态系统,我没有再深入讨论这一点。在客户端,WebAssembly经常存在阻抗不匹配的问题:例如,使用Rust闭包作为回调需要隐晦的样板文件。

尽管如此,所有的部分都就位了,进行更改的速度非常快,而且我相信编译器会检查我没有破坏任何东西。

我特别喜欢WebSockets、Serde和bincode的组合:让客户端和服务器都处理强类型的事件流,这让事情更容易推理。