我正在使用 Go 服务器、WebSocket 和 MessagePack 运行一个小型浏览器 MMO。我写了它的模拟、网络更新和客户端渲染如何配合的,因为 ping 数值只覆盖了部分等待。

服务器每 50 ms(20 Hz)更新一次世界,而常规的共享状态快照则每 250 ms(4 Hz)发送一次。战斗事件拥有独立的 50 ms 批量间隔。模拟和发送状态是独立的调度:降低快照率可以节省流量,但也会延长权威状态更新到达其他客户端的时间。

一个简化的时间示例:

0 ms: 客户端发送 world:goal,请求向 x = 200 移动
50 ms: 请求到达,刚好错过处理机会
100 ms: 下一个模拟帧处理它
250 ms: 计划中的快照包含当前 x = 120
300 ms: 快照到达客户端

这假设双向各 50 ms,且请求错过了一个帧。300 ms 只是对时钟对齐的示例,不是实际平均值。角色在快照时仍在向 200 移动,因此其当前位置仅为 120。

本地玩家不必等待该响应才能看到移动。客户端预测可以立即显示自己的输入,随后进行校正。对于远程实体,插值会连接已接收的位置,通常会有显示延迟。短暂的外推可以填补间隙,但需要设置上限并在服务器不同意时进行修正。这些技术提升呈现效果,而不降低网络延迟本身。

另一部分是出站流量。游戏每月约使用 5 TB,主要是服务器到玩家的流量。CPU 和内存不是唯一需要在选择托管时考虑的因素。

为了进行一种刻意简化的广播计算,假设每位玩家 100 bytes 的状态,并每 4 秒发送给同一区域的所有人:

10 位玩家 × 10 个接收者 × 100 bytes × 4 = 40,000 bytes/s
100 位玩家 × 100 个接收者 × 100 bytes × 4 = 4,000,000 bytes/s

在上述假设下,玩家数量翻 10 倍,出站字节数翻 100 倍。它不包括压缩和协议开销;将玩家分区或使用兴趣管理会改变结果。只序列化一次并不消除向多个连接发送结果的成本。

我的实现中有一项小的优化是将怪物的显示元数据与循环的位置/血量状态分离。客户端不需要每次移动时都重复怪物的名称和外观。元数据在必要时重新发送。这不是增量复制:循环快照仍然包含所在区域的怪物状态列表。

我自学成才,因此这只是一个实现示例,而非适用于所有多人游戏的推荐设置。部分手动移动在客户端计算并由服务器验证;战斗结果仍由服务器负责。

对于运行类似游戏的开发者,你们是如何平衡快照频率、插值延迟和出站流量的?我也很想对说明进行修正。

我开发 MODAK Online;插图版在我的博客上:[https://lim.sh/mmorpg-lag-and-server-cost](英文在语言菜单中可选)。