帧同步到底是什么

前段时间看了很多关于同步机制的文章,和同组leader也讨论了很多这方面相关的内容,总结了一部分,在此写下保存自留吧

帧同步

主旨:同步的玩家操作指令 “相同的输入 + 相同的时机 = 相同的显示

目的:在于消除网络波动性带给玩家的卡顿以及忽快忽慢的不良体验。

大致流程:

  1. 同步随机数种子(可以保持同步的一致性)
  2. 客户端上传操作指令(游戏操作以及当前帧索引)
  3. 服务端转发客户端指令(没有指令也要广播空指令以推进游戏帧)

特点:客户端的逻辑实现和表现实现是需要完全分离的,需要有自己的一套物理引擎,这样及时unity的渲染不同步,但逻辑跑出来是同步的

设计模式:所有C端强制采用一个逻辑帧率,从而保证输出一致

同步机制:时间同步+指令同步

时间同步:所有玩家同时间开始游戏

  1. Loading界面来同步所有玩家的资源加载进度
    • 加载完之后也不一定同步,因为还有些初始化的逻辑
  2. 开场动画消除初始化逻辑差异
    • 三秒的动画,A用了两秒初始化,B用了一秒初始化,A就播一秒动画而B就播两秒动画
  3. 多端时间的同步:
    • 请求服务器的时间并同时计算这次请求的ping值,就是包到服务器然后再回来的时间。然后这个ping值除以2再加上服务器的返回的时间,就可以得到一个准确的当前服务器的时间。接着,后面游戏过程中的同步,我们也可以根据更小的ping值来修正时间。

指令同步:服务器每一帧会收集所有玩家的操作,然后广播给所有的玩家。

核心逻辑的实现

  • 命令队列:所有的玩家操作不会直接添加到角色身上,而是放在队列中,然后再从队列中拿出操作
    • 单机模式的监听器:监听到玩家操作的时候,直接把操作加入队列中
    • 网络模式的监听器:监听到玩家的操作后,把它发送到服务端,同时他也会监听服务端,把服务器返回的操作插入队列中
  • 游戏的主循环帧同步对游戏的逻辑执行顺序有严格的控制,因此一般不使用引擎上的update,得要控制帧率

帧率的控制

  1. 按特定的帧率去执行游戏:比如一秒10帧的一个逻辑帧,60秒一个显示帧
  2. 要控制追帧:如果说他的游戏进度落后了,那么他应该要跑更多的逻辑帧,原本一秒10帧,追帧的情况下可能一秒需要跑60帧,快进到当前进度。
    • 比如,我们游戏卡了很久,那么update传进来Delta会很大,那么把这个Delta加进去之后,它会远远大于你原本所需要执行的帧数,这时候我们会加速执行,执行到一定的数量退出,然后下一次再加速执行,直到当前进度。
    • 在执行逻辑之前,需要对所有的对象先走一次排序,然后按特定的顺序执行每个对象的update的方法去执行逻辑。

如何对抗网络延迟

  1. 增帧缓冲和前摇动画去掩盖延迟
  2. UDP替换TCP
    1. 原因:TCP实时性差(Nagle算法的TCP_NODELAY会使发送时间变长,因为他需要收集小包后在一次性传);超时重传发生的时间相比也太长(只要有一帧的包没有收到,游戏逻辑就无法正常执行,直到接收到包。等超时了,TCP的快速重传机制会被触发,才会重新发包)
    2. 可靠的UDP:在UDP上加一层封装,自己去实现丢包处理,消息序列,重传等类似TCP的消息处理方式,保证上层逻辑在处理数据包的时候,不需要考虑包的顺序,比如KCP。还是会有风险,因为它为了保证包的顺序和处理丢包重传等,在网络不佳的情况下,Delay很大
    3. 冗余的UDP:两端的消息里面,带有确认帧信息,比如客户端(C)通知服务器(S)第100帧的数据,S收到后通知C,已收到C的第100帧,如果C一直没收到S的通知(丢包,乱序等原因),就会继续发送第100帧的数据给S,直到收到S的确认信息。效果会更好,只要和服务器商议好确认帧和如何重传即可
  3. 锁帧同步(deterministic lockstep):客户端的每一帧的推进,都需要得到同一局的所有玩家确认,所以一个玩家掉线游戏就会暂停。卡住等这一帧的数据下来,之后会加速追回到当前的进度
    1. 适合局域网P2P,比如DOTA等。因为是局域网这种情况比较少,所以可以接受
  4. 不锁帧同步(预测回滚):每个客户端和服务端以一定的帧率进行帧数据的同步,服务端只管当帧时间抵达时,将数据整理成一个帧数据包广播给所有的客户端。客户端收到帧数据包后对帧号进行比对,判断是否需要进行追帧,和执行逻辑。如果客户端没有操作的话,服务器是不会下发空包的,有操作的时候才进行广播。
    1. 客户端没有收到包也不会卡住,而是继续执行。所以客户端的网络只会影响自己的游戏体验,其它网络正常的玩家是可以正常游戏的。
    2. 需要客户端纠错。向服务器请求一份最新的状态,在客户端反序列化出来。另外,客户端本身也可以做回滚然后重试”这样的一个机制,就是回滚到最近的一个正确的状态,然后再追回。
    3. 适合C/S模式,比如LOL,王者荣耀
    4. 不适合局域网,因为如果作为主机的客户端离线或者丢包,其他客户端是没办法保持一致的状态。而中心服务器模式,会将帧数据储存起来再广播出去,即使客户端重连,也是可以获取到帧数据队列进行追帧

战斗框架设计

核心:逻辑层与显示层分离,使用延迟执行实现模块解耦

  1. 逻辑层和显示层分离后,可以按不同的频率跑。比如逻辑帧10/s,显示帧60/s,显示层会根据逻辑帧状态变化而变化
  2. 低耦合高内聚。利于客户端的序列化和反序列化
  3. 安全性高一般帧同步都是信任客户端的,这样可以在服务器上实时或者离线地校验战斗结果,对外挂有很好的防范效果

断线重连的优化

  1. 服务器跑逻辑,重连的时候让服务器发送最新的状态给客户端。如果频繁的断线重连,会对服务器的性能要求比较高
  2. 序列化数据储存, 对每一帧的数据保存快照
  3. 小重连:只有几帧的数据,客户端序列化关键帧数据,重连回滚
    1. 每收到一个正确的包会做一次关键帧序列化
    2. 每隔10s再做一次定时帧序列化
    3. 最多贮存三个定时帧序列化和一个关键帧序列化。每次断线重连的时候,就从当前时间往前找最近一份数据来做恢复加速(回滚)
    4. 客户端根据这些快照来更新各自的世界状态,通常会用插值”方法在两个相邻的快照间做平滑
  4. 大重连:序列化数据会做5秒的缓存,如果这段时间一直断线重连,则会一直复用该数据
    1. 客户端序列化异步储存磁盘,从磁盘里加载数据来做恢复

序列化和反序列化

核心:把所有属性写入buffer,或从buffer读取恢复

  1. 优先序列化目录:我们所有对象的一个表,我们需要先确保所有的对象都已经实例化出来了,后面在恢复对象属性的时候需要用到
  2. 已经死亡的对象,如果还有其他地方引用到,也需要序列化和反序列化,以保证逻辑正确
  3. 需要注意创建和删除对象时的副作用。比如在反序列化恢复的时候,添加对象进来的时候,会执行一个技能,这个技能会修改其他对象的一些属性,那么就污染到了其他人身上的一些变量。或者重连回来了结果战斗结束了,可以在战斗结束的时候,通过缓存战斗结果一段时间,等玩家重连回来后再把最后结果发送给他,直接返回到结算界面

一致性问题

  1. 浮点数计算:在不同机器上有不同的表现,由此,导致了浮点数的精度可能导致计算结果不一致
    1. 使用定点数:包含浮点数计算的常规算法,包含加、减、乘、除、绝对值、负运算等基本运算,另外,还要根据自己的使用实现开平方、指数函数、对数函数,三角函数
      • 在原来浮点数的基础上乘1000或10000,对应地方除以1000或10000,再辅以三角函数查表,能解决一些问题,减少计算不一致的概率。但有风险,例如一个int和一个float做乘法,如果原数值就要*1000,那最后算出来的数值,可能会非常大,有越界的风险
      • FP替代float,TSVector替代Vector
    2. 一致性数学工具:向量、矩阵、欧拉角、四元数,点、线、面、体,各种几何元素的关系,相交性检测等
    3. 一致性物理系统 :动力学和碰撞检测。Pyhsic.Raycast检测地面和围墙算出的浮点数可能会造成不确定性;尾数截断,按碰撞方向截断来保持一致性
    4. 一致性动画系统:需要实现逻辑层上的动画,时间要改成整数或者定点数,另外,插值数据的类型也得使用一致性得数据类型,比如位置向量等 
  2. 控制随机:举例玩家A的暴击几率是80%,假设在第200帧中,玩家A进行了一次平A攻击,客户端A计算得到这次攻击产生了暴击伤害300,而客户端B计算得到的结果是暴击伤害280。这就导致了伤害不一致,随着战斗的进行两边客户端的差距会越来越大,得到不同的战斗结果
    • 解决方案:客户端做伪随机算法。战斗开始时,服务器给客户端下发一个随机种子,通过自定义随机算法,保证每次的随机结果都是可控且一致的
  3. 指针参与计算
  4. 未重置上一局的静态变量
  5. 执行顺序不同
    1. 不用一些不稳定的排序例如Dictionary,或者不确定的算法排序,比如同样血量同样距离,客户端A返回A但客户端B返回B
    2. 逻辑部分不使用Coroutine
    3. 通过一个统一的逻辑Tick入口,来更新整个战斗逻辑,而不是每个逻辑自己去Update。保证每次Tick都从上到下,每次执行的顺序一致。
      • 比如我们项目中用到了很多第三方的插件(例如UGUI),这些插件的Update是没法由帧同步去控制的,各个客户端可能执行某个计算片段的时间并不是在同一逻辑帧内导致出现不同步的情况。unity的物理系统,动画系统也是如此。解决方案就是不使用第三方插件,使用成熟稳定的开源插件或者自己实现,保证每个update都是自主可控的。避免出现不可控导致的不同步的情况出现
  6. 全局里用了主观逻辑。比如先执行我的在执行他的,这样会造成不同的客户端会有不同的结果。所以一般都是按顺序,一号玩家和二号玩家以此类推
  7. 接受网络顺序不一致:UDP在出现网络波动的情况下,接收端接收到的消息是无序的,客户端在接收服务器发送的消息时,后发的消息可能会比先发的消息还要先接收到,这样就会造成客户端对于操作输入的顺序不一致,也是会造成最终的不同步的。
    • 解决方案:对每一个发出的消息做一个自增的编号,根据编号的连续性确定消息的顺序,就算先收到后面的消息,也可以等待前面的消息收到之后进行顺序传入游戏逻辑中

如何debug一致性

  1. 打内存log。每次战斗结束之后,把这两个玩家他们的内存日志做一个哈希,上报到服务器那边。然后去做一个对比,如果对比说不一致的话,那么两个客户端就把这份详细的内存日志压缩然后上报到后台,寻找bug

防作弊

核心:检验关键帧数据

  1. 服务端隔一段时间收集每个客户端指定帧的帧数据,进行对比。少数服从多数,出现异常的客户端要求重连
  2. 将客户端的逻辑实现抽象出来,服务端跑一个逻辑服务器,跟游戏进行同步运行或者结算的时候运行,关键状态数据以逻辑服上报的结果为主。

 

帧同步与状态同步对比

帧同步:

游戏中的操作同步。是一种客户端与服务器的同步方式,是为了实现高实时性,高同步性的应用而产生的。

  • 实时性
    • 所有玩家的指令一定是要及时地同步到所有玩家的终端上的。
    • 客户端发出指令到服务器需要时间,服务器发送指令到其他客户端也需要时间,发送消息的周期一定要短
    • 同步消息频率越大,对于性能要求越高,成本也就越高。
    • 能够减小服务器的压力,也为了能够更快地转发信息,游戏的逻辑一般会放到客户端去执行,这样更快
  • 同步性
    • 所有玩家收到的信息一定要是一致的
    • 客户端需要将指令同步后然后在固定的帧间隔内进行逻辑计算,保证每个客户端收到相同指令都会运行出唯一的结果
    • 为了应对玩家掉线的情况,服务器应该保存一场游戏中的指令,在玩家断线重连后发送到玩家终端。

状态同步:

游戏中的各种状态同步。一般的流程是客户端上传操作到服务器,服务器收到后计算游戏行为的结果,然后以广播的方式下发游戏中各种状态,客户端收到状态后再根据状态显示内容。

  • 不同玩家屏幕上的表现的一致性并不是重要指标, 只要每次操作的结果相同即可,状态同步对网络延迟的要求并不高。RPG用的比较多。
  • RPG的动画效果做的很华丽,放技能之前一般也有一个动画前摇,同时将攻击请求提交给服务器,等服务器结果返回时,动画也播放完毕,之后就是统一的伤害效果和结算。

 

Reference:

  1. https://www.youxituoluo.com/528021.html
  2. https://blog.uwa4d.com/archives/USparkle_frame-alignment.html
  3. https://mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg
  4. https://blog.csdn.net/zhang1461376499/article/details/116670361
  5. https://www.youxituoluo.com/528021.html
posted @ 2022-03-20 08:13  cancantrbl  阅读(1416)  评论(0编辑  收藏  举报