为啥选择帧同步

和写单机游戏类似,客服端收集自己的指令操作发送到服务器,服务器进行收集广播给所有的玩家,客服端本地通过收到的包来推进游戏进度

服务器:每隔一段时间收集客服端的操作,发给客服端,然后继续采集下一次的操作,在发给客服端

客服端:收到服务器的广播下来的操作—->计算逻辑—>采集自己操作上报服务器

帧同步适用于实时性要求高,人数较少的情况

帧同步服务器每隔多久同步一次比较合适呢

上限: 网络传输时间,比如我们的ping百度网站你得到的时间是10ms 那么 1000/10 100

Typoraimage-20220220231415930

下限:下发给玩家的速度,也就是玩家的体验,科学数据玩家在50 ms-100ms 之间人不会感觉到卡顿认为比较流畅

那么就是[1000/50,1000/100]—>[20,10] 所以王者荣耀一般取中间值15fps 也就是1秒15帧 一帧 1000/15 66ms

算下带宽是多少,承受的了吗

假设5000个人,按一个房间10人,那么就得500个房间

假设**1秒的数据, 一帧我们每人16个字节的指令数据,那么16 * 10 * 15 * 500** —>1,200,000Byte –> 1172KB —> **1MB**带宽,对于现在的服务器完全是可以承受的

选用udp还是tcp

tcp :准确 丢包重传

通常tcp也能做到做帧同步,但是很难因对网络波动,因为假如tcp有条链路发送了一个1号包过去,这个时候因为网络波动这个链路网速下降,这个时候可能会触发重传,在发送2号包过来的时候,就可能会卡主2号包,从而导致客服端收不到包数据

Typoraimage-20220220233319776

udp:高效,可能丢包,乱序 假设1号包卡主了,那么udp不会去等待1号包是否发完,在下一次需要发送的时候就会再次通过新的链接发送2号包过去

Typoraimage-20220220233337319

基于可靠传输的UDP,是指在UDP上加一层封装,自己去实现丢包处理,消息序列,重传等类似TCP的消息处理方式,保证上层逻辑在处理数据包的时候,不需要考虑包的顺序,丢包等。类似的实现有Enet,KCP等。

冗余信息的UDP,是指需要上层逻辑自己处理丢包,乱序,重传等问题,底层直接用原始的UDP,或者用类似Enet的Unsequenced模式。常见的处理方式,就是两端的消息里面,带有确认帧信息,比如客户端(C)通知服务器(S)第100帧的数据,S收到后通知C,已收到C的第100帧,如果C一直没收到S的通知(丢包,乱序等原因),就会继续发送第100帧的数据给S,直到收到S的确认信息。

一旦客户端没收到服务端已确认其发送的数据,就会一直重传直到服务端确认为止

发送者维持一个发送队列,对每一次发送进行编号。每一次发送时,会将待发送的数据写入队列。然后将队列里的数据+编号发送给接收者。

接收者收到数据后,会将该编号回送给发送者以确认。发送者收到确认编号后,会将该编号对应的数据包从队列中删除,否则该数据仍保存在发送队列中。

下次发送时,会有新的数据进入队列。然后将队列中的数据+最新的编号发送给接收者。以此循环反复

image-20221103153555980

上图解析:

  1. 第1次发送,在发送队列里只有Data1,于是将Data1和编号1(Seq=1)发送给接收者。收到确认编号1(Ack=1)后,将Data1从队列中删除。
  2. 第4到7次发送,由于从第4次发送开始就没有收到确认编号,于是队列中包含了Data4到Data7。第7次发送后,收到确认编号6,于是将Data4至Data6从队列中删除。
  3. 第8次发送,队列中包含Data7和Data8。发送后收到确认编号8,从而将Data7和Data8从队列中删除。

以上的关键点是,发送者未收到确认编号,并不一直等待,而是会继续下一次发送。结合图1:

  1. 如果发送者是服务器,则会每隔66MS会将一个Frame数据写入发送队列,然后将该队列里的所有Frame数据一起发送给客户端 。
  2. 如果发送者是客户端,则会在玩家有操作时,将玩家的每一个OperateCmd数据写入发送队列,然后将该队列里的所有OperateCmd数据一起发送给服务器 。如果发送队列不为空,则每隔99MS重复发送。如果发送队列为空,则不再发送。直到玩家下一次操作。
  3. 由于服务器和客户端即是发送者,又是接收者。则服务器和客户端的每一次发送,除了会带上该次发送的编号,还会带上对对方发送编号的确认。

区别

但是这两种方式,区别是巨大的。可靠传输的UDP,在帧同步中,个人认为是不合适的,因为他为了保证包的顺序和处理丢包重传等,在网络不佳的情况下,delay很大,将导致收发包处理都会变成类似tcp的效果,只是比TCP会好一些。必须要用冗余信息的UDP的方式,才能获得好的效果。并且实现并不复杂,只要和服务器商议好确认帧和如何重传即可,自己实现,有很大的优化空间

帧同步运行前提

  1. 输入一致性

    • 确定性的碰撞,寻路结果
    • 玩家操作顺序唯一
  2. 计算要一致性

    • 服务器针对每个单局游戏开局时间生成随机数种子,逻辑帧计算均为伪随机

    • 浮点数采用多个定点数保存和运算,确保浮点数计算结果一致

    • 每个网络包都包含自增序号,具体算法可根据项目自行定义

    • 严格控制静态变量(全局变量)的使用

    • 禁止使用不稳定的排序算法

    • 禁止使用顺序不确定的数据结构

    • 尽量不使用非开源的第三方库(无法确定第三方库中是否有上述的结果不一致算法)

    • 多线程问题

      主要是每个客服端多线程算出结果可能会不一致,比如A客服端用了3个线程来算你 这个数据,b客服端用1个线程来算你这个数据导致最后A和B的结果不一致,除非你 能保证最后结果一致,要不最好别用

    • 协程 (Coroutine)内写逻辑带来的不确定性也要注意

  3. 开始演示代码前,要保证运行的多个客服端代码版本要一致,如果因为版本不一致导致运行结果不一样,然后查了很久bug那就太2了,如果当前线上存在多个版本,则只有同版本玩家可匹配到单局游戏中

  4. c#的dictionary遍历的时候是无序的,这个要注意,很容翻车

  5. 如果同批发送的包比较多,尽量合并,减少包头信息的冗余

  6. 业务层上面尽量减少数据结构包的大小

  7. 逻辑帧的规则

    收到第N帧,只有当我收到第N+1帧的时候,第N这一帧我才可以执行。 服务器会按照一定的频率,不同的给大家同步帧编号,包括这一帧的输入带给客户端,如果带一帧给你的数据你拿到之后就执行,下一帧数据没来就不能执行,它的结果就是卡顿。

战斗流畅保证方法

  1. 逻辑帧保证在15-18帧上下

    数据包冗余发送,发送数据量较少的当前帧时,可以把前几帧数据合并发送

  2. 渲染帧保证在30帧以上

    常见的客户端预测,客户端插值,服务器延迟补偿方法保证客户端画面流畅

帧同步流程

image-20221015152041408

服务器:

  1. 服务器的每个比赛对象,都有一个成员frameid 保持了当前的比赛,下一帧要进入的id,frameid =1

  2. 我们在服务器上定义了一个数据结构 match_frames 用来保存我们所有玩家每帧的操作

    保存match_frames这个结构的作用:

    • 录像回放

    • 断线重连

    • 在不同步的情况下,看看有没有作弊

    • udp丢包时序问题,丢包的时候需要补发给客服端

  3. next_frame_opt 每帧服务器采集到的客服端操作

    next_frame_opt = {frameid ,{1号操作玩家指令,2号操作玩家指令,3号操作玩家指令,4号操作玩家指令, ..}}

  4. 服务器启动定时器 每隔66ms触发一次 on_logic_frame

  5. 保存我们当前的操作到match_frames

  6. 遍历每个玩家,给每个玩家发送我们的帧操作

  7. 服务器进入下一帧 frameid = frameid + 1

  8. 服务器进入采集下一帧的操作,清空上一帧采集到的客服端操作:也就是把next_frame_opt清空

    next_frame_opt = {frameid ,{}}

  9. 发送服务器认为这个玩家还没有同步的帧,每个玩家对象记录了一个变量 sync_frameid 用来记录这个客服端已经同步了多少帧

    同步的帧: sync_frameid + 1 TO 最新的帧 —>主要是用来解决udp丢包和时序问题

  10. 采用udp 将我们需要补发的帧同步给客服端[sync_frameid + 1,最新帧]

客服端:

  1. 通过网络受到网络受到帧同步的数据包以后,

  2. 每个客服端也会有一个sync_frameid,用来记录一下你这个客服端真正已经同步到那个帧了

  3. 如果收到的帧id小于客服端的帧id,那么直接丢弃这个帧

    • 为什么会出现需要丢弃这个帧的情况

      因为udp 时序问题 :有先发后到,后发先到的可能

    • 为什么我们没有收到99帧,可以开始处理100帧,还能同步吗

      99帧没有处理,服务器发100帧的时候回补发99帧

  4. 如果上一帧的操作不为空,那么这个时候,我们在处理下一帧之前,一定要先同步下上一帧的结果

    客服端A:|….|..66.3.|….|

    客服端B:|….|…66.2.|….|

    在播放动画的帧与帧之间,我们会出现时间的差异,会导致位置不同步,

    logic_pos 66ms –>统一用66ms来计算新的位置和结果

    客服端A:|….|…66.|….|

    客服端B:|….|…66.|….|

    每帧都同步,处理下一帧之前,每帧都要同步,同样的输入—>同样的输出

  5. 跳帧 快速的同步完过时的帧,直到最新的帧

  6. 控制我们的客服端,来根据操作,来播放动画,更新我们的逻辑推进,创建怪物,防御塔,等等逻辑

  7. 采集你自己的操作,上报给客服端

服务器:

  1. 收到玩家的操作,更新服务器上认为玩家已经处理的帧id

    98处理完–>99, 服务器发99帧->客服端—> 处理完99帧,客服端收集100帧操作,服务器收到100帧操作 100 - 1已经同步完了,这个时候就吧98变成99帧也就是 变成98–>99

  2. 如果收到玩家操作的帧id,next_frame_opts.frameid 等于马上要触发的帧id,说明收到了玩家过时的操作

    假设服务器已经处理完99帧,马上要下发100帧了,这个时候客服端还上传99帧,那么可以认为玩家因为网络或者特殊原因发送了过时的操作,所以直接丢弃

    • 这样丢弃会影响玩家的手感吗

      丢帧肯定会影响玩家的手感,但是基本不影响玩家操作,15fps,按一个按钮基本4次,中间丢一帧,不太会影响玩家整体,基本玩家感受不出来。

  3. 保存玩家的操作,等待下一帧的触发,goto到逻辑4

如何克服udp的时序和丢包问题

客服端: 丢包, 晚到,服务器会补发丢掉的帧

服务器: 丢包, 没有太多的影响, 下一帧马上就可以处理

防外挂

  1. 视野外挂

    • 划分地图区域
    • 玩家信息分层
    • image-20221101223315472
  2. 属性外挂 多客户端状态校验,客户端执行完每个逻辑帧后,根据游戏的状态计算出一个Hash值,用其标定一个具体的游戏状态。不同客户端通过对比这个值,即可判断客户端之间是否保持同步

  3. 数据的加密处理

  4. 输入的合理性检测

  5. 服务器运行一个精简的可信赖的客户端环境,得到可信赖的数据

  6. 反外挂是一个很大的议题。帧同步结构中,所有数据都在玩家本地,理论上玩家可以任意修改这些数据。这里不讨论传统的加壳及反调试技术。这里讨论在实际开发中,帧同步框架能够通过什么方法来解决该问题。框架能提供至少3种保护: a. 关键数据保护,b. 虚拟化, c. 服务器后验证。关键数据保护可以有很多技术,框架对核心数据,可以做内存加密,内存多拷贝冗余保护等。框架提供虚拟化技术,也是一个不错的选择,部分代码可以在虚拟机(lua)中直接执行,破解难度会增加(前提是资源保护足够)。服务器后验证是杀手锏,验证服务器能运行游戏录像,并直接得出游戏战斗结果,任何作弊都无所遁形。

    因此对于帧同步,反外挂相对是一件比较容易的事情。游戏过程中,玩家作弊只会影响到自己,不会影响到他人。游戏结算时,当服务器检测到玩家之间游戏结果不一致时,通过验证服务器,对游戏录像进行验证计算,很容易就能发现是哪个玩家发生了作弊。

怎么优化卡顿的问题

  1. buffer缓存

  2. 本地插值平滑加逻辑与表现分离

  3. 使用UDP(在手机环境下,弱网的情况下,TCP很难恢复重连)

  4. 服务端Sleep(1),并不是代表休息1ms,具体精度看操作系统。(windows约15ms,linux约1ms)

    要在windows上测试需要将全局设置高精度计时器 timeBeginPeriod(1),有些别的软件开启时会设置全局的精度。

    当调用Sleep(1)时,CPU会进入睡眠状态,以节省电量,因此,如果CPU处于睡眠状态,操作系统(OS)如何唤醒你的线程?答案是硬件中断。操作系统对计时器芯片进行编程,然后该计时器芯片触发中断以唤醒CPU,然后操作系统可以调度线程

    计时器中断之间的间隔取决于Windows版本和你的硬件,但在我最近使用的每台计算机上,默认间隔为15.625毫秒(1,000毫秒除以64)。这意味着,如果你在某个随机时间调用Sleep(1),那么将来每当下一个中断触发时(或者如果下一个中断过早,则在此之后触发),你可能会在1.0毫秒至16.625毫秒之间的某个时间被唤醒。

  5. 最近用ET框架遇到的问题 NLOG没有开启异步日志模式导致的周期性卡顿

逻辑帧率是否越高越好

并非如此,建议15帧/秒。

逻辑帧率增加带来的影响如下:

  1. 逻辑层计算次数增多,cpu消耗越大。
  2. 网络流量消耗越多
  3. 对服务器CPU和带宽压力增大

利用预测回滚,客服端插值,对抗高延迟

预测

预测就是将玩家的输入立即应用到本地状态,而无需等待服务端返回。

如果玩家的每一次操作如果都要等到服务端确认后才能生效,那么延迟将是不可避免的。解决方案就是:玩家做出任何操作后,立刻将输入应用到本地状态,并刷新表现层显示。例如按下了 “右”,那么就立即向右移动,而无需等待服务端返回,效果如图。

图片

现在,操作的延迟消失了。你按下 “左” 或者 “右” 都可以得到立刻的反馈。

但问题似乎并没有完全解决,在移动过程中,你总是能感到来回的 “拉扯” 或者位置抖动。这是因为你在执行本地预测的时候,也在接收来自服务端的同步,而服务端发来的状态总是滞后的。

例如:

  1. 你的坐标是 (0,0)
  2. 你发出了 2 个 右移 指令(每次向右移动 1 个单位),服务器尚未返回,执行本地预测后,坐标变为 (2,0)
  3. 你又发出了 2 个 右移 指令,服务器尚未返回,执行本地预测后,坐标变为 (4,0)
  4. 服务端发回了你的前 2 个右移指令:从 (0,0) 执行 2 次右移,坐标变为 (2,0),被拉回之前的位置

由于延迟的存在,服务端的同步总是滞后的,所以你总是被拉回之前的位置。如此往复,就是你在图中看到的抖动和拉扯。

归根到底,是服务端同步过来的状态与本地预测的状态不一致,所以我们需要 “和解” 它们。

和解

和解就是一个公式:预测状态 =权威状态 + 预测输入

重要

和解的概念最难理解,但也是实现无延迟感体验最重要的一步。你可以先简单记住上面的公式,应用到项目中试试看。

权威和预测

一般我们认为服务器总是权威的,从服务端接收到的输入称为 权威输入,经权威输入计算出来的状态称为 权威状态。同样的,当我们发出一个输入,但尚未得到服务端的返回确认时,这个输入称为非权威输入,也叫 预测输入

在网络畅通的情况下,预测输入迟早会按发送顺序变成权威输入。我们需要知道发出去的输入,哪些已经变成了权威输入,哪些还是预测输入。在可靠的传输协议下(例如 WebSocket)你无需关注丢包和包序问题,所以只需简单地对比消息序号即可做到。

和解过程

在前述预测的基础上,和解就是我们处理服务端同步的状态的方式。如果使用的是状态同步,那么这个过程是:

  1. 收到服务端同步来的 权威状态
  2. 将本地状态立即设为此权威状态
  3. 在权威状态的基础上,应用当前所有 预测输入

如果使用的是帧同步,那么这个过程是:

  1. 收到服务端同步来的权威输入
  2. 将本地状态立即 回滚上一次的权威状态
  3. 将权威输入应用到当前状态,得到此次的 权威状态
  4. 在权威状态的基础上,应用当前所有 预测输入

由此可见,状态同步和帧同步只是网络传输的内容不同,但它们是完全可以相互替代的 —— 最终目的都是为了同步权威状态。

例子

这有用吗?我们回看一下上面预测的例子,有了和解之后,会变成怎样:

  1. 你的坐标是 (0,0)

  2. 你发出了 2 个 右移 指令(每次向右移动 1 个单位),服务器尚未返回

    • 权威状态:(0,0)
    • 预测输入:右移#1 右移#2
    • 预测状态:(2,0) (权威状态 + 预测输入)
  3. 你又发出了 2 个 右移 指令,服务器尚未返回

    • 权威状态:(0,0) (未收到服务端同步,不变)
    • 预测输入:右移#1 右移#2 右移#3 右移#4
    • 预测状态:(4,0) (权威状态 + 预测输入)
  4. 服务端发回了你的前 2 个右移指令 (帧同步)

    • 上一次的权威状态:(0,0)
    • 权威输入:右移#1 右移#2
    • 权威状态:(2,0) (上一次的权威状态 + 权威输入)
    • 预测输入:右移#3 右移#4#1#2 变成了权威输入)
    • 预测状态:(4,0) (权威状态 + 预测输入,之前的拉扯不见了

看!虽然服务端同步来的权威状态是 “过去” 的,但有了和解之后,拉扯问题解决了,效果如图:

图片

预测 + 和解处理本地输入是非常通用的方式。你会发现,在没有冲突时,网络延迟可以完全不影响操作延迟,就跟单机游戏一样!例如上面移动的例子,如果不发生冲突(例如与它人碰撞),即便网络延迟有 10 秒,你也可以毫无延迟并且平滑的移动。这就是在有延迟的情况下,还能实现无延迟体验的魔术。

冲突

那么冲突的情况会怎样呢?比如上面的例子,你发送了 4 次移动指令,但在服务端,第 2 次移动指令之后,服务端插入了一个新输入 —— “你被人一板砖拍晕了”。这意味着,你的后两次右移指令将不会生效(因为你晕了)。那么该过程会变成这样:

  1. 你的坐标是 (0,0)

  2. 你发出了 2 个 右移 指令(每次向右移动 1 个单位),服务器尚未返回

    • 权威状态:(0,0)
    • 预测输入:右移#1 右移#2
    • 预测状态:(2,0)
  3. 你又发出了 2 个 右移 指令,服务器尚未返回

    • 权威状态:(0,0)
    • 预测输入:右移#1 右移#2 右移#3 右移#4
    • 预测状态:(4,0)
  4. 服务端发回了你的前 2 个右移指令

    • 权威状态:(2,0)
    • 预测输入:右移#3 右移#4#1#2 变成了权威输入)
    • 预测状态:(4,0)
  5. 服务端发回了与预期冲突的新输入

    • 上一次的权威状态:(2,0)
    • 权威输入:你被拍晕了 右移#3 右移#4
    • 权威状态:(2,0) (因为先被拍晕了,所以后两个右移指令无效)
    • 预测输入:无 (所有预测输入都已变为权威输入)
    • 预测状态:(2,0)

此时,之前的预测状态 (4,0) 与最新的预测状态 (2,0) 发生了冲突,客户端当然是以最新状态为主,所以你的位置被拉回了 (2,0) 并表现为晕眩。这就是网络延迟的代价 —— 冲突概率。

插值

插值指在表现层更新 “其它人” 的状态变化时使用插值动画去平滑过渡。

到目前为止,我们已经获得了自己在本地无延迟的丝滑体验。但在其它玩家的眼中,我们依旧是卡顿的。这是由于同步帧率和显示帧率不一致导致的,所以我们在更新其它人的状态时,并非一步到位的更新,而是通过插值动画来平滑过渡。

预测+和解是解决 自己 的问题,发生在 逻辑层;插值是解决 其它人 的问题,发生在 表现层

例如上面的例子,显示帧率是 30fps,服务端的同步帧率是 3 fps。收到服务端同步的其它玩家的状态后,不是立即设置 node.position,而是通过 Tween 使其在一个短暂的时间内从当前位置平滑移动到新位置。如此,在其它玩家眼中,我们看起来也是平滑的了:

图片

解决快节奏有冲突的同步,就是 预测、和解、插值 这 3 个核心思想,掌握了它们你应该就能举一反三,轻松应对各种场景。

延迟不影响操作

从上面的几个例子中,我们可以得出几个重要的结论:

  • 在无冲突时,网络延迟并 不会 影响操作延迟,预测+和解能实现本地 零延迟 的操作体验
  • 发生冲突时,本地状态立即重设到最新状态,画面跳变,只有此时玩家能明显感受到 “卡了”
  • 网络延迟影响的是冲突概率:网络延迟越大,发生冲突的可能性越大

当使用了预测 + 和解之后,我们之前认为的 “网络延迟越大操作延迟越大”,就变成了一个 误解

即便是一个 MOBA 游戏,你在打野,另外一名玩家在刷兵线 —— 你们之间不存在 “冲突” 的可能性。此时即便网络有很大延迟,你们各自的游戏体验也应该都是单机游戏般 零延迟 的!只有当你们在打团战时,才可能出现因为网络延迟导致技能判定等冲突;也只有当冲突出现时,你们才能直观感受到延迟的存在。

延迟越小越好吗

服务端可以在收到客户端输入后立即广播出去,也可以通过 LockStep 的方式固定同步频率。除了网络之外,同步频率也会影响延迟。比如服务端逻辑帧率每秒同步 10 次,那么意味着即便局域网内也可能出现 100ms 的延迟。

但网络延迟真的越低越好吗?其实,延迟小也有一个副作用:插值不平滑。

假设你用 1 秒时间从 A 点匀速移动到 B 点,如果同步频率恰好是每秒 1 次,那么通过插值,其它玩家看到的应该是一个完全匀速的移动过程。但如果同步频率是每秒 60 次呢?理论上每 16ms 你就会收到一个新状态,然后每 16ms 就要更新一次插值动画。但就跟延迟一样,网络抖动也是客观存在的 。你大概率不是均匀的每 16ms 收到一次消息,而是很可能时而 200ms 才收到一条消息,时而 20ms 内就收到 N 条消息。如此,其它玩家看到的移动过程将是忽快忽慢的,这种不平滑的动画会带来直观的卡顿感。

所以,延迟并非越小越好,这也是一个权衡利弊的过程:

  • 延迟大 :插值更平滑,冲突概率更大
  • 延迟小 :插值不平滑,冲突概率更小

延迟和同步频率在多少是最好的呢?这个没有标准答案,应该根据实际玩法需要权衡利弊后决定。

有延迟下的判定

在有延迟的情况下,技能命中的判定,该听谁的呢?来看一个简单的例子。

场景举例

在一片空地上,你拿起狙击枪瞄准一个正在移动的敌人头部。点下鼠标,一发弹道闪过 —— 你很确定,命中了!然而,由于网络延迟的存在,你看到的敌人,实际上是 200ms 以前的位置。在服务端的视角看来,你开枪的时刻敌人已经走远 —— 你打空了。那么此时,应当如何判定呢?我们分别来看看。

假设我们选择以 服务端 的判定为准,那么你会很不爽。因为在你看来,明明打中了,敌人却没掉血,那对面肯定是开挂了。理论上,对面会很爽,因为服务端保护了他免于受伤。但事实上他没什么可开心的,因为他完全不知道服务端为他做了什么,他只会觉得 “对面真菜” 。

那如果我们选择以 客户端 的判定为准呢?当然你会很爽,因为判定结果和你的预期一致,你觉得这个游戏丝滑流畅没延迟,爽爆了。理论上对面会不爽,因为从服务端视角来看,其实你没打中他。但事实上他并不知道实际上发生了什么,他只会觉得是你枪法不错,打中了他。虽然被打中了,但对于他而言,游戏体验是流畅和符合预期的,没什么不爽。

所以看起来听客户端的大家都开心,那么是不是这样就万无一失了呢?也存在例外。

假如对面不是在空地上跑,而是躲进了一堵墙后面。此时他认为自己是安全的,但由于网络延迟,你这边依旧判定打中了他。此时在墙后的他仍然受到了伤害,他肯定很不爽,要么是网卡了要么是你开了穿墙挂。所以并没有 100% 完美的解决方案,权衡利弊后,如果你觉得出现这种情况的概率比较小可以接受,那么可以选择以客户端判定为准从而带来更好的游戏体验。

你也可以在客户端发送输入时带上游戏时间,由服务端根据实际延迟来决定由谁判定。比如延迟在 200ms 以内时由客户端判定,否则由服务端判定。

断线重连

下面方法从低级到高级递增

方法1:在玩家登录的时候把所有历史帧的数据发送给客户端,让客户去进行追帧

方法2:客户端记录当前已经跑到了第几帧,把当前帧数据和状态值数据存入本地文档,并定时把文档数据MD5发送到服务器进行对比验证合法性,然后当断线重连的时候服务器把客户端的已经执行到的最新帧数发送到服务器进行请求,也就是只请求(客服端断线是最新帧,服务器现在跑到的帧数)发送给客户端进行追帧,极端情况下玩家在玩的途中,换新手机这个时候客户端已经没有本地存档了,那么我们就应该在服务器建立一个通过指令跑出断线客户端执行到了第几帧,如果能算出内存数据一下发给客户端最好,这也是方法3

方法3: 服务器ECS进行数据分层,把客服端收集到的指令进行转换,存入C,当客户端断线重连的时候,可以把C里面的状态数据发给客户端,让客户端进行恢复,这样客户端也不需要在进行追帧操作,客户端也能很快的进入游戏,卡的关键点也只会是客户端的资源加载进度