概括
在系列的第1部分中,我们讨论了以下内容:
- 多人游戏面临的挑战
- 如何通过客户端预测解决无响应的UI
但是,我确实忽略了基本的服务器实现细节,我们将在本文中重点介绍这些细节。
免责声明:我不是专业的游戏开发人员,所分享的大部分知识都是基于我的阅读经验和我的小型业余爱好项目的经验。 本文的主要目的是为多人游戏中的网络提供一个易于理解的介绍。
服务器的作用是什么?
让我们从定义服务器应该做什么开始,通常服务器应该充当
a)玩家的连接点
在多人游戏中,玩家需要访问一个公共端点才能互相访问,这是服务器程序的角色之一,即使在P2P通信模型中,也存在一个连接点,玩家可以在此之前交换其网络信息。可以建立P2P连接。
b)处理单元
在许多情况下,服务器运行游戏模拟代码,处理来自玩家的所有输入并更新游戏状态。 请注意,情况并非总是如此,某些现代游戏将大量处理任务转移给客户端。 在本文中,我们将假定玩家负责处理游戏,即。 使游戏打勾。
c)游戏状态的单一真相来源
在许多多人游戏中,服务器程序还具有游戏状态权限,主要是为了防止作弊,并且更容易推断何时只有一个点来获取正确的游戏状态。
天真的服务器实施
让我们开始以最简单的方式实现服务器,然后从那里进行改进。
游戏服务器的核心是一个循环,该循环不断使用玩家的输入(通常称为TICK)来更新GameState,其功能签名如下:
(STATEn,INPUTn)=> STATEn + 1
简化的服务器代码段如下所示
服务器处理每个TICK
来自缓冲区的所有输入的事实,这意味着GameState将取决于网络延迟。 下图说明了为什么这是一个问题
该图显示了2个客户端向服务器发送输入,我们观察到2个有趣的事实。
- 请求从不同的客户端到服务器花费的时间不同,从客户端A到服务器花费1个时间单位,从客户端B到服务器花费1.5个时间单位
- 从同一客户端到服务器的请求花费了不同的时间,第一个请求花费了1个单位时间,第二个请求花费了2个单位时间。
简而言之,即使在同一连接上,延迟也是不一致的。
不一致的延迟和Greedy Game Loop
给出了几个问题,让我们进一步研究。
客户端预测将不起作用
如果我们无法预测服务器何时会收到输入(由于延迟),那么我们就无法进行任何高精度的预测。 (忘记了客户端预测的工作原理?请在此处阅读)
低延迟玩家获得优势
如果输入需要更短的时间到达服务器,则将更快地处理输入,这对于具有快速网络的播放器会产生不公平的优势。 例如。 两个玩家同时射击,应该同时杀死对方,但是玩家B的等待时间较短,因此在处理玩家A的命令之前杀死了玩家A。
我们在上一篇文章中讨论了一个简单的解决方案,用于缓解不一致的延迟,即锁步状态更新。 这个想法是服务器直到收到所有玩家的输入后才继续运行,它有两个好处:
- 它不需要客户端预测
- 所有玩家的延迟似乎都与最慢的玩家相同,从而消除了我们提到的优势
但是,由于响应速度慢,因此不适用于快节奏的动作游戏。 (更多详细信息可以在上一篇文章中找到,因此在此不再赘述。)
下一节,我们将讨论如何使服务器端在快节奏的游戏中发挥作用。
服务器对帐
为了解决客户端预测不准确的问题,我们需要使客户端与服务器之间的交互从客户端的角度更可预测。 当播放器在客户端按下键时,客户端程序需要知道何时在服务器端处理此输入。
一种可能的方法是让客户端建议何时应应用输入 ,这样客户端可以可靠地进行预测。 “建议”一词用作服务器,如果该建议无效,则可能会拒绝该建议,例如,在您的魔力为空时尝试施放魔法。
输入应在用户输入后即刻应用。 Tinput + X ,其中X是延迟。 确切的值取决于游戏,通常小于100毫秒即可响应。 注意X也可以是零,在这种情况下,它应该在用户提供输入后立即发生。
假设我们选择X = 30ms,这将以30fps(每秒帧)的速度转换为大约1帧,并且输入要花费150ms才能传输到服务器,当输入到达服务器时,输入的目标帧已经通过的机会很大。
如图所示,用户A在T处按了一个键,该键本应在T + 30ms处处理,但是由于等待时间已超过T + 30ms ,服务器在T + 150ms处接收到输入。 这是我们在本节中要解决的问题
服务器如何应用过去应该发生的输入?
这个概念
您可能还记得,由于缺少对手的信息,客户端预测也存在类似的错误预测问题,并且稍后会通过使用Reconcilation从服务器进行状态更新来纠正错误预测。 这里可以使用相同的技术,唯一的区别是我们正在使用来自客户端的用户输入来纠正服务器上的GameState。
所有用户输入都需要标记时间戳,然后将使用该时间戳来告诉服务器何时处理此输入。
注意:在第一条虚线上,客户端是时间X ,服务器端是时间Y ,这是多人游戏(和许多其他分布式系统)的有趣特性,因为客户端和服务器独立运行,客户端和服务器的时间服务器通常会有所不同,我们的算法会处理不同。
上图显示了1个客户端与服务器之间的交互,
- 客户端发送带有时间戳的输入,通知服务器客户端A的该输入应在时间X发生。
- 服务器在时间Y收到了请求,为便于讨论,我们假设时间X比时间Y早。 在开发算法时,我们不应该假设时间Y大于或小于时间X,这将为我们提供更大的灵活性。
- 红色框是进行对帐的地方,服务器需要将输入X应用于最新的游戏状态,以便看起来输入X发生在时间X上。
- 服务器的GameState还包括时间戳,这是服务器端和客户端端协调所必需的。
对帐详细信息(红色框)
服务器需要维护
- GameStateHistory —时间段P内GameState的历史记录,例如。 从第二秒开始所有GameState
- ProcessedUserInput —在时间范围P内处理的UserInput的历史记录。 与GameStateHistory的时间范围相同的值
- UnprocessedUserInput —在时间段P内收到但尚未处理的UserInput
服务器收到用户的输入后,应将其插入UnprocessedUserInput中 。
接下来,当服务器打勾时,
- 检查UnprocessedUserInput中是否有任何用户输入早于当前帧
- 如果不是,那么您就很好,只需使用最新的GameState和相应的Inputs(如果有)运行游戏逻辑,并广播给客户端。
- 如果是,则表示由于缺少信息,先前生成的某些游戏状态是错误的,我们需要对其进行更正
- 首先,我们需要找到最早的未处理用户输入,假设它在时间N上(提示:如果对UnprocessedUserInput进行排序,则此操作很快)。
- 然后我们需要从GameStateHistory获取时间N上的对应GameState,并从ProcessedUserInput获取时间N上的已处理用户输入
- 使用这3个数据,我们可以创建一个更准确的新GameState。
- 然后将未处理的输入N移到ProcessedUserInput,以便将来我们可以使用它进行对帐。
- 在GameStateHistory中更新GameState N
- 对
N+1, N+2 ...
重复步骤4至7,直到获得最新的GameState。 - 服务器将最新帧发送给所有播放器。
讨论区
服务器端对帐遇到与客户端对帐类似的问题,当我们进行对帐时,这意味着我们做错了事,并且我们通过更改历史记录来进行更正。 这意味着我们不能应用不可逆转的结果,即杀死一个玩家,这种不可逆转的结果仅在它脱离GameStateHistory时才适用。 当无法再重写时。
此外,错误的GameState有时会导致可怕的UI跳转。 下图说明了它是如何发生的
实体从左上角开始,向右移动,之后经过5个滴答声,然后向右移动,但是服务器收到用户输入,说实体在标记N上改变了方向,因此服务器调和了游戏状态,并且现在实体突然跳到画布的左下方。
我可能会夸大效果,有时实体不会移动那么多,因此跳跃的影响不那么明显,但在许多情况下仍然很明显。 我们可以通过更改GameStateHistory,UnprocessedUserInput和ProcessedUserInput的大小来控制跳转,缓冲区大小越小,跳转就越少,这是因为我们对例如迟到的输入的容忍度较小。 如果忽略了迟于100毫秒的输入,则ping> 200毫秒的玩家将无法玩游戏。
我们可以用 网络等待时间容忍度 来换取更 准确的游戏状态更新 ,反之亦然。
克服游戏状态不正确问题的一种流行技术是实体 插值 ,其思想是通过在短时间内散布跳跃来平滑跳跃。
我不会在本文中包括实体插值的实现细节,但是将在文章底部提供一些参考。
包起来
我们已经讨论了客户端和服务器在多人游戏中如何工作。
通常,多人游戏具有3个松耦合循环, 服务器游戏循环 , 客户端预测循环和客户端UI渲染循环 。 通过在它们之间具有某种缓冲,它们的执行可以分离,从而为我们提供了创建更好游戏体验的灵活性。
结论
到这里结束我有关多人游戏的文章,我从该领域的专家那里学到了很多知识,构建一个简单的多人游戏也有很大帮助。 我仅展示了一种实现多人游戏服务器的方法,还有更多其他方法,具体取决于您要构建的游戏类型,我鼓励您通过构建一个简单的游戏来探索其中的一些想法。
感谢您的阅读,黑客入侵愉快!
-青薇
参考资料和进一步阅读
- [实体插值]-http://www.gabrielgambetta.com/fpm3.html
- [实体插值]-http://gafferongames.com/networked-physics/snapshots-and-interpolation/
- [滞后补偿] — https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking#Lag_compensation