联网战斗同步之优化篇
回顾
在上篇文章,主要讲述
- 联网战斗的简介
- 网络传输协议
- 网络同步模型
- 网络拓扑模型
- 实现联网战斗的方案
- 实现时的一些重点处理
- 实现后的一些优化改进畅想
- 我的感悟
更详细的内容,请跳转:联网战斗同步实现
正文
之前实现了一版联网战斗方案,还比较粗糙,存在许多不足的地方。
秉承着 先实现,再持续交付、快速迭代 的理念。
由于实践的效果不是很好,所以需要做一版优化。
优化方案
总览:
- 轮询机制都改为即时
- 帧推送间隔时间修改
- 建立帧缓存机制
- 扩充操作帧上传时机
- 完善的统计机制
- 自动化工具完善
轮询机制改为即时
在捋清各个流程步骤时,发现有些轮询机制,会导致延迟的增高。
- 逻辑处的轮询
- 协议收发的轮询
轮询的作用是减轻各模块功能的压力,降低成本。
我个人是觉得,在实现功能的时候,对于这种可控性成本需求,先按最优版本实现,做出我们最优效果。
之后,再根据成本等问题,进行打折简化。
我们做好对比数据的提供,交由项目负责人决策。
帧推送间隔时间修改
之前,为了让客户端压力小一些,处于“饥饿”状态,服务器推帧间隔设置为 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帧的普适规则,既然定下来缓存1帧,就做一个更为详细的策略。
先说结论再说步骤:
帧池内帧数量为 m
分为四个阶段
- 正常运行阶段
- 0 < m <= 2, 每次释放1帧
- 大步调优阶段
- m <= 5, 释放1次释放2帧
- m <= 8, 释放1次释放3帧
- 小步快跑阶段
- m <= 14, 进入快跑模式, 往后每次释放k帧, 直到恢复正常, k = math.ceil((m+1) / 2)
- 当不足以释放k帧时,释放 k-1 帧, 退出快跑模式
- 瞬间传送阶段
- m > 14, 释放 (m-1)帧
流程图:
这个是怎么算出来的呢?
我列了一个表格,
列为:
帧池内帧数量 | 延迟时间 | 释放帧数量 | 释放后帧数量 | 下次释放帧时,帧池内可能帧数量 | 恢复正常所需要快播次数 | 往后每次释放帧数量
行为:
从1-15, 每帧33ms,15帧大约为 495ms
接下来开始填释放帧数量,依据以下原则:
- 每次释放帧数量要保证在3帧内
- 恢复正常所需要的快播数,保证在3次内
- 尽可能平缓的恢复
- 若无法满足,更倾向于保证3次快播,恢复正常
然后就开始填表格。
有一点遗憾的是,没有卡到300ms 和 500ms,都差一帧。
扩充帧上传时机
之前方案中,客户端收集完帧,上传时机在于执行逻辑帧前:先上传之前的操作,再去执行帧操作。
扩充为三个方案:
- 执行逻辑帧前
- 执行逻辑帧后
- 执行绘制帧前
完善的统计机制 [重点]
我个人一直特别推崇数据驱动,数据不会说谎,除非数据收集的粒度与广度不够。
建立一套完善的统计机制,通过数据来分析用户状态、行为,进而辅助设计,优化体验,做到有依据有目的有验证标准的方式的去解决问题是非常必要的。
统计数值大概包括:
- 基本信息
- 机型
- 操作系统
- 位置信息
- 连接的服务器位置
- 战斗开始时间
- 战斗类型
- 掉线标记
- 服务器项(分为两部分,客户端发往服务器&服务器发往客户端)
- 丢包率
- 重发率
- 协议平均延迟
- 客户端项
- 快播率
- 释放帧时帧数大于1帧
- 卡死率
- 释放帧时帧数等于0帧
- 正常率
- 释放帧时帧数等于1帧
- 帧间隔值
- 收到帧间隔
- 帧间隔波动值
- 距离帧应到时间的差值
- PING值
- FPS值
- 快播率
- 帧操作的一生
- 操作的产生
- 操作的上传
- 操作的下发
- 操作的执行
针对每个数值,都可以包括更详细的项:
- 最大值
- 最小值
- 阈值达到次数
- 100%
- 70%
- 50%
- 平均值
注意:
- 计算平均值的时候,超出阈值的取阈值,避免一些极端数值影响平均值。
- 这里我采用了python的 pandas模块去处理并分析数据
自动化工具
判定&收集不同步
首先对战斗产生日志分级存储:
- 【同步】帧处理,随机数,实体状态 相关
- 【部分同步】实体添加删除,BUFF添加删除等(包含1的所有内容)
- 【不同步】时间戳,其他同步信息打印(包含1,2)
注意:
- 包含关系,越往下越详细,而且下一级的内容包含上一级的所有内容,即当一条信息在 2级日志内时,不会在1级日志内出现,但会在3的日志内出现。这样做方便定位更详细的日志的各级位置
- 【同步】、【部分同步】、【不同步】 代表同样的战斗在不同客户端上的日志是否一致,【部分同步】表示不一致的内容只会在开头&结尾,不会在中间出现
1级日志内容会比较少(因为会在战斗中生成,每场战斗要重置),是重点的同步信息。这份日志主要是用来判断各客户端战斗是否同步使用的,同步判断包括两部分:
- 随机数取值一致
- 实体状态一致
2级日志,是用来辅助1级日志查找不同步问题,但是内容会相对3级日志更偏重联网战斗一些
3级日志,就是更广范围的日志,包含游戏其他功能模块的处理逻辑日志打印
不同步判定&收集:
战斗结束后,客户端战斗产生的1级日志内容,压缩为MD5,上传给服务器
服务器收集各客户端MD5,进行比较;判定战斗不同步即分版本存储本场战斗录像(所有的帧操作)
- 任意客户端可在debug模式中,拉取不同步的战斗进行回放现场
重现不同步
通过不同步收集,可以获取到不同步的战斗信息。
剩下的就是重现不同,这里采用的方式很简单,就是不断的跑这场战斗。
比如,跑100次战斗,将MD5不同的日志收集起来,进行比对,进而修改,直至这100场战斗的日志产生的MD5均一致。
其实,只要能重现BUG,就离解决不远了,而且是能无限重现的BUG。
其他项
前期测试时期,可以让服务器把同步的战斗操作也存储,然后拉去下来本地跑100场。
100场也只是一个样例,嫌多可以跑30场,200场,都随意。
善于利用闲置电脑,让所有客户端都能替你跑不同步测试
- 我是在游戏内开发一套debug工具,配合服务器,可以拉取各个角色的回放信息,并进行不同步测试。在战斗时,把自己带入为双方中的任何一方视角进行战斗并输出日志。
- 后期也支持,只跑逻辑,不跑绘制的战斗,这样使得测试时间大大缩短。(还顺便检测出了一些BUG)
优化效果
通过最新一期的线上测试;大概近6000场战斗,总的不同步率 及 各战斗模式的不同步率 均已经降低到了 0.1%以下。(包含 1V1 PK,多人组队战斗等)。
可能还是战斗场次不够多,需要更大量的数据来检测,但是,起码目前已经到及格线,下一步就是调整优化延迟了。
联网战斗就是这样:
延迟调完调同步,同步调完调延迟。
下一步方向
现在已经算是一个闭环的方案了。
- 发现问题
- 重现问题
- 解决问题
- 验证问题
- 收集问题
- 主动收集
- 被动收集
- 提前发现问题
各个方面都有解决方案了,剩下的就是基于这个基础上再去不断的优化完善。
在不同步上,目前待测试的:
- 浮点数问题
不知不觉已经做了这么多,可惜最后无法见证最终的效果,时也命也。
仅愿:
功成须献捷,未必去经年。
番外
这些是在优化过程中遇到的一些问题,采用的一些方法,一些策略
番外:痛苦的不同步
每次接到不同步的反馈,都是异常痛苦的,就如之前所说。
同步模块做的很多的事情都是大方向框架性的,具体的流畅问题、不同步问题,往往是负责最繁琐复杂的发现问题的角色,
然后带着发现的问题找相应模块负责人,去反馈。(最麻烦的就是发现问题,一旦定位问题,距离解决基本不远了)
但是,
同步方面的问题,往往是上线前无人问津,上线后铢锱必较。
一旦出了问题,虽然能把锅分分钟甩出去,但是最终留下来加班修改的还是自己;所以,解决这个问题才是关键。
分析
上线前为什么会无人问津?
- 代码尚在修改,测试收效甚微
- 数据尚未到位,测试范围不足
- 待到代码和数据双双齐备,距离上线不足弹指一挥间,甚至上线后依然在修改调整
- 测试方法繁琐复杂
解决
- 针对代码修改问题;可以提高版本内相关功能优先级,先修改相关代码,再做其他相关功能
- 针对数据到位问题;可以分批次到位,分批次测试,而非最终一口吃个胖子
- 针对测试方法繁琐复杂;可以实现自动化测试,提供测试工具
自动化测试,本不应该属于这片内容。
作为联网战斗的实现,处理同步问题,是重中之重。
处理方向有两个:
- 预防不同步,在出现前,扼杀在摇篮中
- 解决不同步,当出现时,如何快速定位并解决
预防不同步,就是使用自动化测试的方案,针对策划所配置的所有项都提前做好测试。做一个改动后,就相关影响的战斗,都多跑几遍;就是把人力做的事情,通过技术来自动化跑。
解决不同步,可以分为三个部分:
- 收集
- 重现
- 验证
收集,就是当出现不同步问题的时候,我们一定要知道;
重现,就是我们要对现场进行重现,这样就方便查找问题,进而验证是否解决问题。
这些通过上面优化方案的自动化工具都已经初具模型。
番外:集思广益
以前做东西,习惯闷头开发,因为为了做这方面的内容,查阅资料,实验功能,做了各种各样的努力,对这方面的认知和理解还是有一定的把控的。
但是,“不识庐山真面目,只缘身在此山中”,有时候往往会自己把自己限制住,对一个问题不同高度,不同角度,不同层次的解析,也是值得尝试的。
于是,这次优化,准备了以下内容,然后先在内部范围开了个会,收集大家的意见:
- 整个机制的框架
- 上次测试的数据
- 针对测试数据分析的原因
- 针对分析的原因,准备做的优化方向
事实证明,通过这次会议,收集到了很多有用的建议与方案,回去做了针对性的调整,再去实施。
番外:延迟与平滑的博弈
联网战斗的效果,最终都是归咎与延迟与平滑的博弈,效果平滑,完全可以通过最粗暴的高延迟实现,但是动作游戏的高延迟还是比较影响玩家体验的,所以就需要不断的调整优化,找到那个平衡点。
下面是一个经典的样例:
- 以当前帧池数量 n 为准,差值释放,假定平均3次释放完毕,每次释放 n/3
- 注重延迟,当需要快播,每次选择最大释放帧数释放(3帧),保证3次释放即可恢复(100ms内)
- 假设帧池中有5帧,选择
- 释放3帧 => 5-3=2 ,已恢复现场
- 假设帧池中有5帧,选择
- 注重平滑,当需要快播,每次选择尽可能平滑的方式去释放帧
- 假设帧池中有5帧,选择
- 释放2帧 => 5-2=3
- 释放2帧 => 3-2=1 ,已恢复现场
- 假设帧池中有5帧,选择
- 平衡平滑与延迟,指定每个数值缓存帧时的缓存策略,
注意:
- 快播时,是否重新计算该释放的帧数
番外:电脑与收集MD5不一致
在重现不同步时,手机产生的MD5与PC产生的MD5不一致。
经过一系列实验检测发现,在时间种子传输过程中(Lua -> C++),种子的数据类型发生变化,导致种子数值发生了改变。
番外:卡顿反馈
为了更近一步手机玩家反馈,同时也不影响玩家正常体验。
在联网战斗后,结算界面添加反馈按钮,收集第一手玩家信息。
然后,设定一个卡顿阈值来自动弹出反馈面板(该面板允许玩家选择永久不自动弹出)。
参考: