ouster开发笔记第二周
2014-04-14
因为要计划离开上海回家,停留武汉几天。期间收拾东西,联系亲友,关注一下公司对自己游戏的善后处理。分心了,这周就是真的没做什么事情了,项目完全没有推进。
封包那边,服务端Go语言毫无问题,痛苦的根源就在客户端C语言这一边,即使用了msgpack-c的库,也只能解析出一个msgpack_object出来。但是C语言肯定要转成struct 才好用。因为没有类型信息,Unmarshal很难做,用void*来表示是不行的,代码生成也没法处理递归调用。最后做了折衷,代码生成时不支持结构体中嵌套结构体。
言归正传。这篇文章就主要整理一下同步问题吧。
记得云风大神的开发笔记系列中提过的,这一块不解决好,到后期项目忙起来,就没机会做好了。
以角色移动为例,从客户端发送一个消息包出去,到达服务端,服务端再转发到其它客户端,这中间是有两个网络传输的时差的。同步就是如何让不同客户端之间,客户端与服务端这间,表现相对的一致。在网上查了些资料,相关东西不多,讲得也不细。有几类做法。一类客户端等待服务端的确认包后才能执行移动。另一类客户端可以自己移动,根据服务端发过来的真实坐标,进行补偿。补偿比如对移动速度进行调整,或者加快减慢帧的播放。
darkeden的做法是最简单的。充分信任客户端,角色移动之后,客户端每隔一定的时间向服务端发送一个PCMove的包,含有角色的坐标信息。服务端收到包之后,几乎没做什么校验,立即就向客户端发回确认,并向其它玩家广播这个移动包。收到服务端发回的确认包之后,客户端那边角色才会继续移动。如果网速卡,收不到服务端发回的移动确认,角色就停顿在某一帧了。而如果发生粘包,同一次中收到服务器那边发回来的确认过多,则出来角色以很快的速度迅速播放完移动的动画。
以前玩这个游戏时看到过一个场景,跟朋友组队在网吧玩,我自己机器上看到角色还在往前走,而朋友机器上看到的则是我被怪物定身了殴打。他那边显示是服务器的真实信息,而我看到的是假的。几秒之后我这边角色被强制拉回原处,显示被怪打死了。这个现象说明,客户端移动是先于服务器确认的:就是客户端走几步,服务端确认客户端走的这几步,客户端收到确认后,再继续走几步这种流程。
darkeden是2003年左右的游戏了,由于充分信任客户端,外挂满天飞。角色移动的坐标不是在服务端做的,而是在客户端做的。而且服务端收到客户端发送的坐标后也没做严格的校验,只是把一个客户端发来的包广播给他周围玩家。导致可以看到别人用外挂,一秒之前还在你看不到的远处,下一秒就飞到你前面来了。原理就是服务器信任并 广播了外挂伪造的移动包坐标信息。除了移动,这个游戏包括技能方面也没处理好,有些无冷却CD之类的技能很容易被外挂利用做加速,所以上面枪那个职业开挂那叫一个变态啊。水魔灵也是的冰矛也是可以开挂,我玩游戏时看到过的有人用加速。总之这个故事告诉我,一定不能信任客户端,必须放到服务端做!
不过也不绝对,跟网友交流,他们做一个山寨COC,寻路逻辑是在客户端做的,服务端只校验。一方面是节省性能的考虑,另一方面是手机的网络环境跟端游差远了,会卡。扯远了...
网上看到的,写WOW的位置同步的做法。WOW方向控制移动的,跟这种2d的坐标控制移动略有不同。客户端发送一个移动包过去,包中会带有移动的方向信息,服务端收到包后开始执行移动。在收到停止移动或者改变移动方向的包之前,服务器那这会一直保持方向执行移动。服务端每隔一定的时间会向客户端发送一个包,对客户端的当前移动进行确认。如果客户端没有收到进一步的移动确认,就停在原处。动画还是播放的,那么显示上就是人物在原地跑啊跑的。服务端每送过来一个包,客户端就允许往前走一点点距离,如果没有下一个包到达了,客户端就像在原处。
如果改变方向了,那么客户端这边会发送一个方向改变的移动包,那么服务端就会调整执行新的移动方向。服务端主导的移动,就会出现跳崖之类的现象:网络卡的情况下,客户端发出向前走了,服务端收到包并执行向前走的动作,由于网络延时客户端没及时收到及时反馈,于是玩家没有下达停止移动的指令,然后就掉下去了。事实上我没玩过WOW,有些东西说的可能不一定对。
由服务端主导移动做法是对的,不应该信任客户端。客户端那边只能决定往哪个方向走,是走还是停,然后向服务端发送命令。
看到网上有人提到了粘包的问题。服务器那边对于一次移动的包并没立即发给客户端,而是与下一次的移动包一起发送了。客户端那边就遇到一次没收到移动确认,而另 一次却同时来了两个移动。这是别人的实战经验,我暂时还没有太多思考过,到遇上再看。作为常识,肯定要关掉Nagle算法的。
上面都是写别人的做法和遇到的一些问题,下面说自己的想法。
客户端始终不知道当前发生了什么,只知道曾经发生过什么,还不知道准确是什么时间发生的。因为客户端收到服务端发来的消息包时,已经经历过一个网络传输时间,而且经历的这个时间由于网络波动或者粘包等原因,还是不确定的。
每个客户端都想告诉服务端它要干什么,但是服务端才能决定实际发生了什么。比如玩家想一个技能甩上去把对方秒了。服务端却告诉他,技能甩上去Miss了,反而被对方 一个技能秒了。即使最简单的,想从A点走到B点,也不能保证你在A到B点移动过程中被技能定身了。
真实与流畅的折衷是必须要的,最反映真实的做法就是服务端告诉客户端发生过什么后,客户端才能发生。但是这代表每次都经过了两次的网络传输,影响流畅性上的体验。最反映流畅的做法就是客户端接受用户输入后,立马执行,不等待服务端的确认。
- 移动都在服务端做,完全不信任客户端。
客户端只能给向服务端发送“我想去坐标(XX XX)”。服务端收到请求后,在自己这边执行移动。并每隔一段时间向客户端发送一个角色当前坐标信息。
- 伪同步,客户端只拿服务端坐标作为参考值。
我想做一个预测同步,客户端收到服务端发来的包才能执行移动,但也不是先移动一小段,在期间收到服务端发来包才移动下一小段,否则等待。
服务端发来的包中会包含当前坐标和目标坐标,客户端收到包就开始移动了,根据速度,坐标方向执行。只要客户端与服务端坐标相差不超过一个范围,客户端都是可以继续移动的,以保证流畅。这是一个伪同步,期间如果网络正常,客户会不停地收到服务端发来的坐标值,根据服务端发来的真实位置进行补偿以达到同步。
- 补偿
前面已经说过是伪同步了,补偿就是来做尽量的同步。做精确对时会是一个坑死人不偿命的方式。客户端收到服务器发来的坐标的那一刻,代表着一个网络传输时间之前,服务器上的角色的坐标是在XX。客户端可以通过这个参考值,估算自己与服务端真实位置偏移了多少。只要偏移值不大到一定程度,都不会理会,客户端按照自己的节奏播放移动动画。如果偏移值太大了,则进行补偿。
客户端可以记录以下信息,上次包到达时时间,真实坐标,目标坐标,客户端坐标。这次包到达时时间,真实坐标,目标坐标,客户端坐标。