0 常识

1
在一款单机游戏中,玩家只需和本地游戏里面的元素(如AI,NPC等)进行实时交互即可。

2
而在多人在线网络游戏中,玩家还需要同联网的其他玩家进行互动,在本地看到的其他玩家,本质上,都是其他玩家的镜像。我们知道,其他玩家的行为,通过网络传输过来,是有延时的。这和真实世界不一样,在真实世界里看到某个人作出某个动作,从他做出动作到被你看到,这个时间是可以忽略不计的(距离/光速)。而在网络上传输的时延就大多了,特别是对于一个在南半球的玩家A,和另一个在北半球B的玩家来说,他们的传输距离很可能大于地球直径(6400km),如果传输按照最快的光速来计算,那么从A到B的最小时延也是6400km/300000km*1s = 0.021s = 21ms,再加上经过每一个hop时,路由器计算转发所需的时间,延时可能会达100ms,数据一个来回大概200ms。

3
如何处理好长达200ms的网路时延,减少它对游戏体验带来的负面影响,是一个具有挑战性的技术活。

1 Model Involution

1.1 Peer To Peer

在早期的游戏中,Doom和Duke Nukem就是采用的Peer To Peer模型,在这些游戏中,每一个客户端运行的程序都是相同的,对每个玩家来说,都是将当前输入和时间戳发送给其它客户端进行处理,由于每个客户端游戏的前置状态相同,逻辑相同,在相同输入的情况下,就会产生相同的结果(状态)。其同步算法采用的是LockStep,在这里,我们将“所有客户端针对相同的玩家输入,一定会产生相同的结果”的同步算法,统称为LockStep算法。
这类模型的缺点有:

  1. 所有玩家依赖于相同的初始状态,玩家不能随意的进出游戏;
  2. 由于使用LockStep,这样当玩家数量递增时,网络引发故障的几率也相应的增加;
  3. 由于所有玩家的机器都使用相同的帧率,所以很难支持各种不同的机器;
  4. 网状网络总的连接数为O(N*N),玩家数量递增时,网络复杂度高,总体流量大。

1.2 Client To Pseudo Server

在多个玩家中,选一个作为server,在游戏过程中,其他玩家将输入指令发送给选定的server进行广播,这个模型相对于Peer To Peer而言,最显著的优势是,它使用星型网络结构,所有的客户端只和选定的server进行通讯,网络复杂度降低,总体流量要小很多;另外,它还有个好处,对于服务器资源匮乏的开发商而言,将玩家客户端选举为服务器来负责通讯,也不失为一种节省成本的方案,但它的缺点也很明显:

  1. 如果选定的server掉线了,必须再选举一个client所为新的server才能继续游戏,这增加了游戏设计的复杂度;
  2. 被选定为server的客户端拥有绝对的权威,一些geek可以利用这个特性来修改数据包从而进行作弊。
    暴雪经典的RTS游戏,比如星际争霸,魔兽争霸,使用的就是这种模型,是的,它需要有个人做主机,DOTA1由于是运行在魔兽争霸之上的,所以最开始也是这种模型,后来为了支持跨局域网功能,就由各平台服务器来充当这个server。

1.3 Client To Server

现代网游几乎都采用这种真实的C/S模型,所有客户端都通过这个权威的server进行通讯,最大的好处是,通过这个权威的服务器,你可以在服务端设置特定的反作弊策略。具体地,服务器同步那些数据,是同步所有游戏的帧状态,还是定时同步所有游戏状态的某个子集,还是仅仅同步玩家操作,不同的游戏服务器有不同的选择,however,无论哪一种要实现起来都不是一件很简单的事情。

2 C/S Architecture

引入C/S架构的一个最大好处是可以有效的防止玩家作弊,作弊是极影响游戏体验的,一款游戏其本身体验做的再好,如果玩家作弊的成本很低,外挂多,这款游戏基本也就GG了。所以,对一款C/S架构的网络游戏来说,基本准则就是:“ The server is the man”!(见UE3 UDK)

最早使用的C/S模型大致是这样子的,玩家在游戏里的所有操作都先发送给服务端,服务端统一计算,计算完成后,将游戏状态更新到客户端,客户端根据更新的状态进行渲染。举个例子,角色的初始位置在p(10, 10),按下“->”的方向键之后,会先发送消息给服务端,请求向右移动,服务端确定此玩家可以向右移动,将角色坐标更新为(11, 10),然后再将新的坐标(11,10)更新到客户端,客户端收到更新的位置之后,将角色坐标更新到(11, 10),如下图1所示:
图1

这是非常理想的C/S模型,客户端发送所有输入到服务端,服务端完成所有的操作响应和计算,最后将状态更新到客户端。这里,服务端拥有绝对的权威,对服务端来说,客户端仅仅只是一个输入和显示的终端而已。
从上图可以看出,从客户端作出向右移动的操作,到服务端计算后再新状态传回,网络延时就是一个ping的值(一般的网络大概在100ms左右) 这样的设计,对一些操作响应要求没那么高的游戏来说(例如棋类,卡牌,策略类游戏),100ms的延时还是可以接受的,而对竞技性非常强的游戏来说,100ms的时间就可以作好几次操作,每做一个操作,等100ms后才能看到反馈,对竞技类游戏来说,这点是非常影响游戏体验的。

3 Client-Side Prediction and Server Reconciliation

为了提高游戏体验,让游戏能实时地响应玩家操作,就必须在客户端有所计算。
举例,玩家按右方向键之后,先将此操作发送给服务端,与此同时,自行根据游戏地图,判定在右的方格没有阻挡物,接着执行向右移动的操作,播放移动动画,动画播放100ms之后,将坐标更新到p(11,10),接下来它就收到服务器更新位置的消息p(11,10),它刚好和客户端的位置匹配,这一切看起来很合理,如下图2所示:
图2

假定现在网络时延是250ms,客户端向右移动一格之后,并没有收到服务端的状态更新消息,此刻又向右移动了一格,根据客户端的计算,它的坐标已经是p(12,10)了,此刻又收到客户端的第一个操作的状态更新消息p(11,10),然后客户端直接将坐标更新到p(11,10),100ms之后,收到第二个操作的状态更新消息p(12,10),将坐标更新到p(12,10),如图3所示:
图3
很明显,这样的处理方式会使得客户端的坐标有明显的拉扯和抖动。由于当前服务端更新的坐标是客户端250ms之前的坐标数据,所以如果在客户端作预测的话,就不能只是简单地更新服务端发送过来的坐标。而是应该将所有服务器未确认的状态(坐标)和输入都缓存起来,并给予一个序列号用于表示缓存的时间点,客户端发送输入消息时,会带上序号,服务端在发送状态更新消息的时候,也会带上此序号,继而客户端收到之后,根据此序号,找到对应的状态,检查客户端缓存的状态和服务端发送的更新状态有无差别,没差别,说明客户端在此序号之前所作预测都是正确的,改善之后的图示看起来是这样子的,如图4:
图4

这里有一点,上面并没有解释清楚,就是在收到#1消息时,发现服务端同步的位置和客户端本地缓存不一致,此刻客户端具体如何处理,这里有一个统一的处理方式:

  1. 根据server的new state更新#1里的缓存状态
  2. 回滚input到#1,将#1之后的所有缓存的操作都应用到#1的new state中进行计算,得到最新的结果
  3. 客户端根据最新的结果进行更新
  4. 将#1的状态和input 从缓存buffer里删除

上面的例子虽然简单,但是它说明了客户端预测最主要的问题。例如,在一款回合制游戏中,你攻击了其他玩家,本地客户端可以立刻显示攻击特效,可以显示飘血数值,但是该玩家的血条不应该马上更新,直到服务端应答消息过来告诉你产生了多少伤害,这时候才进行血条更新。

由于游戏状态的复杂性,有些状态是不可以回滚的,比如死亡状态。在客户端预测时,如果某个角色因为你的攻击,它的血条为0了,你不能立刻把它判定为死亡,万一服务端一会告诉你,它在受到你攻击之前,吃了个大药,或者开了无敌呢?
总而言之,即便在游戏中的所有玩家都没有作弊,在多人在线游戏中,本地客户端预测的行为很可能和服务端实际情况不一致,这时候如何处理,如何权衡,也是一件很有意思的事情。

4 Entity Interpolation

4.1 Server time step

在上面所描述的服务器中,所举的都是单人玩家的例子,服务器的工作很简单,在主循环中,通过网络消息接收玩家的输入,然后计算,接着发送最新的状态到客户端;如果有多个玩家在一起玩,那服务端主循环的逻辑有所不同了。
在接下来的场景中,几个客户端玩家会同时发送数据,玩家发出操作指令会非常频繁(高APM的玩家,一秒钟可能发出10个以上的操作指令),如果服务端每收到某个玩家的一条指令,就去执行input逻辑,然后广播游戏状态,这样的性能是非常差的,会消耗很多的cpu和bandwidth。
一个更好的办法是,利用一个queue,将客户端的输入信息缓存起来,在服务端,游戏状态以一定的频率(例如100ms一次)进行更新,每次更新的时间间隔(100ms),就被称之为time step。
在每个更新循环迭代中,服务端处理queue里面所有的input,然后逐个进行计算,将最后计算的结果更新到所有客户端。
总体来说,整个游戏世界的更新和客户端的输入频率、数量是相对独立的,它的更新频率也是可控的。

4.2 Dealing with low-frequency updates

从客户端来看,这个方法运行的很平滑,客户端的预测行为独立于服务端的更新,所以它依然可以使用预测技术。不过,由于真个游戏世界的状态以一个比较低的频率进行更新,那么本地客户端对其他玩家知道的信息就很少了,信息越少的话,其他玩家的行为动作要模拟的话,就不会很准确。
本地客户端收到其他玩家更新坐标的消息,如何进行处理呢,有一个最简单的办法就是,每次直接使用收到的坐标进行更新,但这样看起来会有抖动,每100ms更新一次坐标,就得抖动一次,如下图5所示:
图5

对Client2来说,Client1的位置最开始再p(10,10),不一会直接跳到p(11,10),100ms后又跳到(12,0),这样的体验是很差的。那么,如何改善游戏体验呢? 针对不同的游戏,有不同的处理方式,一般来说,客户端的行为越方便预测,就越容易做平滑处理。

4.3 Dead Rocking

假定你在制作的是一款赛车类游戏,那么,赛车的速度越快,其运行轨迹越好预测,比如它的速度是100m/s,1s之后,它的位置大概就是向前移动了100m。为什么呢?因为赛车在高速运行的情况下,惯性很大,在1s内,它的运动轨迹即便不是直线,那也是一个粗略的直线,因为在这么短的时间内,这么快的速度,它是无法做180度大转弯的。

在服务端100ms更新一次的情况下,该如何处理呢?客户端在当前收到了一辆赛车的速度+朝向,100ms之后,万一没有收到新的赛车数据,客户端可以使用上一次更新的速度+朝向进行演算,不过这么做,有可能在50ms后又收到了之前的更新数据,结果发现本地模拟的赛车已经开得很远了,这时候赛车的位置就需要校准。根据多种因素,校准可大可小,如果这辆赛车一直在走之前,那就几乎不用怎么校准,但如果它发生了车祸,那预测的位置就完全错了。

4.4 Entity interpolation

某些情况下,Dead Rocking算法是不可以用来预测的。比如在一些3D的FPS游戏中,玩家跑动,停止,转向的动作是非常迅速的,这种情况下Dead Rocking算法毫无作用,因为你根本没办法从上一次更新的速度+朝向等信息中,推测出它下一步的速度+朝向。
但是,你如果只是简单地利用服务端更新过来的位置+速度进行渲染的话,在本地客户端看来,其他玩家就总是跳来跳去的,不是一个连续走动的过程,而更像是隔一段时间,从一个点瞬移到另一个点,这样的游戏是没法玩的。
有一个临时的办法解决办法,本地客户端收到数据时,根据位置和速度,在指定的时间内平滑地移动到目的地,不过这样,客户端每次都是在追逐服务端最新发过来的位置,由于网络的波动性,很难保证每100ms就能按时收到服务端发送的更新,如果下一个100ms没有收到这个更新,该如何处理呢。
解决的关键在于,不要让客户端总是去追逐服务端的更新,而是先将"不定时"收到的更新先缓存到queue中,缓存一定时间(可以是200ms)之后,再依次从queue取出数据进行定时(100ms)的更新,对于定时100ms的更新,我们做好平滑处理即可。打个比方,我们看网络视频的时候,网络时快时慢,实时播放的话有时候会很卡,但我们可以先按一下暂停键,让它下载缓冲个几分钟,然后再继续播放,边播放边下载,看起来就顺畅多了,看视频的体验也会好很多。

以下图6为例子:
图6

假定你在t=1000收到一个位置更新,而你在t=900的时候已经收到了一个位置更新,所以现在t=900,t=1000的位置更新你都是知道的,在时刻t=1000到t=1100,你就可以显示其它玩家从t=900到t=1000的位置,采用这种方式,你总是显示的是玩家实际的运行轨迹(它只是比之前的那种方式晚了100ms而已,但看起来更顺畅)。

显示其它玩家从t=900到t=1000所使用到的具体插值方法,依赖于具体的游戏,一般来说这个问题不大。如果表现还不够流畅的话,可以考虑在每次更新时,服务端发送更多移动相关的数据细节。举例,带上玩家行走过程中的一系列线段数据,或者客户端每10ms采样一次数据(你不必发送10倍的数据,因为你在移动过程中,你可以利用数据差值deltas进行数据压缩)。

注意,使用这样的技术方案,每个玩家在同一时刻渲染的游戏世界是有些不同的,因为每个玩家看自己是当前时刻,看别人则永远是过去。即使对于快节奏的游戏来说,延时100ms显示其它玩家的行动,这也并不显眼。

例外:如果你需要一些在空间和时间上的精度的话,比如当你在游戏中拿枪瞄准了某个玩家,放了一枪,由于你看到的玩家是100ms之前的,所以你标准的那个位置,实际上是玩家100ms之前所在的位置,类似这种问题将会再下一节中细述。

5 Lag Compensation

前面讲的C/S架构,可以总结一下:

  1. 服务端以比较高的频率接收客户端的输入(带时间戳)
  2. 服务端在每个time step中处理这些输入,并更新世界的状态,将世界快照(snapshot)更新到所有客户端
  3. 客户端发送输入指令,并在本地模拟输入的效果
  4. 客户端收到服务端的世界更新消息:
    4.1 同步预测状态到更新状态
    4.2 对其他玩家,依据他们过去的位置进行插值
    对一个玩家来说,这有两个重要的结果:

1 他看自己是现在时
2 看别人是过去时
这一般来说也没什么大问题,但对时间和空间很敏感的事件来说,就行不通了,比如你拿狙击枪对着某个玩家爆头。

你用狙击步枪瞄准目标的头部。你投篮——这是一个你不能错过的投篮。
但你错过。
为什么会这样?
由于之前解释过的客户机-服务器体系结构,您的目标是敌人的头部在您射击前100毫秒的位置——而不是在您射击时!
在某种程度上,这就像在一个光速非常非常慢的宇宙中玩耍;你瞄准的是你的敌人过去的位置,但当你扣动扳机时,他早已不在了。
幸运的是,对于这个问题有一个相对简单的解决方案,这对于大多数玩家来说在大多数情况下都是令人愉快的(除了下面讨论的一个例外)。
它是这样工作的:
当您射击时,客户端将此事件发送到服务器,并提供完整的信息:您射击的确切时间戳和武器的确切目标。
这是关键的一步。由于服务器获取所有带有时间戳的输入,因此它可以在过去的任何时刻重构世界。特别是,它可以在任何时间点按照任何客户端眼中的样子重建世界。
这意味着服务器可以准确地知道你开枪的那一刻你的武器瞄准了什么。这是你的敌人过去的头部位置,但服务器知道这是他的头部在你当前客户端所在的位置。
服务器及时处理该镜头,并更新客户端。
每个人都很满意!
你很高兴,因为你瞄准了敌人的头部,一枪爆头!
敌人可能是唯一不完全快乐的人。如果他中弹时站着不动,那是他的错,对吧?如果他在动…哇,你真是个很棒的狙击手。
但如果他是在一个开放的位置,躲到墙后,然后在不到一秒钟的时间里,当他认为自己是安全的时候,被击中了怎么办?
这是可能发生的。这就是你要做的权衡。因为你在过去向他射击,他可能会在掩护后几毫秒内被击中。
这有点不公平,但对所有相关人员来说,这是最令人满意的解决方案。错过一个不可错过的镜头会更糟!

6 Further reading

  1. What Every Programmer Needs to Know About Game Networking
  2. Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization
  3. Unreal Networking Architecture
  4. 《守望先锋》架构设计与网络同步

多人在线网络游戏的同步算法一 基础篇相关推荐

  1. 多人在线网络游戏实现细节

    作为一个程序员,你有没有想象过多人游戏是如何实现的? 在外行人看来游戏很神奇:两个或者更多的玩家在网络上分享共同的经历,就像他们真实的存在于相同的虚拟的世界一样.游戏看起来犹如一个巨大的魔术,奇妙而又 ...

  2. 网络游戏demo开发实例:多人在线RPG游戏(MMO RPG)demo的开发记录(第0篇)

    最近在业余时间制作一个MMO RPG的demo,代码提交在github上,有兴趣的朋友可以自己导出代码(https://github.com/changjixiong/MMO-RPGGame), 方法 ...

  3. Java创建服务端和客户端基础(一)多人在线聊天程序实战基础

    基于Java一步步实现多人在线聊天!(持续更新中) 文章目录 ⚡前言

  4. 使用FLEX3开发大型多人在线游戏

    使用FLEX3开发大型多人在线游戏 2009-09-02 10:07 使用FLEX3开发大型多人在线游戏 收藏 使用FLEX3开发大型多人在线游戏 大型多人在线游戏(MMO)技术已经涉足到各种软件形式 ...

  5. 使用Adobe Flex 3开发大型多人在线游戏

    使用Adobe Flex 3开发大型多人在线游戏 2011年03月28日 大型多人在线游戏(MMO) 技术已经涉足到各种软件形式中了. 当我们还在思考MMO 时, 多人游戏已经使很多玩家能够实时连接进 ...

  6. rudesocket如何使用_[WebSocket入门]手把手搭建WebSocket多人在线聊天室(SpringBoot+WebS...

    前言 本文中搭建了一个简易的多人聊天室,使用了WebSocket的基础特性. 源代码来自老外的一篇好文: 本文内容摘要: 初步理解WebSocket的前后端交互逻辑 手把手使用 SpringBoot ...

  7. 多人在线游戏服务器端开发心得(转)

    http://edu.21cn.com/youxi/g_141_548352-1.htm 一个多人在线的棋牌类网络游戏的项目临近尾声,我参与了该项目的整个设计流程,并且完成了90%的核心代码.关于这个 ...

  8. 做一个“多人在线编辑器”,你会怎么开始

    看似只是一个简单的问题,但是其中却隐含了非常多的知识,对于"多人在线编辑器"这么一个产品来说,如果让你来负责设计并开发,你会怎么去开始一步步展开工作,其中主要考察的并不是让你迅速的 ...

  9. 疯狂的华为MateX2:375万人在线抢,转手一台赚2万

    「核心提示」 华为MateX2还未发售,就被经销商.消费者拱上神坛.发售后,引得375万人在线抢购,抢到一台转手就赚2万元.热炒十天后,MateX2售价全线下跌,溢价空间只有5000元左右,经销商热情 ...

最新文章

  1. ABBYY在MS Office中创建PDF文件的方法
  2. Roberts 边缘检测
  3. ubuntu14.04连接网络 No valid active connections found!
  4. box2d 碰撞检测_(译)如何使用box2d来做碰撞检测(且仅用来做碰撞检测)
  5. SQL Server索引怎么用
  6. Angular Render2
  7. ImageNet标签竟然部分有误!数据集MNIST也会出错?
  8. HijackThis日志细解【简明教程增强版】(六)
  9. Tomcat SSL Configuration
  10. 统计叶子结点c语言,统计二叉树中叶子结点个数
  11. 开关电源电压型与电流型控制
  12. Oracle的表使用plsql导出excel文件
  13. java doc转pdf又来了,这一次超简单
  14. s7scan 安装使用教程
  15. 网络编程(五) ———— 万字详解TCP协议
  16. kali linux 软件包密钥管理
  17. 印度人写的java代码
  18. gsensor direction调试
  19. Minecraft 1.18.1、1.18.2模组开发 23.3D动画盔甲制作
  20. c执行cmd pdf2swf_在web 上调用exe权限解决方法 ,关于FlashPaper2 命令行FlashPrinterexe的调用...

热门文章

  1. CloudKit快速入门之02 通过保存记录创建数据库架构 (SwiftUI iCloud CloudKit中文文档手册教程)...
  2. 世界杯 叮当 机器人 树莓派_这个夏天,叮当很忙! 腾讯叮当世界杯大数据报告...
  3. 深度学习课程大纲_一份639页深度学习:Deep Learning硬核课程PPT
  4. 直放站引起上行高干扰 小区处理案例
  5. 本地查找重复/相似的图片、视频的图片查重工具
  6. 【操作系统】计算机中内存、cache和寄存器之间的关系及区别
  7. 设计模式之备忘录模式(Java实现)
  8. 绚丽的野外环境AE电子相册模板
  9. java CSAE 的穿透
  10. 我所知道坦克大战(单机版)之添加多个机器坦克、让机器坦克动起来、让坦克智能起来...