我有一个1v1坦克游戏,整个游戏运行在一个HTML文件中。最近,我添加了在线多人游戏。点击一个按钮,会将你匹配到其他人。匹配系统使用Cloudflare Durable Objects。全球队列将两个玩家配对并将相同的世界种子传递给两台客户端,让他们在相同的地图上建造,然后浏览器每秒25次通过一个中继进行状态交换。这个部分在第一次部署时就成功了。两个陌生人,真实网络,坦克在同步。 我期待这将是最难的部分。然而并非如此。窗口是竞技场。 canvas.width = window.innerWidth。我的台式机是1920x1080的场地,我的手机是390x844,两台客户端都在发送彼此的坐标。坐标(400,300)是两个不同的地方。每个网码指南都警告这个问题。然而我并没有阅读其中任何一个。一个isMobile分支摧毁了一个共享的种子。两台客户端获得了相同的数字并建造了不同的地图。障碍生成器中的一行代码:const target = isMobile ? 7+rnd(4) : 8+rnd(6)。它改变了障碍的数量,所以两台客户端绘制了不同的随机数,之后的所有内容都被偏移。种子是正确的但却无用。一个早期返回杀死了三个东西。客人在updateObstacles()函数中提前返回了一行,紧接着是gameFrame++,这是一个共享的动画时钟,而不是障碍逻辑。客人障碍永远不会旋转。帧驱动的颜色会冻结。场地只有在接收到快照时才会移动,快照被读取为跳过在手机上。三个症状,一个行。 我修复了一个传送门,写了一个不同的传送门。坦克在竞技场的边缘会绕行。一个简单的lerp会将远程坦克拉回地图的另一侧,所以我写了一个 %W aware 的插值。坦克生活在 -10..W+10,而不是 0..W。我的取模函数将合法的位置拉回了内部。这个游戏已经有一个 wrappedDelta 辅助函数。AI 对手已经理解了绕行的概念几个月了。我的网码并没有使用它。一个尾随的 // 删除了一行代码。我的编辑目标是前缀的一行代码,而我的替换是注释。剩下的那行代码 MP.searchT0 = Date.now(); 在注释之后出现并消失了。计时器计算 15 - (Date.now()-0)/1000,约为负 1.78 亿,立即过期,所有匹配都将两名玩家dump到CPU上。语法检查通过。测试套件通过。这个功能已经死了。我的测试通过了所有内容。两台客户端都是同一台机器上的 stub。一个测试框架无法发现只存在于两个设备之间的 bug。两台机器无法独立做出决定。这个就是整个教训。每个 bug 都是客户端假设另一个客户端不共享的值:其大小,时钟,RNG 流,赢家。代码的标志是将一个值当作局部数字处理,而实际上它是共享状态。 我指示一个 AI 编写代码。设计和测试是我的。它以完全自信的态度写出了上述所有 bug,我通过查看两屏幕找到了所有 bug。所有上述内容都是可检查的。它是一个未压缩的HTML文件,所以查看源代码就能看到整个网码层:种子生成器, %W 错误及其修复,快照校正,竞技场边缘。没有构建步骤,没有压缩,没有隐藏。如果我错了,我希望听到。https://www.hammeranvilbrew.com/vibetanks 我仍然无法测试的唯一一件事是它是否在我不拥有的两个设备之间保持有效。如果你有一个手机和一个笔记本电脑,就可以在搜索屏幕上获得一个共享链接,直接将第二个玩家送入你的匹配中。