联网战斗同步优化

联网战斗同步之优化篇


回顾

在上篇文章,主要讲述

  1. 联网战斗的简介
    • 网络传输协议
    • 网络同步模型
    • 网络拓扑模型
  2. 实现联网战斗的方案
  3. 实现时的一些重点处理
  4. 实现后的一些优化改进畅想
  5. 我的感悟

更详细的内容,请跳转:联网战斗同步实现




正文

之前实现了一版联网战斗方案,还比较粗糙,存在许多不足的地方。
秉承着 先实现,再持续交付、快速迭代 的理念。
由于实践的效果不是很好,所以需要做一版优化。


优化方案

总览:

  1. 轮询机制都改为即时
  2. 帧推送间隔时间修改
  3. 建立帧缓存机制
  4. 扩充操作帧上传时机
  5. 完善的统计机制
  6. 自动化工具完善


轮询机制改为即时

在捋清各个流程步骤时,发现有些轮询机制,会导致延迟的增高。

  • 逻辑处的轮询
  • 协议收发的轮询

轮询的作用是减轻各模块功能的压力,降低成本。
我个人是觉得,在实现功能的时候,对于这种可控性成本需求,先按最优版本实现,做出我们最优效果。
之后,再根据成本等问题,进行打折简化。
我们做好对比数据的提供,交由项目负责人决策。


帧推送间隔时间修改

之前,为了让客户端压力小一些,处于“饥饿”状态,服务器推帧间隔设置为 35ms。
但是,在有些模块进行逻辑处理依旧以 30帧/秒(固定间隔 33.3ms)处理数据。
会导致部分客户端逻辑 与 实际有差异,造成抖动。

因此,将帧推送间隔时间修改为 33ms,减缓抖动。


建立帧缓存机制[重点]

基础方案,过于依赖网络状况。
显然这样是不现实的,所以需要做一套帧缓存机制,来应对网络的波动。

释放帧的时机:

  • 逻辑帧执行时
  • 绘制帧执行时
  • 逻辑帧执行时释放一次,绘制帧执行时根据需要补充释放

释放帧策略:

  • 释放一次,每次释放一帧
    • 可能导致帧积压,越来越卡
  • 释放一次,每次释放帧数量可变
    • 每次释放帧数量根据帧缓存池内帧数变化
  • 释放多次,每次释放数量可变
  • 缓存帧模式,缓存帧数量可配置
  • 缓存帧模式,缓存一帧
    • 如果缓存帧数量可配置,就需要做一个让所有配置相对平衡的策略;单做缓存一帧,可以更细致的制定策略
    • 但是经用户体验,发现这个策略并没有比大锅饭更好,甚至更差。。
  • 释放一次,每次释放所有帧

可配置缓存

第一版做的是可配置缓存。
缓存帧的数量可配,通过实际体验来决定具体缓存帧的数量。
(最开始设想是,先做成可配;之后再根据实际网络状况,动态修改配置缓存的帧数量,类似于流媒体领域的 JitterBuffer)
对于释放帧的数量方案是:

缓存帧数量为 n
帧池内帧数量为 m

  • 若 m <= (n+1), 释放1帧
  • 若 m <= (n+1) * 2, 释放2帧
  • 若 m <= (n+1) * 3, 释放3帧
  • 若 m > (n+1) * 3, 释放 (m-1)帧

流程图:

缓存N帧

缓存一帧

经过实际体验,缓存一帧是比较理想的,大于一帧的延迟,大家接受不了。
之前的释放帧策略是针对缓存n帧的普适规则,既然定下来缓存1帧,就做一个更为详细的策略。

先说结论再说步骤:
帧池内帧数量为 m

分为四个阶段

  1. 正常运行阶段
    • 0 < m <= 2, 每次释放1帧
  2. 大步调优阶段
    • m <= 5, 释放1次释放2帧
    • m <= 8, 释放1次释放3帧
  3. 小步快跑阶段
    • m <= 14, 进入快跑模式, 往后每次释放k帧, 直到恢复正常, k = math.ceil((m+1) / 2)
    • 当不足以释放k帧时,释放 k-1 帧, 退出快跑模式
  4. 瞬间传送阶段
    • m > 14, 释放 (m-1)帧

流程图:

缓存一帧

这个是怎么算出来的呢?
我列了一个表格,

列为:
帧池内帧数量 | 延迟时间 | 释放帧数量 | 释放后帧数量 | 下次释放帧时,帧池内可能帧数量 | 恢复正常所需要快播次数 | 往后每次释放帧数量

行为:
从1-15, 每帧33ms,15帧大约为 495ms

接下来开始填释放帧数量,依据以下原则:

  • 每次释放帧数量要保证在3帧内
  • 恢复正常所需要的快播数,保证在3次内
  • 尽可能平缓的恢复
  • 若无法满足,更倾向于保证3次快播,恢复正常

然后就开始填表格。
有一点遗憾的是,没有卡到300ms 和 500ms,都差一帧。


扩充帧上传时机

之前方案中,客户端收集完帧,上传时机在于执行逻辑帧前:先上传之前的操作,再去执行帧操作。
扩充为三个方案:

  • 执行逻辑帧前
  • 执行逻辑帧后
  • 执行绘制帧前


完善的统计机制 [重点]

我个人一直特别推崇数据驱动,数据不会说谎,除非数据收集的粒度与广度不够。
建立一套完善的统计机制,通过数据来分析用户状态、行为,进而辅助设计,优化体验,做到有依据有目的有验证标准的方式的去解决问题是非常必要的。

统计数值大概包括:

  • 基本信息
    • 机型
    • 操作系统
    • 位置信息
    • 连接的服务器位置
    • 战斗开始时间
    • 战斗类型
    • 掉线标记
  • 服务器项(分为两部分,客户端发往服务器&服务器发往客户端)
    • 丢包率
    • 重发率
    • 协议平均延迟
  • 客户端项
    • 快播率
      • 释放帧时帧数大于1帧
    • 卡死率
      • 释放帧时帧数等于0帧
    • 正常率
      • 释放帧时帧数等于1帧
    • 帧间隔值
      • 收到帧间隔
    • 帧间隔波动值
      • 距离帧应到时间的差值
    • PING值
    • FPS值
  • 帧操作的一生
    • 操作的产生
    • 操作的上传
    • 操作的下发
    • 操作的执行

针对每个数值,都可以包括更详细的项:

  • 最大值
  • 最小值
  • 阈值达到次数
    • 100%
    • 70%
    • 50%
  • 平均值

注意:

  • 计算平均值的时候,超出阈值的取阈值,避免一些极端数值影响平均值。
  • 这里我采用了python的 pandas模块去处理并分析数据


自动化工具

判定&收集不同步

首先对战斗产生日志分级存储:

  1. 【同步】帧处理,随机数,实体状态 相关
  2. 【部分同步】实体添加删除,BUFF添加删除等(包含1的所有内容)
  3. 【不同步】时间戳,其他同步信息打印(包含1,2)

注意:

  • 包含关系,越往下越详细,而且下一级的内容包含上一级的所有内容,即当一条信息在 2级日志内时,不会在1级日志内出现,但会在3的日志内出现。这样做方便定位更详细的日志的各级位置
  • 【同步】、【部分同步】、【不同步】 代表同样的战斗在不同客户端上的日志是否一致,【部分同步】表示不一致的内容只会在开头&结尾,不会在中间出现

1级日志内容会比较少(因为会在战斗中生成,每场战斗要重置),是重点的同步信息。这份日志主要是用来判断各客户端战斗是否同步使用的,同步判断包括两部分:

  • 随机数取值一致
  • 实体状态一致

2级日志,是用来辅助1级日志查找不同步问题,但是内容会相对3级日志更偏重联网战斗一些

3级日志,就是更广范围的日志,包含游戏其他功能模块的处理逻辑日志打印

不同步判定&收集:

  1. 战斗结束后,客户端战斗产生的1级日志内容,压缩为MD5,上传给服务器

  2. 服务器收集各客户端MD5,进行比较;判定战斗不同步即分版本存储本场战斗录像(所有的帧操作)

  3. 任意客户端可在debug模式中,拉取不同步的战斗进行回放现场

重现不同步

通过不同步收集,可以获取到不同步的战斗信息。

剩下的就是重现不同,这里采用的方式很简单,就是不断的跑这场战斗。

比如,跑100次战斗,将MD5不同的日志收集起来,进行比对,进而修改,直至这100场战斗的日志产生的MD5均一致。

其实,只要能重现BUG,就离解决不远了,而且是能无限重现的BUG。

其他项

  • 前期测试时期,可以让服务器把同步的战斗操作也存储,然后拉去下来本地跑100场。

  • 100场也只是一个样例,嫌多可以跑30场,200场,都随意。

  • 善于利用闲置电脑,让所有客户端都能替你跑不同步测试

    • 我是在游戏内开发一套debug工具,配合服务器,可以拉取各个角色的回放信息,并进行不同步测试。在战斗时,把自己带入为双方中的任何一方视角进行战斗并输出日志。
    • 后期也支持,只跑逻辑,不跑绘制的战斗,这样使得测试时间大大缩短。(还顺便检测出了一些BUG)



优化效果

通过最新一期的线上测试;大概近6000场战斗,总的不同步率 及 各战斗模式的不同步率 均已经降低到了 0.1%以下。(包含 1V1 PK,多人组队战斗等)。

可能还是战斗场次不够多,需要更大量的数据来检测,但是,起码目前已经到及格线,下一步就是调整优化延迟了。

联网战斗就是这样:

延迟调完调同步,同步调完调延迟。



下一步方向

现在已经算是一个闭环的方案了。

  1. 发现问题
  2. 重现问题
  3. 解决问题
  4. 验证问题
  5. 收集问题
    • 主动收集
    • 被动收集
  6. 提前发现问题

各个方面都有解决方案了,剩下的就是基于这个基础上再去不断的优化完善。

在不同步上,目前待测试的:

  • 浮点数问题

不知不觉已经做了这么多,可惜最后无法见证最终的效果,时也命也。

仅愿:

​ 功成须献捷,未必去经年。




番外

这些是在优化过程中遇到的一些问题,采用的一些方法,一些策略


番外:痛苦的不同步

每次接到不同步的反馈,都是异常痛苦的,就如之前所说。
同步模块做的很多的事情都是大方向框架性的,具体的流畅问题、不同步问题,往往是负责最繁琐复杂的发现问题的角色,
然后带着发现的问题找相应模块负责人,去反馈。(最麻烦的就是发现问题,一旦定位问题,距离解决基本不远了)

但是,
同步方面的问题,往往是上线前无人问津,上线后铢锱必较。
一旦出了问题,虽然能把锅分分钟甩出去,但是最终留下来加班修改的还是自己;所以,解决这个问题才是关键。

分析

上线前为什么会无人问津?

  • 代码尚在修改,测试收效甚微
  • 数据尚未到位,测试范围不足
  • 待到代码和数据双双齐备,距离上线不足弹指一挥间,甚至上线后依然在修改调整
  • 测试方法繁琐复杂

解决

  • 针对代码修改问题;可以提高版本内相关功能优先级,先修改相关代码,再做其他相关功能
  • 针对数据到位问题;可以分批次到位,分批次测试,而非最终一口吃个胖子
  • 针对测试方法繁琐复杂;可以实现自动化测试,提供测试工具

自动化测试,本不应该属于这片内容。
作为联网战斗的实现,处理同步问题,是重中之重。
处理方向有两个:

  • 预防不同步,在出现前,扼杀在摇篮中
  • 解决不同步,当出现时,如何快速定位并解决

预防不同步,就是使用自动化测试的方案,针对策划所配置的所有项都提前做好测试。做一个改动后,就相关影响的战斗,都多跑几遍;就是把人力做的事情,通过技术来自动化跑。

解决不同步,可以分为三个部分:

  1. 收集
  2. 重现
  3. 验证

收集,就是当出现不同步问题的时候,我们一定要知道;

重现,就是我们要对现场进行重现,这样就方便查找问题,进而验证是否解决问题。

这些通过上面优化方案的自动化工具都已经初具模型。


番外:集思广益

以前做东西,习惯闷头开发,因为为了做这方面的内容,查阅资料,实验功能,做了各种各样的努力,对这方面的认知和理解还是有一定的把控的。

但是,“不识庐山真面目,只缘身在此山中”,有时候往往会自己把自己限制住,对一个问题不同高度,不同角度,不同层次的解析,也是值得尝试的。

于是,这次优化,准备了以下内容,然后先在内部范围开了个会,收集大家的意见:

  1. 整个机制的框架
  2. 上次测试的数据
  3. 针对测试数据分析的原因
  4. 针对分析的原因,准备做的优化方向

事实证明,通过这次会议,收集到了很多有用的建议与方案,回去做了针对性的调整,再去实施。


番外:延迟与平滑的博弈

联网战斗的效果,最终都是归咎与延迟与平滑的博弈,效果平滑,完全可以通过最粗暴的高延迟实现,但是动作游戏的高延迟还是比较影响玩家体验的,所以就需要不断的调整优化,找到那个平衡点。

下面是一个经典的样例:

  1. 以当前帧池数量 n 为准,差值释放,假定平均3次释放完毕,每次释放 n/3
  2. 注重延迟,当需要快播,每次选择最大释放帧数释放(3帧),保证3次释放即可恢复(100ms内)
    • 假设帧池中有5帧,选择
      1. 释放3帧 => 5-3=2 ,已恢复现场
  3. 注重平滑,当需要快播,每次选择尽可能平滑的方式去释放帧
    • 假设帧池中有5帧,选择
      1. 释放2帧 => 5-2=3
      2. 释放2帧 => 3-2=1 ,已恢复现场
  4. 平衡平滑与延迟,指定每个数值缓存帧时的缓存策略,

注意:

  • 快播时,是否重新计算该释放的帧数


番外:电脑与收集MD5不一致

在重现不同步时,手机产生的MD5与PC产生的MD5不一致。

经过一系列实验检测发现,在时间种子传输过程中(Lua -> C++),种子的数据类型发生变化,导致种子数值发生了改变。


番外:卡顿反馈

为了更近一步手机玩家反馈,同时也不影响玩家正常体验。

在联网战斗后,结算界面添加反馈按钮,收集第一手玩家信息。

然后,设定一个卡顿阈值来自动弹出反馈面板(该面板允许玩家选择永久不自动弹出)。





参考: