最近在做联网战斗同步这块的东西,读了不少文章、书籍,于是整理了一下。
之前也有在 团队内部技术分享 中分享过这块内容,但是有些东西受限于时间,只是大概的略过,重点放在了实现与遇到的难题解决上。
后来,在做优化调整的时候,又有不少新的收获,改进了之前的分享稿。
欢迎各位小伙伴来一起讨论,通过分享讨论来不断进步。
网络游戏的同步方案,大概由以下三部分搭配组成
分类
共同点
区别
UDP | TCP | |
---|---|---|
传输可靠性 | 不可靠 | 可靠 |
传输速度 | 快 | 慢 |
带宽 | 包头小,省 | 包头大,费 |
连接速度 | 快 | 慢 |
其他
分类
共同
对比
帧同步 | 状态同步 | |
---|---|---|
流量 | 通常较低,取决于玩家数量 | 通常较高,取决于该客户端可观察到的网络实体数量 |
预表现 | 难,客户端本地计算,进行回滚等 | 较易,客户端进行预表现,服务器进行权威演算,客户端最终和服务器下发的状态进行调节或回滚 |
确定性 | 严格确定性 | 不严格确定性 |
弱网影响 | 大,较难做到预表现 | 小,较易做到预表现 |
断线重连 | 难,需要获取所有相关帧且快播追上进度 | 易,根据快照迅速恢复 |
实时回放 | 难,客户端需要消耗非常大性能去从头播放到对应序列,回放完后需要快播追赶 | 易,根据快照进行回放,回放完再根据快照恢复 |
逻辑性能优化 | 难,客户端需要运算所有逻辑,跟客户端性能强相关 | 易,大部分逻辑可在服务器进行,分担客户端运算压力 |
外挂影响 | 大,客户端拥有所有信息,透视类外挂影响严重 | 小,服务器可做视野剔除等处理 |
开发特征 | 平时开发高效,不需要前后端联调;但是开发时要保证各模块确定性,不同步BUG出现,难以排查 | 平时开发效率一般,需要前后端联调,无不同步BUG |
第三方库影响 | 大,第三方库需要确保确定性 | 小,第三方库不需要确保确定性 |
分类
共同
对比
P2P结构 | CS结构 | |
---|---|---|
样式 | 全连接的网状结构 | 星状结构 |
连接数 | O(n^2) | O(n) |
流量 | 各客户端相等,均为 O(n^2) | 服务器为 O(n),客户端为 O(1) |
客户端间的时延 | 较小,为RTT/2 | 较大,为RTT |
广义上来说,游戏采用的技术是:
关联类
功能
功能
功能
如同帧同步的简介中介绍,要保证 输出的一致性,先要确保输入、过程、运算的一致性。
浮点数的运算在不同的操作系统,甚至不同的机器上算出来的结果都是有精度差异的。
一般解决该类问题方法:
这里主要麻烦点在于lua支持定点数,lua中的小数是double,需要把lua源码中的基础小数全部替换为定点数。
然后,物理引擎的计算,第三方库的引用(比如随机数),都需要使用定点数。
确定的随机数机制就是保证各个客户端一旦用到随机数,随机出来的值必须是一样的。
得益于计算机的伪随机,通过设定同样的随机种子即可实现。
但是,在客户端内,需要明确区分随机数的类型
这里,为了更明确区分,在客户端做了一层封装:
做好区分,也便于相关日志的打印。
使用战斗类随机数模块:
使用 非战斗类 随机数模块:
当然,也不是绝对的,比如实体相关的有些可以不用战斗类随机数,比如NPC弹出个对话,也是纯显示性的。这里是为了好区分,方便开发,一刀切了。
所有模块都可以分为 draw 与 update 两部分
实现帧同步尤其需要对 逻辑层的数据进行封装与隔离
以位移组件为例:
同理的还有:
做好分离,也便于之后做快照相关的优化。
创建联网战斗场景基类继承自单人战斗场景基类,用来统一控制联网相关的特殊操作,如 传送,协议交互 等。
然后,设置本地战斗变量,用来进行控制,若是本地战斗,交由基类处理。
每次执行一次处理帧操作,具体释放帧数量
断线重连,主要由 联网战斗数据缓存类(CacheNetworkedFight)负责。
验证多个客户端是否同步,主要依赖于随机数及调用随机数的位置。
在联网战斗运行时,会将使用的随机数都打印出来,由于我们随机种子一致,所调用的随机数序列也应该是一致的,辅助以调用随机数的位置信息,战斗结束后对不同客户端的随机种子文件日志比对,可以校验同步。
我处理这块的方式是使用两个日志文件,
两场战斗结束后,用对比工具比较日志,一旦有差异,用更详细的日志信息,进行排查。
联网战斗同步向来不是一个做完就行的东西,而且也没有一套东西,在各个类型游戏通吃的情况。
所以,在实现完基础的同步架构后,还有很长的路要走。
目前只是搭建了一个基础的框架,要真正投入还有下面这些优化项可以做。
下面这些东西,有些已经做了,有些正在做,有些是一些设想,即将做的;欢迎各位伙伴一起来讨论。
在帧同步基础上,进行优化;就是 帧同步+快照 的模式。
其实已经不属于帧同步了,偏向状态同步。
快照作用就是将整个现场备份,缺点是数据量过大。
但是,我们以房间为单位的战斗,尤其适合 帧同步+快照;因为有明确的划分单位;并且房间初始,很多东西都是不需要存储的。
这三者区别,
帧同步 => 没有进度条的播放器;想要看到第6分30秒的内容,必须从头开始看
状态同步 => 有进度条的播放器;知道时间,就可以直接切到相应时间开始播放
帧同步+快照 => 有进度条,但单位是5分钟;要看 6分30秒的内容,不需要从头看,但是也要从第5分钟开始播放,直到6分30秒
帧同步的安全性也是一个重大的问题,可以分为几大部分。
客户端的安全模块,游戏的核心战斗逻辑演算都在客户端进行,所以对于数据的加密,防篡改等都是由安全模块统一处理。
网络模块,对于网络层的外挂,由底层网络模块的加密等处理。
联网战斗系统的防外挂模块
基础的几个方案
防外挂这个东西,就是魔高一尺,道高一丈,不断优化,不断调整的过程,有些东西也不好讲太细,只能说个大概。
解决不同步问题,也是帧同步方案的一大痛点。
对于不同步的处理,可以分为三个部分:发现 -> 重现 -> 解决
作为开发,应该深有感触,如果方便重现,那解决问题就很简单了。
下面的处理方式都是针对传统的不同步处理各个步骤,进行优化设想。
一般出现不同步: 发现不同步 -> 打开日志开关 -> 使用同样的数据源 -> 复现问题 -> 解决问题
发现不同步,最简单粗暴的方式,肯定是人力跑,没有技术成本,纯跑…
但是,缺点很多:
所以,需要一种自动化的测试工具,来进行大量全面的测试。
目前打算是使用 python + jenkins 来部署自动化测试流水线,等测试完,再单独来说一说。
重现不同步,也是很重要的一个步骤,能完美重现,那距离解决就不远了。
这里预期采用的方案是,固定数据源 + 回放机制。
固定数据源
需要和服务器配合,服务器需要存储参战玩家信息及帧内容,便于回放。
前期可以全部存储,但是这样服务器压力会比较大;后期可以将本地战斗产生的同步文件形成MD5,发给服务器;服务器判断各客户端MD5不同,采取缓存录像。
回放机制
需要客户端实现一套根据帧内容回放机制,理论上来讲帧同步的回放还是比较好实现的。
毕竟 确定的输入,确定的运算,确定的过程,都与时间无关联,可以得到确定的输出。
但是,我们需要的是日志文件,所以绘制帧内容可以忽略掉,尽量做到逻辑帧的播放,这样在时间上也会更快。
解决不同步问题,那就相对简单很多了。
实现了上面的发现 与 重现,可以无数次反复执行不同步数据源,验证是否解决也很便捷。
这应该属于发现不同步的部分。
在实际项目中,日志的实现都是比较粗暴的,一般来说线上运行的模块,都不会开启日志文件。因为一般日志文件都会比较大,尤其是查同步问题的日志文件,涉及模块繁多,产生文件体积大。
所以,线上出不同步问题,往往也很难复现并解决,就是无法固定数据源。(不产生校验文件,就不能上传MD5,不能传MD5,服务器无法判断是否不同步,就不会缓存)
如果有一套性能损耗小一些的日志收集系统,会对同步问题的解决有很大的帮助,
正好最近看到了 《腾讯游戏开发精粹》- 第六部分 - 第14章 - 一种高效的帧同步全过程日志输出方案 。
上面的方案也对我有一些启发,之后可以去实验一下。
在实际测验中,会有玩家反馈卡顿情况。
延迟、卡顿的玩家体验,一般可以分为:
而且,不同游戏类型对延迟的敏感度也不同,现在实现的这种偏格斗类型的游戏,对延迟敏感度还是比较高的。
再者,传统帧同步的处理,逻辑上就是比本地操作要慢一帧:
A帧操作 -> B帧上传 -> C帧执行
B ≥ A,C ≥ B+1
最终,还是要用数据来验证延迟的具体位置,可以按照下流程打时间戳,再收集各个数据,来分析并解决延迟与卡顿:
这里列出几个方向:
玩家的位置
玩家的机型
战斗的时间
玩家的运营商
数据收集的选项:
这里还要注意设置阈值,防止某个异常操作,导致数据不准确,拉高或拉低平均值。
甚至可以设置一些字段,来做筛选剔除异常数据。
推送缺点
比如:
游戏进行过程中,所有的相关模块:
实体管理器 - EntityManager
场景管理器 - SceneManager
碰撞管理器 - AECollision
摄像机管理器 - CameraManager
等等
这些模块的更新,都是固定顺序执行,所有参数都是确定的,所用随机都是指定随机方法
需要客户端同步的东西,必须通过帧来驱动。
同步,就像一个管理器,它的策略项设计项不难,难点主要在于管理的各个模块的内部实现。因为一场战斗涉及的模块很多,只要有一个模块实现有不同步的地方,整场战斗就不同步了。
在到后期查找不同步原因,也往往是去排查下属模块的实现,可能就是在于遍历方式,随机数的使用,逻辑帧绘制帧等。
主要还是要求:
解决这个问题的方向:
参考资料: