这是JavaScript多人游戏开发系列的第二部分。 它从第一部分开始的地方继续,描述了我们在开发过程中面临的挑战以及我们如何解决这些挑战。 生成的游戏称为spaaace ,您可以在线玩游戏,欧洲有一个游戏服务器,而美国有一个服务器。 物理和网络代码被拆分成一个单独的名为Lance.gg的开源项目。
如果您分叉,我也不会生气。 该代码在此处可用。

客户端预测
多人游戏的一个问题是,玩家输入需要一些时间才能从客户端发送到服务器,进行注册,影响游戏状态并将结果发送回客户端。 因此,如果鲍勃将车向左转,他希望能立即在屏幕上看到结果,而不是一秒钟后的三分之一。 这是客户端预测的起点–它尝试为客户端预测一旦输入到达服务器后游戏状态将发生什么。 如果从服务器收到的更新与客户的预期相抵触,则客户现在必须进行必要的更正以解决这些差异。 在一个简单的示例中,玩家Bob将车向左转,由于网络延迟,玩家Alice对此一无所知。 在爱丽丝的屏幕上,她将看到鲍勃的车仍在按客户的预测前进。
客户端预测必须执行以下操作:
- 通常,假设对象以当前速度继续沿其当前方向移动。 该计算包括应用相对于时间(即速度)的预期位置变化以及应用相对于时间(即角速度)的预期方位变化。
- 接下来,当新信息从服务器到达时,客户端必须回滚时间,并在更改的确切时间处理更改的参数。
- 接下来,客户端现在重新制定缺少的步骤以达到当前时间。
- 最后,某些对象将具有新的位置,但是我们不能简单地将它们传送到这些新位置,这会导致对象以不稳定的方式移动。 因此,我们必须使对象的属性逐渐向其正确位置弯曲。 该弯曲必须在多个步骤上逐步完成。 还必须与每个渲染事件所经过的确切时间成比例地完成它。
挑战3:阴影物体。
症状:“我发射了导弹,但没有立即显示出来”
客户预测通常被解释为一种推断对象位置的方法。 但实际上,游戏中还会发生很多其他事情,这是游戏预测不可避免的必要条件。 您甚至可能需要预测对象将在服务器上创建!
在我们的案例中,部分预测涉及发射新导弹。 如果玩家按下“开火”按钮-玩家等待200毫秒直到服务器创建的导弹出现,这是不可接受的。 为了解决这个问题,在检测到“火力”输入后,客户必须预测服务器上将创建新的导弹。 为此,我们定义了阴影对象的概念。 影子对象是在客户端上创建的临时对象,并且很快也会在服务器上存在。 一旦服务器副本到达以后的广播中,它就必须成为确定的对象,替换临时客户端对象。
此功能的详细信息要求我们创建一个单独的对象ID范围,该范围对于每个客户端都是私有的。 我们还需要一种机制来将阴影与相应的真实对象相关联。 我们通过用引起它的输入ID标记每个对象来完成此操作。

挑战4:游戏循环。
症状:“每次我发射导弹,飞船都会怪异地跳跃”
游戏以一系列“游戏步骤”执行。执行每个步骤的循环称为“ 游戏循环” 。 在每个游戏步骤结束时,已经应用了游戏规则-这包括物体的移动,导弹的射击或用户输入的应用。 将此与“ 渲染循环 ”( Render Loop)进行对比,在“ 渲染循环”中 , 循环的每个步骤都涉及与图形卡进行通信并在用户的屏幕上绘制当前的游戏图像,绿色怪物以及所有物体。
游戏循环在服务器和每个客户端上独立执行。 但是,服务器仅以一定间隔发送同步广播,例如,每6个服务器游戏步骤。 也许我们应该将此称为“ 客户端更新循环” 。
即使游戏循环和渲染循环都在客户端上以60Hz运行,也无法真正保证它们以恒定的相位差运行。 没那么运气。 不幸的是,渲染器需要基于当前时间访问插值位置。 在服务器上和客户端上,可以相对精确的方式在游戏引擎上以60Hz(即每秒60次)运行这两个循环。 渲染循环不依赖于游戏循环,只要它可以在需要时获取对象坐标即可。
但是,有些原因需要临时调整循环间隔。 例如,如果服务器和客户端之间甚至发生了很小的偏移,或者服务器由于重负载而减慢了速度,或者以更平滑的方式处理网络高峰。
按下时有抖动
我们注意到,以poltergeist的形式出现的一个具体问题是每次在Android设备上发射导弹时都会发生怪异的跳跃。 查看这些痕迹,我们发现游戏循环没有执行150毫秒。 那是10个完整的游戏步骤,根本没有发生。
原因? 使用排定的超时注册的功能(在JavaScript中为setTimeout()
)将不会在Android设备上运行的Chrome的触摸(按下)状态的前150毫秒内运行 。 谁知道? 另一种选择是使用浏览器同样不可靠的渲染事件(在JavaScript中,这是requestAnimationFrame()
)安排游戏步骤。
由于setTimeout()
和requestAnimationFrame()
在定义上都不可靠,但是在不同情况下,我们决定运行两个循环,使这两个循环都能够执行游戏步骤。 此外,我们添加了漂移调整因子,当客户端运行速度过快或过慢时,可以根据需要延迟或匆忙执行下一个客户端游戏步骤。

挑战5:事件泄漏。
症状:“服务器运行了一段时间后,新客户端无法连接”
对于这个问题,我们有些幸运,但我不确定为什么,但是我决定查看服务器的CPU使用率。 这是一个很大的预感。 可以肯定的是,在性能较高的服务器上大约一小时后,在性能较弱的服务器上经过15分钟后,CPU使用率会上升。
用于JavaScript编程的布朗尼蛋糕之一是调试工具(又名Chrome开发者工具),对此说法很少有人反对。 几秒钟后,我启动了节点检查器,并使用Chrome将其连接到该检查器,并准备捕获性能跟踪。
查看跟踪数据时,我注意到事件-发射器代码占用了大量CPU,这没有任何意义。 深入研究,我发现了一个事件处理程序泄漏-处理程序是在创建新船时注册的,但从未清除。 我们很惊讶地发现,这样的错误可能会对游戏产生如此大的影响,但是事实确实如此。
从中学到的教训是,多人游戏可以长时间运行,因此在这种情况下,您需要密切注意所有类型的漏洞,并在可能的情况下维护不会随时间变化的资源计数器。
挑战6:序列化泄漏。
症状:“当服务器运行了一段时间后,新的连接会在游戏开始前的几秒钟内看到数百枚导弹”
同样,我们将看到一个带有意外后果的错误。 通常,根据症状尝试猜测问题的根本原因非常棘手。 也许随着时间的流逝,人们在这方面会发展出更好的直觉,但是我的印象并不十分深刻。
在这种情况下,我们需要通过添加新的跟踪点来隔离问题,直到将问题缩小到服务器上对象的序列化为止。 序列化是获取深层JavaScript对象并将其打包为可通过网络发送的数据帧的过程。 在设定的时间间隔内,服务器对游戏对象进行序列化并将其广播给所有客户端。 服务器仅需要发送自上次广播以来已更改的对象。 客户端接收这些帧并执行逆过程(反序列化)以获取新的对象状态。
我们的错误是序列化事件队列在没有客户端连接的时间内没有重置“原子”事件(例如对象创建事件)本身。 听起来像是一个大嘴巴,但在大多数JavaScript情况下,此修复方法是两层的。
在本文中,我们介绍了四个令人不快的意外,错误……好吧,错误。 第3部分将讨论多人游戏的一般体系结构,并为有抱负的多人游戏开发人员提供一些指导。
下一页:第3部分-轮到您编写多人游戏了。