others-帧同步lockstep
others-帧同步lockstep
前篇
MOBA游戏里的网络同步技术 – Unite2017陈实分享 - https://gameinstitute.qq.com/community/detail/114305
unity帧同步游戏极简框架及实例(附客户端服务器源码) - https://blog.csdn.net/wanzi215/article/details/82053036
- Client - https://github.com/CraneInForest/LockStepSimpleFramework-Client
- Server - https://github.com/CraneInForest/LockStepSimpleFramework-Server
两个仓库 clone 时都需要 recursive, 包含共享逻辑部分
帧同步优化难点及解决方案 - https://blog.uwa4d.com/archives/USparkle_frame-alignment.html
lockstep 网络游戏同步方案 - https://blog.codingnow.com/2018/08/lockstep.html
云风:浅谈《守望先锋》中的 ECS 构架
云风:继续谈网络游戏的同步问题
GAD:《守望先锋》架构设计与网络同步
GAD: Unity3D RTS游戏中帧同步实现
GAD: 游戏网络开发(五):浮点数的确定性
《王者荣耀》技术总监复盘回炉历程
skywind: 再谈网游同步技术
skywind: 帧锁定同步算法
游戏举例
帧同步
不随着单位增加流量、天然支持回放、强一致
- 王者荣耀
- Dota, 基于魔兽争霸3
- 风暴英雄
状态同步
(因为无法观察整个世界)的第一人称用状态同步会非常合适
- Dota2
- 全民超神
帧同步的重难点
主要参考:
- MOBA游戏里的网络同步技术 – Unite2017陈实分享 - https://gameinstitute.qq.com/community/detail/114305
1. 主循环 频率调成一致
首先一定要把各客户端的核心逻辑的同步频率保持一致。如果你的频率不一样,比如用Unity的函数,每一帧运行时间间隔不一样,如果这个不一样的话会带来非常多的问题,基本上是不可能算出一样的结果,首先第一步就把这个频率调成一样的。
2. 随机数
我们几乎不能使用Unity自带的随机数,因为我们知道我们通常使用的是伪随机数,我们为了保证在各客户端的随机数是一样的,需要做哪些工作。首先需要把初始化的种子要同步,它们是一样。然后还要保证在各个客户端上,随机数调用的次数是一样.
为什么不能使用Unity提供的随机数,因为这个随机数可能会被一些粒子系统,一些别的内部系统去调用,我们是没有办法控制这些调用的次数的,所以,我们需要实现一个自己的随机数,来保证他的调用次数。
3. 定点数
硬件架构不一样,或者用不同的编译器编译我们的代码,能不能保证浮点数是一样的结果,应该是可以,但是基本没有看到哪家厂商有这样实现过,为了做到这一点需要做非常多额外的工作,可能需要针对不同的CPU,然后不同的编译器来优化我们的代码,还要做一些额外的工作,才能保证算出来的结果是一样。我们通常还是用定点数来算。
客户端必须保证对网络帧操作的运算过程和结果一致,然而不同系统平台对浮点数的处理有差别,即便差别甚微,也会造成“蝴蝶效应”,导致不同步现象出现。绝大多数情况下,只需要对游戏对象方位进行定点数改造即可。而Unity3D并非开源游戏引擎,无法对底层transform的position和rotation进行修改。因此,逻辑层计算时需要使用到自定义以定点数为基础的position和rotation,并在每次循环结束之前,将自定义的方位逻辑计算之后所得信息转化Unity3D transform,以便Unity3D更新表现层。使用Unity3D的协同功能Coroutine以及WaitForEndOfFrame()可满足上述需求,即在逻辑层计算完成后,在Unity3D渲染之前更新底层transform的position和rotation。
对于计算的不确定性,我们也有一些小的隐患,就是我们用到了Physics.Raycast来检测地面和围墙,让人物可以上下坡,走楼梯等高低不平的路,也可以有形状不规则的墙。这里会获得一个浮点数的位置,可能会导致不确定性,这里,我们用了数值截断等方式,尽量规避,经过反复测试,没有出现过不一致。但是这种方式,毕竟在逻辑上,存在隐患,更好的方式,是实现一套基于定点数的raycast机制,我们人力有限,就没时间精力去做了。这块有篇文章讲得更细致一些,大家可以参看:
帧同步:浮点精度测试 - https://zhuanlan.zhihu.com/p/30422277
4. 变量初始化
使用一些第三方库的话,比如使用一些box2D这种库,使用自己开发的库来做物理运算的时候,还是注意这个问题,可能一些变量没有初始化,那在不同平台上,或者不同编译器编译出来的代码,初始的值是不确定,如果用这个变量去运行一个逻辑的话,很可能就不同步了,而且还很难意识到这个问题。如果脑子里面有这个概念,变量是必须初始化,就能避免这个问题,同时发生问题的时候,也可以往这个方面想一下
5. 遍历的顺序
我们经常用Dictionary这个结构,你用这个结构是有问题,你可能天真地认为,往Dictionary顺序插入一二三四个元素,他是不是按一二三四的顺序去遍历,官方是不能保证的,如果用这个结构,大家可以尝试使用另外一种SortedDictionary这个容器,这个容器有一些开销,如果不频繁插入的话是没有什么问题。
6. 流畅性 - 网络选择
如果你觉得你的游戏对你的实时性要求比较强,还是推荐你用UDP,使用UDP带来什么问题,首先UDP是不可靠的传输协议,可能需要自己去处理一下丢包,还有顺序。通常常用的解决方案就是增加冗余数据,如果一次发包,可以一次带两帧的数据,如果丢包之后,丢了一个包,但是仍然可能还是有额外的一帧供你使用,继续去渲染
7. 逻辑和显示的分离
例如,我们动作切换的逻辑,是基于自己抽象的逻辑帧,而不是基于Animator中一个Clip的播放。比如一个攻击动作,当第10帧的时候,开始出现攻击框,并开始检测和敌人受击框的碰撞,这个时候的第10帧,必须是独立的逻辑,不能依赖于Animator播放的时间,或者AnimatorStateInfo的NormalizedTime等。甚至,当我们不加载角色的模型,一样可以跑战斗的逻辑。如果抽离得好,还可以放到服务器跑,做为战斗的验证程序,王者荣耀就是这样做的。
8. 外挂
属性外挂
通常外挂就是修改属性,修改属性的话,如果是使用帧同步的话,修改属性基本上没好说的,虽然是在客户端各自算的,但是由于你的数据并没有说去同步,如果只是简单地把本地的数据给改了,比如把攻击力从10调到一万,去秒杀你,但这没什么用,因为别的客户端攻击力还没有提高,别的客户端看到还是正常,这种时候我们可能需要一个检测,是否本地有一些修改。可能你会会把一个玩家所有的属性做一个Hash,然后去服务器上做一个校验,如果你服务器没有运行这个逻辑,不知道这个值算的对不对,可能需要把别的客户端也发出来做一个仲裁,如果这个人算出来跟其他几个人算出来不一样,那更倾向于认为这个客户端肯定是有问题的。
修改属性除了刚才说的方法,可以通过一些加密变量的方案,让外挂找不到真实的值在哪里。通常这个效果不是特别好,如果想改的话也是能改,通常还是会去使用一个校验。
透视外挂
以前老玩家玩的Dota比较多的,这个外挂还是比较多的,因为在Dota里面视野是非常重要的,由于所有数据都在本地,你的控制这个玩家显不显示,可能只是一个变量的问题,改了这个东西,就可以看到这个东西。但是对于一些其他的游戏可能,就相对来说没有那么重要,还是说看你们的游戏,这个重不重要。如果重要的话,我们会怎么办?可能需要,一些关键数据。虽然帧同步只同步指令,但是应对外挂问题上,关键的数据还是选择去同步。比如你的透视只是一个变量的问题,可能就是一个字节的问题,对你网络的带宽就传输的东西影响是非常小的,但是就可以完全避免这个问题。
自动操作外挂
对我们这种类DOTA的MOBA游戏影响不大,能模拟一个玩家的操作表现很困难。但是对《守望先锋》,这种操作外挂就影响非常大,比熟练玩家瞄的准多了,这个没有特别好的方法,如果所做的是一种类DOTA的MOBA游戏,就不需要去关注这种外挂,一因为也不太可能搞出这种外挂,如果做FPS这种游戏还需要关注一下,目前业界没有特别好的解决办法,还是会靠一些发动一些玩家去检测来,用真人来检查,来解决这个问题。