大多数实时网络游戏,将 server 的时间和 client 的时间校对一致是可以带来许多其他系统设计上的便利的。这里说的对时,并非去调整 client 的 os 中的时钟,而是把 game client 内部的逻辑时间调整跟 server 一致即可。
一个粗略的对时方案可以是这样的,client 发一个数据包给 server,里面记录下发送时刻。server 收到后,立刻给这个数据包添加一个server 当前时刻信息,并发还给 client 。因为大部分情况下,game server 不会立刻处理这个包,所以,可以在处理时再加一个时刻。两者相减,client 可以算得包在 server 内部耽搁时间。
client 收到 server 发还的对时包时,因为他可以取出当初发送时自己附加的时刻信息,并知道当前时刻,也就可以算出这个数据包来回的行程时间。这里,我们假定数据包来回时间相同,那么把 server 通知的时间,加上行程时间的一半,则可以将 client 时间和 server 时间校对一致。
这个过程用 udp 协议做比用 tcp 协议来的好。因为 tcp 协议可能因为丢包重发引起教大误差,而 udp 则是自己控制,这个误差要小的多。只是,现在网络游戏用 tcp 协议实现要比 udp 有优势的多,我们也不必为对时另起一套协议走 udp 。
一般的解决方法用多次校对就可以了。因为,如果双方时钟快慢一致的情况下,对时包在网络上行程时间越短,就一定表明误差越小。这个误差是不会超过包来回时间的一半的。我们一旦在对时过程中得到一个很小的行程时间,并在我们游戏逻辑的时间误差允许范围内,就不需要再校对了。
或者校对多次,发现网络比较稳定(虽然网速很慢),也可以认为校对准确。这种情况下,潜在的时间误差可能比较大。好在,一般,我们在时间敏感的包上都会携带时间戳。当双方时间校对误差很小的时候,client 发过来的时间戳是不应该早于 server 真实时刻的。(当时间校对准确后,server 收到的包上的时间戳加上数据包单行时间,应该等于 server 当前时刻)
一旦 server 发现 client 的包“提前”收到了,只有一种解释:当初校对时间时糟糕的网络状态带来了很多的时间误差,而现在的网络状态要明显优于那个时候。这时,server 应该勒令 client 重新对时。同理,client 发现 server 的数据包“提前”到达,也可以主动向 server 重新对时。
一个良好的对时协议的设定,在协议上避免 client 时间作弊(比如加速器,或者减速器)是可行的。这里不讨论也不分析更高级的利用游戏逻辑去时间作弊的方式,我们给数据包打上时间戳的主要目的也非防止时间作弊。
校对时间的一般用途是用来实现更流畅的战斗系统和位置同步。因为不依赖网络传输的统一时间参照标准可以使游戏看起来更为实时。
首先谈谈位置同步。
好的位置同步一定要考虑网络延迟的影响,所以,简单把 entity 的坐标广播到 clients 不是一个好的方案。我们应该同步的是一个运动矢量以及时间信息。既,无论是 client 还是 server ,发出和收到的信息都应该是每个 entity 在某个时刻的位置和运动方向。这样,接收方可以根据收到的时刻,估算出 entity 的真实位置。对于 server 一方的处理,只要要求 client 按一个频率(一般来说战斗时 10Hz 即可,而非战斗状态或 player 不改变运动状态时可以更低) 给它发送位置信息。server 可以在网络状态不好的情况下依据最近收到的包估算出现在 player 位置。而 client 发出的每次 player 位置信息,都应该被 server 信任,用来去修正上次的估算值。而 server 要做的只是抽查,或交给另一个模块去校验数据包的合法性(防止作弊)。
在 server 端,每个 entity 的位置按 10Hz 的频率做离散运动即可。
client 因为涉及显示问题,玩家希望看到的是 entity 的连续运动,所以处理起来麻烦一点。server 发过来的位置同步信息也可能因为网络延迟晚收到。client 同样根据最近收到的包做估算,但是再收到的包和之前已经收到的信息估算结果不同的时候,应该做的是运动方向和速度的修正,尽可能的让下次的估算更准确。
关于战斗指令同步,我希望是给所有战斗指令都加上冷却时间和引导时间,这正是 wow 的设计。这样,信任 client 的时间戳,就可以得到 client 准确的指令下达时间。引导时间(或者是公共冷却时间)可以充当网络延迟时间的缓冲。当然我们现在的设计会更复杂一些,这里不再列出。对于距离敏感的技能,例如远程攻击和范围魔法,我们的设计是有一个模糊的 miss 判定公式,解决距离边界的判定问题。
这里, server 对攻击目标的位置做估算的时候,可以不按上次发出包的运动方向去做位置估计,而选择用最有利于被攻击者的运动方向来做。这样,可以减少网络状况差的玩家的劣势。
对于 PVE 的战斗,甚至可以做更多的取舍,达到游戏流畅的效果。比如一个网络状态差的玩家去打 npc,他攻击 npc 的时刻,npc 是处于攻击范围之内的。但是由于网络延迟,数据包被 server 收到的时候,npc 已经离开。这个时候 server 可以以 client 的逻辑来将 npc 拉会原来的坐标。
虽然,这样做,可能会引起其他玩家(旁观者) client 上表现的不同。但是,网络游戏很多情况下是不需要严格同步的。在不影响主要游戏逻辑的情况下,player 的手感更为重要。
client按10Hz的频率给server发位置包,在线人数多的时候,服务器处理的过来吗,感觉服务器压力蛮大的。还有,小包太多,网络不会堵吗
你好,我在你的对时基础上做了改进。比如当前客户端时间是A,真实的服务端时间是B,校对获取的服务端时间为C,此时客户端的时间为D,那可猜测服务端的时间为C~C+D-A,用C+(D-A)/2作为估计值,误差为(D-A)/2。字母都加*作为下次同步,可猜测服务端的时间为C*~C*+D*-A*。上一次估计在现在的估计时间变为C+D*-D~C+D-A+D*-D。那此时需要同时满足C*~C*+D*-A*和C+D*-D~C+D-A+D*-D两个条件。比如第一次延迟为60和20,而第二次延迟为20和60,这个算法可以让误差缩小到20,真实的误差为0
你好,谢谢你的文章带给我的很多思考,我这里有个问题,希望你能指点一二:我在做的一个游戏demo也需要做人物移动的位置同步,但是我们的前端服务器在消息的广播时有个100ms(或者更长)的tick延迟,所以相邻两次的消息广播的传输时间会有较大的差距,在这种情况下客户端和服务器的时间校对一致会不会有较大的误差,谢谢哦!
网络中最好对于同一个客户端的数据包加上版本号,服务器发送的数据包也一样,从而在逻辑上保证哪些是重复的,随机数是不能使用的,因为有可能重复,只能用于安全验证。无法从逻辑上避开重复性
让我想起了分布式操作系统那门课程……
那如何在旁观者的客户端平滑的显示另一个玩家的运动轨迹呢?我做的时候采用3次曲线插值的方法,可是跟WOW相比,总感觉有些不自然。想听听高手的意见。
NTP在Internet上的理论延时是1/100秒。为什么没有考虑这个呢?
好文!
对我的工作很有帮助!
谢谢!
网游并不需要这么严格的对时吧,一直都让客户端比服务器端慢一定的帧数就行了。至于战斗指令,完全可以认为是服务器收到时刻发出的,对网络时延做补偿意义不大,特别是对wow这样的慢节奏来说。250ms以下的延时,完全可以忽略;再高,应该是ISP的问题,而不是程序员的了。
至于位置同步,我看最重要的不是处理时延,而是广播占用的带宽,O(N^2)的复杂度,膨胀起来可是非常吓人的。wow里一攻打堡服务器就down,上次AQL开门客户端下行数据有50KB/s,一千个玩家就有50MB/s啊,就是400Mb/s。
呃..犯了个常识错误。呵呵。
在复杂的网络环境下同步的确很困难。 :)
你的文章对我作web应用也有点启发。
可见CS的时间同步比较精确了
一颗子弹耽误了时间KILL可不了土匪了
这要看怎样定义通讯协议,长度信息是一个冗余信息,如果有利于设计那么可以选择保留。但是并非所有的设计都需要这个来降低耦合性。正如你说,"一般我们使用一个网络层来接收数据包,由他来划分包的边界",这个只是一般的设计。完全可能存在另一种设计把类型的初步解析放在同一个层次,而并不会增加这个层次的规模。
我的意思是,在设计基于 tcp 的通讯协议的时候不应该有教条。
前两天才看到你的博客的,很多文单对我很有帮助.
看到一处,你决定在网络封包里放弃包长度的说法,觉得不妥.如果你通过包类型来判断包的长度的话,那么会将包逻辑信息与系统的耦合性提前.简单的说吧,一般我们使用一个网络层来接收数据包,由他来划分包的边界,完成一个,向上提交一个,如果按你的办法取消包长度的话,那么这一个本来只负责网络收发的类,就需要与你的通讯协议耦合起来了.不知道说得对不对.
npc 逃掉不是由 client 做的,是由 server 做的。client 只是收到比较晚。
这里说的是一种 server 在可以选择的情况下把时间回退,而非让 client 参于运算。真正处理时有很多东西要考虑,不想展开讨论。
1.客户端发给服务端 时间戳 t1
2.服务端接收到 时间戳 t2
3.服务端发给客户端 时间戳 t3
4.客户端接收到 时间戳 t4
网络来回总延时 delay = (t4 - t1) - (t3-t2) //t3-t2为服务器内部耗时
客户端与服务端时间差 delta = ( (t2-t1) + (t4-t3) - delay ) / 2
delay1 为 t1 至 t2 延时,delay2 为 t3至t4延时
t2 - t1 即 delta + delay1
t4 - t3 即 delta + delay2
delay = delay1 + delay2
这样可否?